1 | var Delegate = require('dom-delegate');
|
2 | var Sequence = require('./lib/sequence');
|
3 | var Firebase = require('firebase');
|
4 |
|
5 | var insertCss = require('insert-css');
|
6 | var prop = require('propper');
|
7 | var store = require('store');
|
8 | var uniqid = require('uniqid');
|
9 | var extractFbPath = require('./lib/extract-fb-path');
|
10 | var queryAll = require('./lib/query-all');
|
11 | var mapIn = require('map-in');
|
12 |
|
13 | function FirebaseTemplate(options) {
|
14 | options = options || {};
|
15 |
|
16 | var self = this;
|
17 |
|
18 | this.user = {
|
19 | name: options.name || 'Guest'
|
20 | };
|
21 |
|
22 | var root = this.root = options.el || document.querySelector('body');
|
23 |
|
24 | var trackMice = options.trackMice || false;
|
25 |
|
26 | this.userId = store.get('userId');
|
27 | if (!this.userId) {
|
28 | this.userId = uniqid();
|
29 | store.set('userId', this.userId);
|
30 | }
|
31 |
|
32 | var fbRoots = this.fbRoots = {};
|
33 |
|
34 | queryAll('[data-path^=\'https://\']', document.querySelector('html')).map(function(fbRoot) {
|
35 | var rootPath = fbRoot.getAttribute('data-path');
|
36 |
|
37 | var firebaseRef = new Firebase(rootPath);
|
38 |
|
39 | var usersRef = firebaseRef.child('users');
|
40 |
|
41 | var authToken = fbRoot.getAttribute('data-jwt');
|
42 | if (authToken) {
|
43 | firebaseRef.authWithCustomToken(authToken, function(err, authData) {
|
44 | if (err) {
|
45 | throw new Error('Auth Failed ' + err);
|
46 | }
|
47 |
|
48 | console.log('Login Succeeded!', authData);
|
49 | });
|
50 | } else {
|
51 | console.warn('WARNING: data-jwt not given, so assuming anonymous access');
|
52 | }
|
53 |
|
54 | fbRoot.removeAttribute('data-jwt');
|
55 |
|
56 | fbRoots[rootPath] = {
|
57 | dataRef: firebaseRef.child('data'),
|
58 | uploadsRef: firebaseRef.child('uploads'),
|
59 | usersRef: usersRef,
|
60 | userRef: usersRef.child(self.userId),
|
61 | oldData: undefined,
|
62 | newData: store.get('data:' + rootPath) || {},
|
63 | start: function() {
|
64 | var self = this;
|
65 |
|
66 | self.oldData = undefined;
|
67 | self.newData = store.get('data:' + rootPath) || {};
|
68 |
|
69 | self.dataRef.on('value', function(snapshot) {
|
70 | self.newData = snapshot.val();
|
71 | store.set('data:' + rootPath, self.newData);
|
72 |
|
73 | updateDom();
|
74 | });
|
75 |
|
76 | self.usersRef.on('value', function(snapshot) {
|
77 | self.users = snapshot.val();
|
78 | updateDom();
|
79 | });
|
80 | },
|
81 | stop: function() {
|
82 | this.dataRef.off('value');
|
83 | this.oldData = null;
|
84 | this.newData = {};
|
85 | }
|
86 | };
|
87 | });
|
88 |
|
89 | this.lookupFbRoot = function(fbPath) {
|
90 | var root = fbPath.root || extractFbPath.parse(fbPath).root;
|
91 | return fbRoots[root];
|
92 | };
|
93 |
|
94 | var updaters = this.updaters = new Sequence(require('./lib/dom-updaters/index'));
|
95 | var listeners = this.listeners = new Sequence(require('./lib/dom-listeners/index'));
|
96 |
|
97 | var transform = options.transform || identity;
|
98 |
|
99 | insertCss('img[data-path] { cursor: pointer; } .avatar .name { background-color: white; color: black; border: 1px solid #999; } .avatar { position: absolute; background-repeat: no-repeat; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAUCAYAAAC9BQwsAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNAay06AAAAAUdEVYdENyZWF0aW9uIFRpbWUAOC83LzEwvsEqxgAAAe9JREFUOI2N081qE1EUB/Bz7teY3GEGRwYSELsQbBkLboTZFGaW4tZko4us8hozD+ADaBKMD2AK2eQBwmySbLIxCW6sIKEgQqMQQ9o6x02nTNU0OXC5m/O7By7/A57nca21JiKaz+c1y7IKnufxKIoQbivHcQwpZZmuajgcVkzTLG7FlmUVOOcPiYh83yciSnfCpmkWOef7REQAsDu+ggcZ3Bn/D+6EN8FNOI5j3ApvxdvgJrwTzOPxePzCtu07eZjmm/LNfx/DMO7+A33fp9ls9jHDrVbrS6/X+4CIrxDxGWPsiVLq3g2YTeh2u8fNZvMkP5UxdsQ5P5BSlrXWOp+clIhSRHyJiNXsIQCgdrv9aTKZvFZKudkSMMYYAcAFAABj7DkifkbEr0mSvK/X6ycAAI1G45Hruk+JCLXWl9VqNc22o8QYe8w535dSloUQDxhjR/mpRJQKIe47jmPEcYysVCpdGobxU0p5qpQ6LRaLZ0KIH4j4PUmSd4PBgIiIRqPRWwC4jpyoVCppp9NZL5fLc601ua5L0+k0XSwWZ2EYviGiYwAgRPwmhFgZhpHeCHsW4CiKMAgCZllWQSnlCiH2hBB72ccEQcAAAHgG+/3+9V2r1WC9Xv9erVbniPhLKbW0bXt1eHh4EYYh9ft9+ANDWHBBjehDcAAAAABJRU5ErkJggg=="); padding-top: 20px; padding-left: 14px;}');
|
100 |
|
101 | var delegate = new Delegate(root);
|
102 |
|
103 | this.set = function(path, data) {
|
104 | self.dataRef.child(path).set(data);
|
105 | };
|
106 |
|
107 | this.setUserStat = function(name, value) {
|
108 | this.user[name] = value;
|
109 | this.user.updated = Date.now();
|
110 | mapIn(fbRoots, function(fbRoot) {
|
111 | fbRoot.userRef.set(self.user);
|
112 | });
|
113 | };
|
114 |
|
115 | this.start = function() {
|
116 | mapIn(fbRoots, function(fbRoot) {
|
117 | fbRoot.start();
|
118 | });
|
119 |
|
120 | updateDom();
|
121 |
|
122 | registerDomListeners();
|
123 | };
|
124 |
|
125 | this.stop = function() {
|
126 | delegate.off();
|
127 |
|
128 | mapIn(fbRoots, function(fbRoot) {
|
129 | fbRoot.stop();
|
130 | });
|
131 | };
|
132 |
|
133 |
|
134 | this._updateDom = function(after, before) {
|
135 | self.newData = after;
|
136 | self.oldData = before || null;
|
137 | updateDom();
|
138 | };
|
139 |
|
140 | function updateDom() {
|
141 | mapIn(fbRoots, function(fbRoot) {
|
142 | fbRoot.newData = transform(fbRoot.newData) || fbRoot.newData;
|
143 | });
|
144 |
|
145 | queryAll('input[type=date][data-path]', self.root).forEach(function(e) {
|
146 | e.setAttribute('data-format', 'date');
|
147 | e.type = 'text';
|
148 | });
|
149 |
|
150 | updaters.each(function(updater) {
|
151 | if (updater.changed === true) {
|
152 | applyDomChanges(updater.selector, updater.update, updater.dataProp || 'path');
|
153 | return;
|
154 | }
|
155 |
|
156 | var hash = updater.hash || identity;
|
157 |
|
158 | applyDomChanges(updater.selector, function(el, newValue) {
|
159 | var fbPath = extractFbPath(el);
|
160 | var fbRoot = fbRoots[fbPath.root];
|
161 |
|
162 | var path = fbPath.path;
|
163 | var oldValue = prop(fbRoot.oldData || {}, path);
|
164 |
|
165 | if (fbRoot.oldData === undefined || hash(newValue) !== hash(oldValue)) {
|
166 | updater.update.apply(self, [el, newValue, path]);
|
167 | }
|
168 | }, updater.dataProp || 'path');
|
169 | });
|
170 |
|
171 | queryAll('input[type=file][data-path]', self.root).forEach(function(e) {
|
172 | e.name = extractFbPath(e).path;
|
173 | });
|
174 |
|
175 | queryAll('input[disabled][data-path],textarea[input][data-path]', self.root).forEach(function(e) {
|
176 | e.removeAttribute('disabled');
|
177 | e.title = '';
|
178 | });
|
179 |
|
180 | mapIn(self.users || {}, function(stats, userId) {
|
181 | if (stats.updated < Date.now() - 1000 * 60) {
|
182 | return;
|
183 | }
|
184 |
|
185 | if (userId !== self.userId) {
|
186 | var userFocus = stats.focus;
|
187 | if (userFocus) {
|
188 | var focus = self.root.querySelector('[data-path=\'' + userFocus + '\']');
|
189 | if (focus) {
|
190 | focus.disabled = true;
|
191 | focus.title = 'editor: ' + stats.name;
|
192 | }
|
193 | }
|
194 |
|
195 | if (trackMice && stats.pos) {
|
196 | var avatar = document.getElementById('avatar-' + userId);
|
197 | if (!avatar) {
|
198 | avatar = document.createElement('div');
|
199 | avatar.id = 'avatar-' + userId;
|
200 | avatar.className = 'avatar';
|
201 | avatar.innerHTML = '<span class="name">' + stats.name + '</span>';
|
202 | document.querySelector('body').appendChild(avatar);
|
203 | }
|
204 |
|
205 | avatar.style.left = stats.pos.x + 'px';
|
206 | avatar.style.top = stats.pos.y + 'px';
|
207 | }
|
208 | }
|
209 | });
|
210 |
|
211 | mapIn(fbRoots, function(fbRoot) {
|
212 | fbRoot.oldData = fbRoot.newData;
|
213 | });
|
214 |
|
215 | if (options.onDomChanged) {
|
216 | options.onDomChanged.apply(self);
|
217 | }
|
218 | }
|
219 |
|
220 | function registerDomListeners() {
|
221 | delegate.off();
|
222 |
|
223 | listeners.each(function(listener) {
|
224 | delegate.on(listener.event, listener.selector, listener.handler.bind(self));
|
225 | });
|
226 |
|
227 |
|
228 | delegate.on('dblclick', '[disabled]', function(e) {
|
229 | var path = extractFbPath(e.target).full;
|
230 | mapIn(self.users, function(stats, userId) {
|
231 | if (stats.focus === path) {
|
232 | self.usersRef.child(userId).child('focus').set(false);
|
233 | }
|
234 | });
|
235 | });
|
236 |
|
237 | if (trackMice) {
|
238 | delegate.on('mousemove', 'body', debounce(function(e) {
|
239 | var pageX = e.pageX;
|
240 | var pageY = e.pageY;
|
241 | if (pageX === undefined) {
|
242 | pageX = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
|
243 | pageY = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
|
244 | }
|
245 |
|
246 | self.setUserStat('pos', {
|
247 | x: pageX,
|
248 | y: pageY
|
249 | });
|
250 | }, 100, true));
|
251 | }
|
252 | }
|
253 |
|
254 | function debounce(func, wait, immediate) {
|
255 | var timeout;
|
256 | return function() {
|
257 | var context = this;
|
258 | var args = arguments;
|
259 | var later = function() {
|
260 | timeout = null;
|
261 | if (!immediate) {
|
262 | func.apply(context, args);
|
263 | }
|
264 | };
|
265 | var callNow = immediate && !timeout;
|
266 | clearTimeout(timeout);
|
267 | timeout = setTimeout(later, wait);
|
268 | if (callNow) {
|
269 | func.apply(context, args);
|
270 | }
|
271 | };
|
272 | }
|
273 |
|
274 | function applyDomChanges(selector, applier, dataProp) {
|
275 | queryAll(selector, root).forEach(function(el) {
|
276 | var fbPath = extractFbPath(el, dataProp);
|
277 | var fbRoot = fbRoots[fbPath.root];
|
278 |
|
279 | applier.apply(self, [el, prop(fbRoot.newData, fbPath.path), fbPath.path]);
|
280 | });
|
281 | }
|
282 | }
|
283 |
|
284 | function identity(val) {
|
285 | return val;
|
286 | }
|
287 |
|
288 | module.exports = FirebaseTemplate;
|