UNPKG

8.89 kBJavaScriptView Raw
1var Delegate = require('dom-delegate');
2var Sequence = require('./lib/sequence');
3var Firebase = require('firebase');
4
5var insertCss = require('insert-css');
6var prop = require('propper');
7var store = require('store');
8var uniqid = require('uniqid');
9var extractFbPath = require('./lib/extract-fb-path');
10var queryAll = require('./lib/query-all');
11var mapIn = require('map-in');
12
13function 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 // exposed for unit testing
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 // Take over a disabled field
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
284function identity(val) {
285 return val;
286}
287
288module.exports = FirebaseTemplate;