pad.pub0.org/static/js/broadcast.js

755 lines
22 KiB
JavaScript

/**
* 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.
*/
var global = this;
function loadBroadcastJS()
{
// 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
{
if (window.console) console.log.apply(console, arguments);
}
catch (e)
{
if (window.console) 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)
}
updateTimer();
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];
}));
}
global.changesetLoader = {
running: false,
resolved: [],
requestQueue1: [],
requestQueue2: [],
requestQueue3: [],
reqCallbacks: [],
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;
var requestID = Math.floor(Math.random() * 100000);
/*var msg = { "component" : "timeslider",
"type":"CHANGESET_REQ",
"padId": padId,
"token": token,
"protocolVersion": 2,
"data"
{
"start": start,
"granularity": granularity
}};
socket.send(msg);*/
sendSocketMsg("CHANGESET_REQ", {
"start": start,
"granularity": granularity,
"requestID": requestID
});
self.reqCallbacks[requestID] = callback;
/*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
});*/
},
handleSocketResponse: function(message)
{
var self = changesetLoader;
var start = message.data.start;
var granularity = message.data.granularity;
var callback = self.reqCallbacks[message.data.requestID];
delete self.reqCallbacks[message.data.requestID];
self.handleResponse(message.data, start, granularity, callback);
setTimeout(self.loadFromQueue, 10);
},
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 ()
fireWhenAllScriptsAreLoaded.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];
var bgcolor = typeof data.colorId == "number" ? clientVars.colorPalette[data.colorId] : data.colorId;
if (bgcolor && dynamicCSS)
{
dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author)).backgroundColor = bgcolor;
}
authorData[author] = data;
}
}
receiveAuthorData(clientVars.historicalAuthorData);
}