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
8 * Copyright 2009 Google Inc.
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
14 * http://www.apache.org/licenses/LICENSE-2.0
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.
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');
31 var map
= Ace2Common
.map
;
32 var forEach
= Ace2Common
.forEach
;
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
)
38 var changesetLoader
= undefined;
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
)
43 Array
.prototype.indexOf = function(elt
/*, from*/ )
45 var len
= this.length
>>> 0;
47 var from = Number(arguments
[1]) || 0;
48 from = (from < 0) ? Math
.ceil(from) : Math
.floor(from);
49 if (from < 0) from += len
;
51 for (; from < len
; from++)
53 if (from in this && this[from] === elt
) return from;
63 if (window
.console
) console
.log
.apply(console
, arguments
);
67 if (window
.console
) console
.log("error printing: ", e
);
76 document
.execCommand("BackgroundImageCache", false, true);
85 var channelState
= "DISCONNECTED";
87 var appLevelDisconnectReason
= null;
90 currentRevision
: clientVars
.revNum
,
91 currentTime
: clientVars
.currentTime
,
92 currentLines
: Changeset
.splitTextLines(clientVars
.initialStyledContents
.atext
.text
),
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
),
99 // generates a jquery element containing HTML for a line
100 lineToElement: function(line
, aline
)
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();
113 applySpliceToDivs: function(start
, numRemoved
, newLines
)
115 // remove spliced-out lines from DOM
116 for (var i
= start
; i
< start
+ numRemoved
&& i
< this.currentDivs
.length
; i
++)
118 debugLog("removing", this.currentDivs
[i
].attr('id'));
119 this.currentDivs
[i
].remove();
122 // remove spliced-out line divs from currentDivs array
123 this.currentDivs
.splice(start
, numRemoved
);
126 for (var i
= 0; i
< newLines
.length
; i
++)
128 newDivs
.push(this.lineToElement(newLines
[i
], this.alines
[start
+ i
]));
131 // grab the div just before the first one
132 var startDiv
= this.currentDivs
[start
- 1] || null;
134 // insert the div elements into the correct place, in the correct order
135 for (var i
= 0; i
< newDivs
.length
; i
++)
139 startDiv
.after(newDivs
[i
]);
143 $("#padcontent").prepend(newDivs
[i
]);
145 startDiv
= newDivs
[i
];
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
);
156 splice: function(start
, numRemoved
, newLinesVA
)
158 var newLines
= map(Array
.prototype.slice
.call(arguments
, 2), function(s
) {
162 // apply this splice to the divs
163 this.applySpliceToDivs(start
, numRemoved
, newLines
);
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
);
170 // returns the contents of the specified line I
173 return this.currentLines
[i
];
175 // returns the number of lines in the document
178 return this.currentLines
.length
;
181 getActiveAuthors: function()
186 var alines
= self
.alines
;
187 for (var i
= 0; i
< alines
.length
; i
++)
189 Changeset
.eachAttribNumber(alines
[i
], function(n
)
194 if (self
.apool
.getAttribKey(n
) == 'author')
196 var a
= self
.apool
.getAttribValue(n
);
210 function callCatchingErrors(catcher
, func
)
214 wrapRecordingErrors(catcher
, func
)();
221 function wrapRecordingErrors(catcher
, func
)
227 return func
.apply(this, Array
.prototype.slice
.call(arguments
));
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
241 function loadedNewChangeset(changesetForward
, changesetBackward
, revision
, timeDelta
)
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
);
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.
256 function applyChangeset(changeset
, revision
, preventSliderMovement
, timeDelta
)
258 // disable the next 'gotorevision' call handled by a timeslider update
259 if (!preventSliderMovement
)
261 goToRevisionIfEnabledCount
++;
262 BroadcastSlider
.setSliderPosition(revision
);
267 // must mutate attribution lines before text lines
268 Changeset
.mutateAttributionLines(changeset
, padContents
.alines
, padContents
.apool
);
275 Changeset
.mutateTextLines(changeset
, padContents
);
276 padContents
.currentRevision
= revision
;
277 padContents
.currentTime
+= timeDelta
* 1000;
278 debugLog('Time Delta: ', timeDelta
)
281 var authors
= map(padContents
.getActiveAuthors(), function(name
)
283 return authorData
[name
];
286 BroadcastSlider
.setAuthors(authors
);
289 function updateTimer()
291 var zpad = function(str
, length
)
294 while (str
.length
< length
)
301 var date
= new Date(padContents
.currentTime
);
302 var dateFormat = function()
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(""));
317 $('#timer').html(dateFormat());
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
)
326 function goToRevision(newRevision
)
328 padContents
.targetRevision
= newRevision
;
330 var path
= revisionInfo
.getPath(padContents
.currentRevision
, newRevision
);
331 debugLog('newRev: ', padContents
.currentRevision
, path
);
332 if (path
.status
== 'complete')
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
++)
340 changeset
= Changeset
.compose(changeset
, cs
[i
], padContents
.apool
);
341 timeDelta
+= path
.times
[i
];
343 if (changeset
) applyChangeset(changeset
, path
.rev
, true, timeDelta
);
345 else if (path
.status
== "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
)
353 // if we've called goToRevision in the time since, don't goToRevision
354 goToRevision(padContents
.targetRevision
);
357 // do our best with what we have...
358 var cs
= path
.changesets
;
360 var changeset
= cs
[0];
361 var timeDelta
= path
.times
[0];
362 for (var i
= 1; i
< cs
.length
; i
++)
364 changeset
= Changeset
.compose(changeset
, cs
[i
], padContents
.apool
);
365 timeDelta
+= path
.times
[i
];
367 if (changeset
) applyChangeset(changeset
, path
.rev
, true, timeDelta
);
370 if (BroadcastSlider
.getSliderLength() > 10000)
372 var start
= (Math
.floor((newRevision
) / 10000) * 10000); // revision 0 to 10
373 changesetLoader
.queueUp(start
, 100);
376 if (BroadcastSlider
.getSliderLength() > 1000)
378 var start
= (Math
.floor((newRevision
) / 1000) * 1000); // (start from -1, go to 19) + 1
379 changesetLoader
.queueUp(start
, 10);
382 start
= (Math
.floor((newRevision
) / 100) * 100);
384 changesetLoader
.queueUp(start
, 1, update
);
387 var authors
= map(padContents
.getActiveAuthors(), function(name
){
388 return authorData
[name
];
390 BroadcastSlider
.setAuthors(authors
);
400 queueUp: function(revision
, width
, callback
)
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
);
408 var requestQueue
= width
== 1 ? changesetLoader
.requestQueue3
: width
== 10 ? changesetLoader
.requestQueue2
: changesetLoader
.requestQueue1
;
415 if (!changesetLoader
.running
)
417 changesetLoader
.running
= true;
418 setTimeout(changesetLoader
.loadFromQueue
, 10);
421 loadFromQueue: function()
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;
428 self
.running
= false;
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);
438 /*var msg = { "component" : "timeslider",
439 "type":"CHANGESET_REQ",
442 "protocolVersion": 2,
446 "granularity": granularity
451 sendSocketMsg("CHANGESET_REQ", {
453 "granularity": granularity
,
454 "requestID": requestID
457 self
.reqCallbacks
[requestID
] = callback
;
459 /*debugLog("loadinging revision", start, "through ajax");
460 $.getJSON("/ep/pad/changes/" + clientVars.padIdForUrl + "?s=" + start + "&g=" + granularity, function (data, textStatus)
462 if (textStatus !== "success")
464 console.log(textStatus);
465 BroadcastSlider.showReconnectUI();
467 self.handleResponse(data, start, granularity, callback);
469 setTimeout(self.loadFromQueue, 10); // load the next ajax function
472 handleSocketResponse: function(message
)
474 var self
= changesetLoader
;
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
];
481 self
.handleResponse(message
.data
, start
, granularity
, callback
);
482 setTimeout(self
.loadFromQueue
, 10);
484 handleResponse: function(data
, start
, granularity
, callback
)
486 debugLog("response: ", data
);
487 var pool
= (new AttribPool()).fromJsonable(data
.apool
);
488 for (var i
= 0; i
< data
.forwardsChangesets
.length
; i
++)
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
]);
498 if (callback
) callback(start
- 1, start
+ data
.forwardsChangesets
.length
* granularity
- 1);
502 function handleMessageFromServer()
504 debugLog("handleMessage:", arguments
);
505 var obj
= arguments
[0]['data'];
506 var expectedType
= "COLLABROOM";
508 obj
= JSON
.parse(obj
);
509 if (obj
['type'] == expectedType
)
513 if (obj
['type'] == "NEW_CHANGES")
516 var changeset
= Changeset
.moveOpsToNewPool(
517 obj
.changeset
, (new AttribPool()).fromJsonable(obj
.apool
), padContents
.apool
);
519 var changesetBack
= Changeset
.moveOpsToNewPool(
520 obj
.changesetBack
, (new AttribPool()).fromJsonable(obj
.apool
), padContents
.apool
);
522 loadedNewChangeset(changeset
, changesetBack
, obj
.newRev
- 1, obj
.timeDelta
);
524 else if (obj
['type'] == "NEW_AUTHORDATA")
527 authorMap
[obj
.author
] = obj
.data
;
528 receiveAuthorData(authorMap
);
530 var authors
= map(padContents
.getActiveAuthors(),function(name
) {
531 return authorData
[name
];
534 BroadcastSlider
.setAuthors(authors
);
536 else if (obj
['type'] == "NEW_SAVEDREV")
538 var savedRev
= obj
.savedRev
;
539 BroadcastSlider
.addSavedRevision(savedRev
.revNum
, savedRev
);
544 debugLog("incorrect message type: " + obj
['type'] + ", expected " + expectedType
);
548 function handleSocketClosed(params
)
550 debugLog("socket closed!", params
);
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");
567 // setChannelState("RECONNECTING", reason);
572 // BroadcastSlider.showReconnectUI();
573 // setChannelState("DISCONNECTED", reason);
577 function sendMessage(msg
)
579 socket
.postMessage(JSON
.stringify(
587 function setChannelState(newChannelState
, moreInfo
)
589 if (newChannelState
!= channelState
)
591 channelState
= newChannelState
;
592 // callbacks.onChannelStateChange(channelState, moreInfo);
596 function abandonConnection(reason
)
600 socket
.onclosed = function()
602 socket
.onhiccup = function()
607 setChannelState("DISCONNECTED", reason
);
610 /*window['onloadFuncts'] = [];
611 window.onload = function ()
613 window['isloaded'] = true;
614 forEach(window['onloadFuncts'],function (funct)
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()
625 // set up the currentDivs and DOM
626 padContents
.currentDivs
= [];
627 $("#padcontent").html("");
628 for (var i
= 0; i
< padContents
.currentLines
.length
; i
++)
630 var div
= padContents
.lineToElement(padContents
.currentLines
[i
], padContents
.alines
[i
]);
631 padContents
.currentDivs
.push(div
);
632 $("#padcontent").append(div
);
634 debugLog(padContents
.currentDivs
);
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()
642 if (goToRevisionIfEnabledCount
> 0)
644 goToRevisionIfEnabledCount
--;
648 goToRevision
.apply(goToRevision
, arguments
);
656 BroadcastSlider
.onSlider(goToRevisionIfEnabled
);
660 for (var i
= 0; i
< clientVars
.initialChangesets
.length
; i
++)
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);
670 var dynamicCSS
= makeCSSManager('dynamicsyntax');
673 function receiveAuthorData(newAuthorData
)
675 for (var author
in newAuthorData
)
677 var data
= newAuthorData
[author
];
678 var bgcolor
= typeof data
.colorId
== "number" ? clientVars
.colorPalette
[data
.colorId
] : data
.colorId
;
679 if (bgcolor
&& dynamicCSS
)
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
685 authorData
[author
] = data
;
689 receiveAuthorData(clientVars
.historicalAuthorData
);
691 return changesetLoader
;
694 exports
.loadBroadcastJS
= loadBroadcastJS
;