adding all weblabels from weblabels.fsf.org
[weblabels.fsf.org.git] / etherpad.fsf.org / 20130506 / files / broadcast.js
1 /**
2 * This code is mostly from the old Etherpad. Please help us to comment this code.
3 * This helps other people to understand this code better and helps them to improve it.
4 * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
5 */
6
7 /**
8 * Copyright 2009 Google Inc.
9 *
10 * Licensed under the Apache License, Version 2.0 (the "License");
11 * you may not use this file except in compliance with the License.
12 * You may obtain a copy of the License at
13 *
14 * http://www.apache.org/licenses/LICENSE-2.0
15 *
16 * Unless required by applicable law or agreed to in writing, software
17 * distributed under the License is distributed on an "AS-IS" BASIS,
18 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 * See the License for the specific language governing permissions and
20 * limitations under the License.
21 */
22
23 var makeCSSManager = require('/cssmanager').makeCSSManager;
24 var domline = require('/domline').domline;
25 var AttribPool = require('/AttributePoolFactory').createAttributePool;
26 var Changeset = require('/Changeset');
27 var linestylefilter = require('/linestylefilter').linestylefilter;
28 var colorutils = require('/colorutils').colorutils;
29 var Ace2Common = require('./ace2_common');
30
31 var map = Ace2Common.map;
32 var forEach = Ace2Common.forEach;
33
34 // These parameters were global, now they are injected. A reference to the
35 // Timeslider controller would probably be more appropriate.
36 function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider)
37 {
38 var changesetLoader = undefined;
39
40 // Below Array#indexOf code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_indexof.htm
41 if (!Array.prototype.indexOf)
42 {
43 Array.prototype.indexOf = function(elt /*, from*/ )
44 {
45 var len = this.length >>> 0;
46
47 var from = Number(arguments[1]) || 0;
48 from = (from < 0) ? Math.ceil(from) : Math.floor(from);
49 if (from < 0) from += len;
50
51 for (; from < len; from++)
52 {
53 if (from in this && this[from] === elt) return from;
54 }
55 return -1;
56 };
57 }
58
59 function debugLog()
60 {
61 try
62 {
63 if (window.console) console.log.apply(console, arguments);
64 }
65 catch (e)
66 {
67 if (window.console) console.log("error printing: ", e);
68 }
69 }
70
71 // for IE
72 if ($.browser.msie)
73 {
74 try
75 {
76 document.execCommand("BackgroundImageCache", false, true);
77 }
78 catch (e)
79 {}
80 }
81
82
83 var socketId;
84 //var socket;
85 var channelState = "DISCONNECTED";
86
87 var appLevelDisconnectReason = null;
88
89 var padContents = {
90 currentRevision: clientVars.revNum,
91 currentTime: clientVars.currentTime,
92 currentLines: Changeset.splitTextLines(clientVars.initialStyledContents.atext.text),
93 currentDivs: null,
94 // to be filled in once the dom loads
95 apool: (new AttribPool()).fromJsonable(clientVars.initialStyledContents.apool),
96 alines: Changeset.splitAttributionLines(
97 clientVars.initialStyledContents.atext.attribs, clientVars.initialStyledContents.atext.text),
98
99 // generates a jquery element containing HTML for a line
100 lineToElement: function(line, aline)
101 {
102 var element = document.createElement("div");
103 var emptyLine = (line == '\n');
104 var domInfo = domline.createDomLine(!emptyLine, true);
105 linestylefilter.populateDomLine(line, aline, this.apool, domInfo);
106 domInfo.prepareForAdd();
107 element.className = domInfo.node.className;
108 element.innerHTML = domInfo.node.innerHTML;
109 element.id = Math.random();
110 return $(element);
111 },
112
113 applySpliceToDivs: function(start, numRemoved, newLines)
114 {
115 // remove spliced-out lines from DOM
116 for (var i = start; i < start + numRemoved && i < this.currentDivs.length; i++)
117 {
118 debugLog("removing", this.currentDivs[i].attr('id'));
119 this.currentDivs[i].remove();
120 }
121
122 // remove spliced-out line divs from currentDivs array
123 this.currentDivs.splice(start, numRemoved);
124
125 var newDivs = [];
126 for (var i = 0; i < newLines.length; i++)
127 {
128 newDivs.push(this.lineToElement(newLines[i], this.alines[start + i]));
129 }
130
131 // grab the div just before the first one
132 var startDiv = this.currentDivs[start - 1] || null;
133
134 // insert the div elements into the correct place, in the correct order
135 for (var i = 0; i < newDivs.length; i++)
136 {
137 if (startDiv)
138 {
139 startDiv.after(newDivs[i]);
140 }
141 else
142 {
143 $("#padcontent").prepend(newDivs[i]);
144 }
145 startDiv = newDivs[i];
146 }
147
148 // insert new divs into currentDivs array
149 newDivs.unshift(0); // remove 0 elements
150 newDivs.unshift(start);
151 this.currentDivs.splice.apply(this.currentDivs, newDivs);
152 return this;
153 },
154
155 // splice the lines
156 splice: function(start, numRemoved, newLinesVA)
157 {
158 var newLines = map(Array.prototype.slice.call(arguments, 2), function(s) {
159 return s;
160 });
161
162 // apply this splice to the divs
163 this.applySpliceToDivs(start, numRemoved, newLines);
164
165 // call currentLines.splice, to keep the currentLines array up to date
166 newLines.unshift(numRemoved);
167 newLines.unshift(start);
168 this.currentLines.splice.apply(this.currentLines, arguments);
169 },
170 // returns the contents of the specified line I
171 get: function(i)
172 {
173 return this.currentLines[i];
174 },
175 // returns the number of lines in the document
176 length: function()
177 {
178 return this.currentLines.length;
179 },
180
181 getActiveAuthors: function()
182 {
183 var self = this;
184 var authors = [];
185 var seenNums = {};
186 var alines = self.alines;
187 for (var i = 0; i < alines.length; i++)
188 {
189 Changeset.eachAttribNumber(alines[i], function(n)
190 {
191 if (!seenNums[n])
192 {
193 seenNums[n] = true;
194 if (self.apool.getAttribKey(n) == 'author')
195 {
196 var a = self.apool.getAttribValue(n);
197 if (a)
198 {
199 authors.push(a);
200 }
201 }
202 }
203 });
204 }
205 authors.sort();
206 return authors;
207 }
208 };
209
210 function callCatchingErrors(catcher, func)
211 {
212 try
213 {
214 wrapRecordingErrors(catcher, func)();
215 }
216 catch (e)
217 { /*absorb*/
218 }
219 }
220
221 function wrapRecordingErrors(catcher, func)
222 {
223 return function()
224 {
225 try
226 {
227 return func.apply(this, Array.prototype.slice.call(arguments));
228 }
229 catch (e)
230 {
231 // caughtErrors.push(e);
232 // caughtErrorCatchers.push(catcher);
233 // caughtErrorTimes.push(+new Date());
234 // console.dir({catcher: catcher, e: e});
235 debugLog(e); // TODO(kroo): added temporary, to catch errors
236 throw e;
237 }
238 };
239 }
240
241 function loadedNewChangeset(changesetForward, changesetBackward, revision, timeDelta)
242 {
243 var broadcasting = (BroadcastSlider.getSliderPosition() == revisionInfo.latest);
244 debugLog("broadcasting:", broadcasting, BroadcastSlider.getSliderPosition(), revisionInfo.latest, revision);
245 revisionInfo.addChangeset(revision, revision + 1, changesetForward, changesetBackward, timeDelta);
246 BroadcastSlider.setSliderLength(revisionInfo.latest);
247 if (broadcasting) applyChangeset(changesetForward, revision + 1, false, timeDelta);
248 }
249
250 /*
251 At this point, we must be certain that the changeset really does map from
252 the current revision to the specified revision. Any mistakes here will
253 cause the whole slider to get out of sync.
254 */
255
256 function applyChangeset(changeset, revision, preventSliderMovement, timeDelta)
257 {
258 // disable the next 'gotorevision' call handled by a timeslider update
259 if (!preventSliderMovement)
260 {
261 goToRevisionIfEnabledCount++;
262 BroadcastSlider.setSliderPosition(revision);
263 }
264
265 try
266 {
267 // must mutate attribution lines before text lines
268 Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
269 }
270 catch (e)
271 {
272 debugLog(e);
273 }
274
275 Changeset.mutateTextLines(changeset, padContents);
276 padContents.currentRevision = revision;
277 padContents.currentTime += timeDelta * 1000;
278 debugLog('Time Delta: ', timeDelta)
279 updateTimer();
280
281 var authors = map(padContents.getActiveAuthors(), function(name)
282 {
283 return authorData[name];
284 });
285
286 BroadcastSlider.setAuthors(authors);
287 }
288
289 function updateTimer()
290 {
291 var zpad = function(str, length)
292 {
293 str = str + "";
294 while (str.length < length)
295 str = '0' + str;
296 return str;
297 }
298
299
300
301 var date = new Date(padContents.currentTime);
302 var dateFormat = function()
303 {
304 var month = zpad(date.getMonth() + 1, 2);
305 var day = zpad(date.getDate(), 2);
306 var year = (date.getFullYear());
307 var hours = zpad(date.getHours(), 2);
308 var minutes = zpad(date.getMinutes(), 2);
309 var seconds = zpad(date.getSeconds(), 2);
310 return ([month, '/', day, '/', year, ' ', hours, ':', minutes, ':', seconds].join(""));
311 }
312
313
314
315
316
317 $('#timer').html(dateFormat());
318
319 var revisionDate = ["Saved", ["Jan", "Feb", "March", "April", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"][date.getMonth()], date.getDate() + ",", date.getFullYear()].join(" ")
320 $('#revision_date').html(revisionDate)
321
322 }
323
324 updateTimer();
325
326 function goToRevision(newRevision)
327 {
328 padContents.targetRevision = newRevision;
329 var self = this;
330 var path = revisionInfo.getPath(padContents.currentRevision, newRevision);
331 debugLog('newRev: ', padContents.currentRevision, path);
332 if (path.status == 'complete')
333 {
334 var cs = path.changesets;
335 debugLog("status: complete, changesets: ", cs, "path:", path);
336 var changeset = cs[0];
337 var timeDelta = path.times[0];
338 for (var i = 1; i < cs.length; i++)
339 {
340 changeset = Changeset.compose(changeset, cs[i], padContents.apool);
341 timeDelta += path.times[i];
342 }
343 if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
344 }
345 else if (path.status == "partial")
346 {
347 debugLog('partial');
348 var sliderLocation = padContents.currentRevision;
349 // callback is called after changeset information is pulled from server
350 // this may never get called, if the changeset has already been loaded
351 var update = function(start, end)
352 {
353 // if we've called goToRevision in the time since, don't goToRevision
354 goToRevision(padContents.targetRevision);
355 };
356
357 // do our best with what we have...
358 var cs = path.changesets;
359
360 var changeset = cs[0];
361 var timeDelta = path.times[0];
362 for (var i = 1; i < cs.length; i++)
363 {
364 changeset = Changeset.compose(changeset, cs[i], padContents.apool);
365 timeDelta += path.times[i];
366 }
367 if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
368
369
370 if (BroadcastSlider.getSliderLength() > 10000)
371 {
372 var start = (Math.floor((newRevision) / 10000) * 10000); // revision 0 to 10
373 changesetLoader.queueUp(start, 100);
374 }
375
376 if (BroadcastSlider.getSliderLength() > 1000)
377 {
378 var start = (Math.floor((newRevision) / 1000) * 1000); // (start from -1, go to 19) + 1
379 changesetLoader.queueUp(start, 10);
380 }
381
382 start = (Math.floor((newRevision) / 100) * 100);
383
384 changesetLoader.queueUp(start, 1, update);
385 }
386
387 var authors = map(padContents.getActiveAuthors(), function(name){
388 return authorData[name];
389 });
390 BroadcastSlider.setAuthors(authors);
391 }
392
393 changesetLoader = {
394 running: false,
395 resolved: [],
396 requestQueue1: [],
397 requestQueue2: [],
398 requestQueue3: [],
399 reqCallbacks: [],
400 queueUp: function(revision, width, callback)
401 {
402 if (revision < 0) revision = 0;
403 // if(changesetLoader.requestQueue.indexOf(revision) != -1)
404 // return; // already in the queue.
405 if (changesetLoader.resolved.indexOf(revision + "_" + width) != -1) return; // already loaded from the server
406 changesetLoader.resolved.push(revision + "_" + width);
407
408 var requestQueue = width == 1 ? changesetLoader.requestQueue3 : width == 10 ? changesetLoader.requestQueue2 : changesetLoader.requestQueue1;
409 requestQueue.push(
410 {
411 'rev': revision,
412 'res': width,
413 'callback': callback
414 });
415 if (!changesetLoader.running)
416 {
417 changesetLoader.running = true;
418 setTimeout(changesetLoader.loadFromQueue, 10);
419 }
420 },
421 loadFromQueue: function()
422 {
423 var self = changesetLoader;
424 var requestQueue = self.requestQueue1.length > 0 ? self.requestQueue1 : self.requestQueue2.length > 0 ? self.requestQueue2 : self.requestQueue3.length > 0 ? self.requestQueue3 : null;
425
426 if (!requestQueue)
427 {
428 self.running = false;
429 return;
430 }
431
432 var request = requestQueue.pop();
433 var granularity = request.res;
434 var callback = request.callback;
435 var start = request.rev;
436 var requestID = Math.floor(Math.random() * 100000);
437
438 /*var msg = { "component" : "timeslider",
439 "type":"CHANGESET_REQ",
440 "padId": padId,
441 "token": token,
442 "protocolVersion": 2,
443 "data"
444 {
445 "start": start,
446 "granularity": granularity
447 }};
448
449 socket.send(msg);*/
450
451 sendSocketMsg("CHANGESET_REQ", {
452 "start": start,
453 "granularity": granularity,
454 "requestID": requestID
455 });
456
457 self.reqCallbacks[requestID] = callback;
458
459 /*debugLog("loadinging revision", start, "through ajax");
460 $.getJSON("/ep/pad/changes/" + clientVars.padIdForUrl + "?s=" + start + "&g=" + granularity, function (data, textStatus)
461 {
462 if (textStatus !== "success")
463 {
464 console.log(textStatus);
465 BroadcastSlider.showReconnectUI();
466 }
467 self.handleResponse(data, start, granularity, callback);
468
469 setTimeout(self.loadFromQueue, 10); // load the next ajax function
470 });*/
471 },
472 handleSocketResponse: function(message)
473 {
474 var self = changesetLoader;
475
476 var start = message.data.start;
477 var granularity = message.data.granularity;
478 var callback = self.reqCallbacks[message.data.requestID];
479 delete self.reqCallbacks[message.data.requestID];
480
481 self.handleResponse(message.data, start, granularity, callback);
482 setTimeout(self.loadFromQueue, 10);
483 },
484 handleResponse: function(data, start, granularity, callback)
485 {
486 debugLog("response: ", data);
487 var pool = (new AttribPool()).fromJsonable(data.apool);
488 for (var i = 0; i < data.forwardsChangesets.length; i++)
489 {
490 var astart = start + i * granularity - 1; // rev -1 is a blank single line
491 var aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision
492 if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
493 debugLog("adding changeset:", astart, aend);
494 var forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
495 var backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
496 revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
497 }
498 if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
499 }
500 };
501
502 function handleMessageFromServer()
503 {
504 debugLog("handleMessage:", arguments);
505 var obj = arguments[0]['data'];
506 var expectedType = "COLLABROOM";
507
508 obj = JSON.parse(obj);
509 if (obj['type'] == expectedType)
510 {
511 obj = obj['data'];
512
513 if (obj['type'] == "NEW_CHANGES")
514 {
515 debugLog(obj);
516 var changeset = Changeset.moveOpsToNewPool(
517 obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
518
519 var changesetBack = Changeset.moveOpsToNewPool(
520 obj.changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
521
522 loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
523 }
524 else if (obj['type'] == "NEW_AUTHORDATA")
525 {
526 var authorMap = {};
527 authorMap[obj.author] = obj.data;
528 receiveAuthorData(authorMap);
529
530 var authors = map(padContents.getActiveAuthors(),function(name) {
531 return authorData[name];
532 });
533
534 BroadcastSlider.setAuthors(authors);
535 }
536 else if (obj['type'] == "NEW_SAVEDREV")
537 {
538 var savedRev = obj.savedRev;
539 BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
540 }
541 }
542 else
543 {
544 debugLog("incorrect message type: " + obj['type'] + ", expected " + expectedType);
545 }
546 }
547
548 function handleSocketClosed(params)
549 {
550 debugLog("socket closed!", params);
551 socket = null;
552
553 BroadcastSlider.showReconnectUI();
554 // var reason = appLevelDisconnectReason || params.reason;
555 // var shouldReconnect = params.reconnect;
556 // if (shouldReconnect) {
557 // // determine if this is a tight reconnect loop due to weird connectivity problems
558 // // reconnectTimes.push(+new Date());
559 // var TOO_MANY_RECONNECTS = 8;
560 // var TOO_SHORT_A_TIME_MS = 10000;
561 // if (reconnectTimes.length >= TOO_MANY_RECONNECTS &&
562 // ((+new Date()) - reconnectTimes[reconnectTimes.length-TOO_MANY_RECONNECTS]) <
563 // TOO_SHORT_A_TIME_MS) {
564 // setChannelState("DISCONNECTED", "looping");
565 // }
566 // else {
567 // setChannelState("RECONNECTING", reason);
568 // setUpSocket();
569 // }
570 // }
571 // else {
572 // BroadcastSlider.showReconnectUI();
573 // setChannelState("DISCONNECTED", reason);
574 // }
575 }
576
577 function sendMessage(msg)
578 {
579 socket.postMessage(JSON.stringify(
580 {
581 type: "COLLABROOM",
582 data: msg
583 }));
584 }
585
586
587 function setChannelState(newChannelState, moreInfo)
588 {
589 if (newChannelState != channelState)
590 {
591 channelState = newChannelState;
592 // callbacks.onChannelStateChange(channelState, moreInfo);
593 }
594 }
595
596 function abandonConnection(reason)
597 {
598 if (socket)
599 {
600 socket.onclosed = function()
601 {};
602 socket.onhiccup = function()
603 {};
604 socket.disconnect();
605 }
606 socket = null;
607 setChannelState("DISCONNECTED", reason);
608 }
609
610 /*window['onloadFuncts'] = [];
611 window.onload = function ()
612 {
613 window['isloaded'] = true;
614 forEach(window['onloadFuncts'],function (funct)
615 {
616 funct();
617 });
618 };*/
619
620 // to start upon window load, just push a function onto this array
621 //window['onloadFuncts'].push(setUpSocket);
622 //window['onloadFuncts'].push(function ()
623 fireWhenAllScriptsAreLoaded.push(function()
624 {
625 // set up the currentDivs and DOM
626 padContents.currentDivs = [];
627 $("#padcontent").html("");
628 for (var i = 0; i < padContents.currentLines.length; i++)
629 {
630 var div = padContents.lineToElement(padContents.currentLines[i], padContents.alines[i]);
631 padContents.currentDivs.push(div);
632 $("#padcontent").append(div);
633 }
634 debugLog(padContents.currentDivs);
635 });
636
637 // this is necessary to keep infinite loops of events firing,
638 // since goToRevision changes the slider position
639 var goToRevisionIfEnabledCount = 0;
640 var goToRevisionIfEnabled = function()
641 {
642 if (goToRevisionIfEnabledCount > 0)
643 {
644 goToRevisionIfEnabledCount--;
645 }
646 else
647 {
648 goToRevision.apply(goToRevision, arguments);
649 }
650 }
651
652
653
654
655
656 BroadcastSlider.onSlider(goToRevisionIfEnabled);
657
658 (function()
659 {
660 for (var i = 0; i < clientVars.initialChangesets.length; i++)
661 {
662 var csgroup = clientVars.initialChangesets[i];
663 var start = clientVars.initialChangesets[i].start;
664 var granularity = clientVars.initialChangesets[i].granularity;
665 debugLog("loading changest on startup: ", start, granularity, csgroup);
666 changesetLoader.handleResponse(csgroup, start, granularity, null);
667 }
668 })();
669
670 var dynamicCSS = makeCSSManager('dynamicsyntax');
671 var authorData = {};
672
673 function receiveAuthorData(newAuthorData)
674 {
675 for (var author in newAuthorData)
676 {
677 var data = newAuthorData[author];
678 var bgcolor = typeof data.colorId == "number" ? clientVars.colorPalette[data.colorId] : data.colorId;
679 if (bgcolor && dynamicCSS)
680 {
681 var selector = dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author));
682 selector.backgroundColor = bgcolor
683 selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5) ? '#ffffff' : '#000000'; //see ace2_inner.js for the other part
684 }
685 authorData[author] = data;
686 }
687 }
688
689 receiveAuthorData(clientVars.historicalAuthorData);
690
691 return changesetLoader;
692 }
693
694 exports.loadBroadcastJS = loadBroadcastJS;