aboutsummaryrefslogtreecommitdiffstats
path: root/etherpad/src/static/js/broadcast.js
diff options
context:
space:
mode:
Diffstat (limited to 'etherpad/src/static/js/broadcast.js')
-rw-r--r--etherpad/src/static/js/broadcast.js610
1 files changed, 610 insertions, 0 deletions
diff --git a/etherpad/src/static/js/broadcast.js b/etherpad/src/static/js/broadcast.js
new file mode 100644
index 0000000..8ea0a15
--- /dev/null
+++ b/etherpad/src/static/js/broadcast.js
@@ -0,0 +1,610 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// just in case... (todo: this must be somewhere else in the client code.)
+// Below Array#map code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_map.htm
+if (!Array.prototype.map)
+{
+ Array.prototype.map = function(fun /*, thisp*/)
+ {
+ var len = this.length >>> 0;
+ if (typeof fun != "function")
+ throw new TypeError();
+
+ var res = new Array(len);
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++)
+ {
+ if (i in this)
+ res[i] = fun.call(thisp, this[i], i, this);
+ }
+
+ return res;
+ };
+}
+
+// Below Array#forEach code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_foreach.htm
+if (!Array.prototype.forEach)
+{
+ Array.prototype.forEach = function(fun /*, thisp*/)
+ {
+ var len = this.length >>> 0;
+ if (typeof fun != "function")
+ throw new TypeError();
+
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++)
+ {
+ if (i in this)
+ fun.call(thisp, this[i], i, this);
+ }
+ };
+}
+
+// Below Array#indexOf code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_indexof.htm
+if (!Array.prototype.indexOf)
+{
+ Array.prototype.indexOf = function(elt /*, from*/)
+ {
+ var len = this.length >>> 0;
+
+ var from = Number(arguments[1]) || 0;
+ from = (from < 0)
+ ? Math.ceil(from)
+ : Math.floor(from);
+ if (from < 0)
+ from += len;
+
+ for (; from < len; from++)
+ {
+ if (from in this &&
+ this[from] === elt)
+ return from;
+ }
+ return -1;
+ };
+}
+
+function debugLog() {
+ try {
+ // console.log.apply(console, arguments);
+ } catch (e) {console.log("error printing: ",e);}
+}
+
+function randomString() {
+ return "_"+Math.floor(Math.random() * 1000000);
+}
+
+// for IE
+if ($.browser.msie) {
+ try {
+ document.execCommand("BackgroundImageCache", false, true);
+ } catch (e) {}
+}
+
+var userId = "hiddenUser" + randomString();
+var socketId;
+var socket;
+
+var channelState = "DISCONNECTED";
+
+var appLevelDisconnectReason = null;
+
+var padContents = {
+ currentRevision: clientVars.revNum,
+ currentTime : clientVars.currentTime,
+ currentLines: Changeset.splitTextLines(clientVars.initialStyledContents.atext.text),
+ currentDivs : null, // to be filled in once the dom loads
+ apool: (new AttribPool()).fromJsonable(clientVars.initialStyledContents.apool),
+ alines: Changeset.splitAttributionLines(
+ clientVars.initialStyledContents.atext.attribs,
+ clientVars.initialStyledContents.atext.text),
+
+ // generates a jquery element containing HTML for a line
+ lineToElement: function(line, aline) {
+ var element = document.createElement("div");
+ var emptyLine = (line == '\n');
+ var domInfo = domline.createDomLine(! emptyLine, true);
+ linestylefilter.populateDomLine(line, aline, this.apool,
+ domInfo);
+ domInfo.prepareForAdd();
+ element.className = domInfo.node.className;
+ element.innerHTML = domInfo.node.innerHTML;
+ element.id = Math.random();
+ return $(element);
+ },
+
+ applySpliceToDivs: function(start, numRemoved, newLines) {
+ // remove spliced-out lines from DOM
+ for(var i=start; i<start+numRemoved && i<this.currentDivs.length; i++) {
+ debugLog("removing", this.currentDivs[i].attr('id'));
+ this.currentDivs[i].remove();
+ }
+
+ // remove spliced-out line divs from currentDivs array
+ this.currentDivs.splice(start, numRemoved);
+
+ var newDivs = [];
+ for(var i=0;i<newLines.length;i++) {
+ newDivs.push(this.lineToElement(newLines[i],
+ this.alines[start+i]));
+ }
+
+ // grab the div just before the first one
+ var startDiv = this.currentDivs[start-1] || null;
+
+ // insert the div elements into the correct place, in the correct order
+ for(var i=0; i<newDivs.length; i++) {
+ if (startDiv) {
+ startDiv.after(newDivs[i]);
+ }
+ else {
+ $("#padcontent").prepend(newDivs[i]);
+ }
+ startDiv = newDivs[i];
+ }
+
+ // insert new divs into currentDivs array
+ newDivs.unshift(0); // remove 0 elements
+ newDivs.unshift(start);
+ this.currentDivs.splice.apply(this.currentDivs, newDivs);
+ return this;
+ },
+
+ // splice the lines
+ splice: function(start, numRemoved, newLinesVA) {
+ var newLines = Array.prototype.slice.call(arguments, 2).map(
+ function(s) { return s; });
+
+ // apply this splice to the divs
+ this.applySpliceToDivs(start, numRemoved, newLines);
+
+ // call currentLines.splice, to keep the currentLines array up to date
+ newLines.unshift(numRemoved);
+ newLines.unshift(start);
+ this.currentLines.splice.apply(this.currentLines, arguments);
+ },
+ // returns the contents of the specified line I
+ get: function(i) {
+ return this.currentLines[i];
+ },
+ // returns the number of lines in the document
+ length: function() {
+ return this.currentLines.length;
+ },
+
+ getActiveAuthors: function() {
+ var self = this;
+ var authors = [];
+ var seenNums = {};
+ var alines = self.alines;
+ for(var i=0;i<alines.length;i++) {
+ Changeset.eachAttribNumber(alines[i], function(n) {
+ if (! seenNums[n]) {
+ seenNums[n] = true;
+ if (self.apool.getAttribKey(n) == 'author') {
+ var a = self.apool.getAttribValue(n);
+ if (a) {
+ authors.push(a);
+ }
+ }
+ }
+ });
+ }
+ authors.sort();
+ return authors;
+ }
+};
+
+function callCatchingErrors(catcher, func) {
+ try {
+ wrapRecordingErrors(catcher, func)();
+ }
+ catch (e) { /*absorb*/ }
+}
+
+function wrapRecordingErrors(catcher, func) {
+ return function() {
+ try {
+ return func.apply(this, Array.prototype.slice.call(arguments));
+ }
+ catch (e) {
+ // caughtErrors.push(e);
+ // caughtErrorCatchers.push(catcher);
+ // caughtErrorTimes.push(+new Date());
+ // console.dir({catcher: catcher, e: e});
+ debugLog(e); // TODO(kroo): added temporary, to catch errors
+ throw e;
+ }
+ };
+}
+
+function loadedNewChangeset(changesetForward, changesetBackward, revision, timeDelta) {
+ var broadcasting = (BroadcastSlider.getSliderPosition() == revisionInfo.latest);
+ debugLog("broadcasting:", broadcasting, BroadcastSlider.getSliderPosition(), revisionInfo.latest, revision);
+ revisionInfo.addChangeset(revision, revision+1, changesetForward, changesetBackward, timeDelta);
+ BroadcastSlider.setSliderLength(revisionInfo.latest);
+ if(broadcasting)
+ applyChangeset(changesetForward, revision+1, false, timeDelta);
+}
+
+/*
+ At this point, we must be certain that the changeset really does map from
+ the current revision to the specified revision. Any mistakes here will
+ cause the whole slider to get out of sync.
+ */
+function applyChangeset(changeset, revision, preventSliderMovement, timeDelta) {
+ // disable the next 'gotorevision' call handled by a timeslider update
+ if(!preventSliderMovement) {
+ goToRevisionIfEnabledCount ++;
+ BroadcastSlider.setSliderPosition(revision);
+ }
+
+ try {
+ // must mutate attribution lines before text lines
+ Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
+ }catch(e) { debugLog(e); }
+
+ Changeset.mutateTextLines(changeset, padContents);
+ padContents.currentRevision = revision;
+ padContents.currentTime += timeDelta * 1000;
+ debugLog('Time Delta: ',timeDelta)
+ updateTimer();
+ BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) {return authorData[name];}));
+}
+
+function updateTimer() {
+ var zpad = function(str, length) {
+ str = str+"";
+ while(str.length < length)
+ str = '0'+str;
+ return str;
+ }
+ var date = new Date(padContents.currentTime);
+ var dateFormat = function() {
+ var month = zpad(date.getMonth()+1,2);
+ var day = zpad(date.getDate(),2);
+ var year = (date.getFullYear());
+ var hours = zpad(date.getHours(),2);
+ var minutes = zpad(date.getMinutes(),2);
+ var seconds = zpad(date.getSeconds(),2);
+ return ([month,'/',day,'/',year,' ',hours,':',minutes,':',seconds].join(""));
+ }
+
+ $('#timer').html(dateFormat());
+
+ var revisionDate = [
+ "Saved",
+ [
+ "Jan", "Feb", "March", "April", "May", "June",
+ "July", "Aug", "Sept", "Oct", "Nov", "Dec"
+ ][date.getMonth()],
+ date.getDate()+",",
+ date.getFullYear()
+ ].join(" ")
+ $('#revision_date').html(revisionDate)
+
+}
+
+function goToRevision(newRevision) {
+ padContents.targetRevision = newRevision;
+ var self = this;
+ var path = revisionInfo.getPath(padContents.currentRevision, newRevision);
+ debugLog('newRev: ', padContents.currentRevision, path);
+ if( path.status == 'complete') {
+ var cs = path.changesets;
+ debugLog("status: complete, changesets: ",cs, "path:", path);
+ var changeset = cs[0];
+ var timeDelta = path.times[0];
+ for(var i=1; i<cs.length; i++) {
+ changeset = Changeset.compose(changeset, cs[i], padContents.apool);
+ timeDelta += path.times[i];
+ }
+ if(changeset)
+ applyChangeset(changeset, path.rev, true, timeDelta);
+ } else if(path.status == "partial") {
+ debugLog('partial');
+ var sliderLocation = padContents.currentRevision;
+ // callback is called after changeset information is pulled from server
+ // this may never get called, if the changeset has already been loaded
+ var update = function(start, end) {
+ // if we've called goToRevision in the time since, don't goToRevision
+ goToRevision(padContents.targetRevision);
+ };
+
+ // do our best with what we have...
+ var cs = path.changesets;
+
+ var changeset = cs[0];
+ var timeDelta = path.times[0];
+ for(var i=1; i<cs.length; i++) {
+ changeset = Changeset.compose(changeset, cs[i], padContents.apool);
+ timeDelta += path.times[i];
+ }
+ if(changeset)
+ applyChangeset(changeset, path.rev, true, timeDelta);
+
+
+ if(BroadcastSlider.getSliderLength() > 10000) {
+ var start = (Math.floor((newRevision) / 10000) * 10000); // revision 0 to 10
+ changesetLoader.queueUp(start, 100);
+ }
+
+ if(BroadcastSlider.getSliderLength() > 1000) {
+ var start = (Math.floor((newRevision) / 1000) * 1000); // (start from -1, go to 19) + 1
+ changesetLoader.queueUp(start, 10);
+ }
+
+ start = (Math.floor((newRevision) / 100) * 100);
+
+ changesetLoader.queueUp(start, 1, update);
+ }
+ BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) {return authorData[name];}));
+}
+
+var changesetLoader = {
+ running: false,
+ resolved: [],
+ requestQueue1: [],
+ requestQueue2: [],
+ requestQueue3: [],
+ queueUp: function(revision, width, callback) {
+ if(revision < 0) revision = 0;
+ // if(changesetLoader.requestQueue.indexOf(revision) != -1)
+ // return; // already in the queue.
+ if(changesetLoader.resolved.indexOf(revision+"_"+width) != -1)
+ return; // already loaded from the server
+ changesetLoader.resolved.push(revision+"_"+width);
+
+ var requestQueue = width == 1 ? changesetLoader.requestQueue3 :
+ width == 10 ? changesetLoader.requestQueue2 :
+ changesetLoader.requestQueue1;
+ requestQueue.push({'rev': revision, 'res': width, 'callback': callback});
+ if(!changesetLoader.running) {
+ changesetLoader.running = true;
+ setTimeout(changesetLoader.loadFromQueue, 10);
+ }
+ },
+ loadFromQueue: function() {
+ var self = changesetLoader;
+ var requestQueue = self.requestQueue1.length > 0 ? self.requestQueue1 :
+ self.requestQueue2.length > 0 ? self.requestQueue2 :
+ self.requestQueue3.length > 0 ? self.requestQueue3 : null;
+
+ if(!requestQueue) {
+ self.running = false;
+ return;
+ }
+
+ var request = requestQueue.pop();
+ var granularity = request.res;
+ var callback = request.callback;
+ var start = request.rev;
+ debugLog("loadinging revision", start, "through ajax");
+ $.getJSON(
+ "/ep/pad/changes/"+clientVars.padIdForUrl+"?s="+start + "&g="+granularity,
+ function(data, textStatus) {
+ if(textStatus !== "success") {
+ console.log(textStatus);
+ BroadcastSlider.showReconnectUI();
+ }
+ self.handleResponse(data, start, granularity, callback);
+
+ setTimeout(self.loadFromQueue, 10); // load the next ajax function
+ }
+ );
+ },
+ handleResponse: function(data, start, granularity, callback) {
+ debugLog("response: ", data);
+ var pool = (new AttribPool()).fromJsonable(data.apool);
+ for(var i=0; i<data.forwardsChangesets.length; i++) {
+ var astart = start + i * granularity - 1; // rev -1 is a blank single line
+ var aend = start + (i+1) * granularity - 1;// totalRevs is the most recent revision
+ if(aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
+ debugLog("adding changeset:", astart, aend);
+ var forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
+ var backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
+ revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
+ }
+ if(callback)callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
+ }
+};
+
+function handleMessageFromServer() {
+ debugLog("handleMessage:", arguments);
+ var obj = arguments[0]['data'];
+ var expectedType = "COLLABROOM";
+
+ obj = JSON.parse(obj);
+ if (obj['type'] == expectedType) {
+ obj = obj['data'];
+
+ if (obj['type'] == "NEW_CHANGES") {
+ debugLog(obj);
+ var changeset = Changeset.moveOpsToNewPool(
+ obj.changeset, (new AttribPool()).fromJsonable(obj.apool),
+ padContents.apool);
+
+ var changesetBack = Changeset.moveOpsToNewPool(
+ obj.changesetBack, (new AttribPool()).fromJsonable(obj.apool),
+ padContents.apool);
+
+ loadedNewChangeset(changeset, changesetBack, obj.newRev-1, obj.timeDelta);
+ }
+ else if (obj['type'] == "NEW_AUTHORDATA") {
+ var authorMap = {};
+ authorMap[obj.author] = obj.data;
+ receiveAuthorData(authorMap);
+ BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) {return authorData[name];}));
+ } else if (obj['type'] == "NEW_SAVEDREV") {
+ var savedRev = obj.savedRev;
+ BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
+ }
+ } else {
+ debugLog("incorrect message type: " + obj['type'] + ", expected " + expectedType);
+ }
+}
+
+function handleSocketClosed(params) {
+ debugLog("socket closed!", params);
+ socket = null;
+
+ BroadcastSlider.showReconnectUI();
+ // var reason = appLevelDisconnectReason || params.reason;
+ // var shouldReconnect = params.reconnect;
+ // if (shouldReconnect) {
+ // // determine if this is a tight reconnect loop due to weird connectivity problems
+ // // reconnectTimes.push(+new Date());
+ // var TOO_MANY_RECONNECTS = 8;
+ // var TOO_SHORT_A_TIME_MS = 10000;
+ // if (reconnectTimes.length >= TOO_MANY_RECONNECTS &&
+ // ((+new Date()) - reconnectTimes[reconnectTimes.length-TOO_MANY_RECONNECTS]) <
+ // TOO_SHORT_A_TIME_MS) {
+ // setChannelState("DISCONNECTED", "looping");
+ // }
+ // else {
+ // setChannelState("RECONNECTING", reason);
+ // setUpSocket();
+ // }
+ // }
+ // else {
+ // BroadcastSlider.showReconnectUI();
+ // setChannelState("DISCONNECTED", reason);
+ // }
+}
+
+function sendMessage(msg) {
+ socket.postMessage(JSON.stringify({type: "COLLABROOM", data: msg}));
+}
+
+function setUpSocket() {
+ // required for Comet
+ if ((! $.browser.msie) &&
+ (! ($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) {
+ document.domain = document.domain; // for comet
+ }
+
+ var success = false;
+ callCatchingErrors("setUpSocket", function() {
+ appLevelDisconnectReason = null;
+
+ socketId = String(Math.floor(Math.random()*1e12));
+ socket = new WebSocket(socketId);
+ socket.onmessage = wrapRecordingErrors("socket.onmessage", handleMessageFromServer);
+ socket.onclosed = wrapRecordingErrors("socket.onclosed", handleSocketClosed);
+ socket.onopen = wrapRecordingErrors("socket.onopen", function() {
+ setChannelState("CONNECTED");
+ var msg = { type:"CLIENT_READY", roomType:'padview',
+ roomName:'padview/'+clientVars.viewId,
+ data: { lastRev:clientVars.revNum,
+ userInfo:{userId: userId} } };
+ sendMessage(msg);
+ });
+ // socket.onhiccup = wrapRecordingErrors("socket.onhiccup", handleCometHiccup);
+ // socket.onlogmessage = function(x) {debugLog(x); };
+ socket.connect();
+ success = true;
+ });
+ if (success) {
+ //initialStartConnectTime = +new Date();
+ }
+ else {
+ abandonConnection("initsocketfail");
+ }
+}
+
+function setChannelState(newChannelState, moreInfo) {
+ if (newChannelState != channelState) {
+ channelState = newChannelState;
+ // callbacks.onChannelStateChange(channelState, moreInfo);
+ }
+}
+
+function abandonConnection(reason) {
+ if (socket) {
+ socket.onclosed = function() {};
+ socket.onhiccup = function() {};
+ socket.disconnect();
+ }
+ socket = null;
+ setChannelState("DISCONNECTED", reason);
+}
+
+window['onloadFuncts'] = [];
+window.onload = function() {
+ window['isloaded'] = true;
+ window['onloadFuncts'].forEach(function(funct) {
+ funct();
+ });
+};
+
+// to start upon window load, just push a function onto this array
+window['onloadFuncts'].push(setUpSocket);
+window['onloadFuncts'].push(function() {
+ // set up the currentDivs and DOM
+ padContents.currentDivs = [];
+ $("#padcontent").html("");
+ for(var i=0; i<padContents.currentLines.length; i++) {
+ var div = padContents.lineToElement(padContents.currentLines[i],
+ padContents.alines[i]);
+ padContents.currentDivs.push(div);
+ $("#padcontent").append(div);
+ }
+ debugLog(padContents.currentDivs);
+});
+
+// this is necessary to keep infinite loops of events firing,
+// since goToRevision changes the slider position
+var goToRevisionIfEnabledCount = 0;
+var goToRevisionIfEnabled = function() {
+ if(goToRevisionIfEnabledCount > 0) {
+ goToRevisionIfEnabledCount --;
+ } else {
+ goToRevision.apply(goToRevision, arguments);
+ }
+}
+
+BroadcastSlider.onSlider(goToRevisionIfEnabled);
+
+(function() {
+ for(var i=0; i<clientVars.initialChangesets.length; i++) {
+ var csgroup = clientVars.initialChangesets[i];
+ var start = clientVars.initialChangesets[i].start;
+ var granularity = clientVars.initialChangesets[i].granularity;
+ debugLog("loading changest on startup: ", start, granularity, csgroup);
+ changesetLoader.handleResponse(csgroup, start, granularity, null);
+ }
+})();
+
+var dynamicCSS = makeCSSManager('dynamicsyntax');
+var authorData = {};
+
+function receiveAuthorData(newAuthorData) {
+ for(var author in newAuthorData) {
+ var data = newAuthorData[author];
+ if ((typeof data.colorId) == 'number') {
+ var bgcolor = clientVars.colorPalette[data.colorId];
+ if (bgcolor && dynamicCSS) {
+ dynamicCSS.selectorStyle(
+ '.'+linestylefilter.getAuthorClassName(author)).backgroundColor =
+ bgcolor;
+ }
+ }
+ authorData[author] = data;
+ }
+}
+
+receiveAuthorData(clientVars.historicalAuthorData);