diff --git a/src/static/css/timeslider.css b/src/static/css/timeslider.css
index b3c201847..c354858f7 100644
--- a/src/static/css/timeslider.css
+++ b/src/static/css/timeslider.css
@@ -1,7 +1,115 @@
+/*
+ * slider handles (SliderHandle)
+ */
+
+.ui-slider-handle {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ cursor: pointer;
+ position: absolute;
+}
+
+.ui-slider-handle-star {
+ background-image: url(../../static/img/star.png);
+ height: 16px;
+ top: 20px;
+ width: 15px;
+}
+
+.ui-slider-handle-handle {
+ background-image: url(../../static/img/crushed_current_location.png);
+ height: 61px;
+ left: 0;
+ top: -14px;
+ width: 13px;
+}
+
+/*
+ * Steppers
+ */
+
+.stepper {
+ background: url(../../static/img/stepper_buttons.png) 0 0 no-repeat;
+ height: 21px;
+ overflow: hidden;
+ position: absolute;
+}
+#leftstar {
+ background-position: 0 -44px;
+ right: 34px;
+ top: 8px;
+ width: 30px;
+}
+#rightstar {
+ background-position: -29px -44px;
+ right: 5px;
+ top: 8px;
+ width: 30px;
+}
+#leftstep:active {
+ background-position: 0 -22px;
+ right: 34px;
+ top: 20px;
+ width: 30px;
+}
+#leftstep {
+ background-position: 0 -66px;
+ right: 34px;
+ top: 20px;
+ width: 30px;
+}
+#rightstep:active {
+ background-position: -29px -22px;
+ right: 5px;
+ top: 20px;
+ width: 30px;
+}
+#rightstep {
+ background-position: -29px -66px;
+ right: 5px;
+ top: 20px;
+ width: 30px;
+}
+
+#playpause_button,
+#playpause_button_icon {
+ height: 47px;
+ position: absolute;
+ width: 47px;
+}
+#playpause_button {
+ background-image: url(../../static/img/crushed_button_undepressed.png);
+ right: 77px;
+ top: 9px;
+}
+#playpause_button:active {
+ background-image: url(../../static/img/crushed_button_depressed.png);
+ right: 77px;
+ top: 9px;
+}
+#playpause_button_icon {
+ background-image: url(../../static/img/play.png);
+ left: 0;
+ top: 0;
+}
+.pause#playpause_button_icon {
+ background-image: url(../../static/img/pause.png)
+}
+
+
+
+
+
+
+
+#timeslider .slider-handle {
+ position: absolute;
+}
#editorcontainerbox {
overflow: auto;
- top: 40px;
- position: static;
+ top: 120px;
+ position: absolute;
}
#padcontent {
font-size: 12px;
@@ -46,18 +154,7 @@
top: 1px;
width: 100%;
}
-#ui-slider-handle {
- -webkit-user-select: none;
- -moz-user-select: none;
- user-select: none;
- background-image: url(../../static/img/crushed_current_location.png);
- cursor: pointer;
- height: 61px;
- left: 0;
- position: absolute;
- top: 0;
- width: 13px;
-}
+
#ui-slider-bar {
-webkit-user-select: none;
-moz-user-select: none;
@@ -69,66 +166,6 @@
position: relative;
top: 20px;
}
-#playpause_button,
-#playpause_button_icon {
- height: 47px;
- position: absolute;
- width: 47px;
-}
-#playpause_button {
- background-image: url(../../static/img/crushed_button_undepressed.png);
- right: 77px;
- top: 9px;
-}
-#playpause_button_icon {
- background-image: url(../../static/img/play.png);
- left: 0;
- top: 0;
-}
-.pause#playpause_button_icon {
- background-image: url(../../static/img/pause.png)
-}
-#leftstar,
-#rightstar,
-#leftstep,
-#rightstep {
- background: url(../../static/img/stepper_buttons.png) 0 0 no-repeat;
- height: 21px;
- overflow: hidden;
- position: absolute;
-}
-#leftstar {
- background-position: 0 -44px;
- right: 34px;
- top: 8px;
- width: 30px;
-}
-#rightstar {
- background-position: -29px -44px;
- right: 5px;
- top: 8px;
- width: 29px;
-}
-#leftstep {
- background-position: 0 -22px;
- right: 34px;
- top: 20px;
- width: 30px;
-}
-#rightstep {
- background-position: -29px -22px;
- right: 5px;
- top: 20px;
- width: 30px;
-}
-#timeslider .star {
- background-image: url(../../static/img/star.png);
- cursor: pointer;
- height: 16px;
- position: absolute;
- top: 40px;
- width: 15px;
-}
#timeslider #timer {
color: #fff;
font-family: Arial, sans-serif;
@@ -188,6 +225,7 @@
overflow: hidden;
padding-top: 3px;
width: 100%;
+ border-bottom: thin solid #cccccc;
}
.timeslider-bar #editbar {
border-bottom: none;
diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js
index d4bda1110..e75e243e0 100644
--- a/src/static/js/broadcast.js
+++ b/src/static/js/broadcast.js
@@ -1,5 +1,5 @@
/**
- * This code is mostly from the old Etherpad. Please help us to comment this code.
+ * This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
@@ -27,13 +27,12 @@ var Changeset = require('./Changeset');
var linestylefilter = require('./linestylefilter').linestylefilter;
var colorutils = require('./colorutils').colorutils;
var _ = require('./underscore');
-
+require("./jquery.class");
// These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate.
-function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider)
+function loadBroadcastJS(tsclient, fireWhenAllScriptsAreLoaded, BroadcastSlider)
{
- var changesetLoader = undefined;
-
+ var changesetLoader;
// 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)
{
@@ -83,14 +82,14 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
var appLevelDisconnectReason = null;
var padContents = {
- currentRevision: clientVars.collab_client_vars.rev,
- currentTime: clientVars.collab_client_vars.time,
- currentLines: Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
+ currentRevision: tsclient.clientVars.collab_client_vars.rev,
+ currentTime: tsclient.clientVars.collab_client_vars.time,
+ currentLines: Changeset.splitTextLines(tsclient.clientVars.collab_client_vars.initialAttributedText.text),
currentDivs: null,
// to be filled in once the dom loads
- apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
+ apool: (new AttribPool()).fromJsonable(tsclient.clientVars.collab_client_vars.apool),
alines: Changeset.splitAttributionLines(
- clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.text),
+ tsclient.clientVars.collab_client_vars.initialAttributedText.attribs, tsclient.clientVars.collab_client_vars.initialAttributedText.text),
// generates a jquery element containing HTML for a line
lineToElement: function(line, aline)
@@ -250,7 +249,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
*/
function applyChangeset(changeset, revision, preventSliderMovement, timeDelta)
- {
+ {
// disable the next 'gotorevision' call handled by a timeslider update
if (!preventSliderMovement)
{
@@ -274,12 +273,12 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
debugLog('Time Delta: ', timeDelta)
updateTimer();
-
+
var authors = _.map(padContents.getActiveAuthors(), function(name)
{
return authorData[name];
});
-
+
BroadcastSlider.setAuthors(authors);
}
@@ -292,7 +291,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
str = '0' + str;
return str;
}
-
+
var date = new Date(padContents.currentTime);
var dateFormat = function()
{
@@ -307,15 +306,15 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
"month": month,
"year": year,
"hours": hours,
- "minutes": minutes,
+ "minutes": minutes,
"seconds": seconds
}));
}
-
-
-
-
-
+
+
+
+
+
$('#timer').html(dateFormat());
var revisionDate = html10n.get("timeslider.saved", {
"day": date.getDate(),
@@ -338,7 +337,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
$('#revision_date').html(revisionDate)
}
-
+
updateTimer();
function goToRevision(newRevision)
@@ -401,7 +400,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
changesetLoader.queueUp(start, 1, update);
}
-
+
var authors = _.map(padContents.getActiveAuthors(), function(name){
return authorData[name];
});
@@ -453,7 +452,8 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
var start = request.rev;
var requestID = Math.floor(Math.random() * 100000);
- sendSocketMsg("CHANGESET_REQ", {
+ //sendSocketMsg("CHANGESET_REQ", {
+ tsclient.sendMessage("CHANGESET_REQ", {
"start": start,
"granularity": granularity,
"requestID": requestID
@@ -461,21 +461,21 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
self.reqCallbacks[requestID] = callback;
},
- handleSocketResponse: function(message)
+ handle_CHANGESET_REQ: function(data)
{
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];
+ var start = data.start;
+ var granularity = data.granularity;
+ var callback = self.reqCallbacks[data.requestID];
+ delete self.reqCallbacks[data.requestID];
- self.handleResponse(message.data, start, granularity, callback);
+ self.handleResponse(data, start, granularity, callback);
setTimeout(self.loadFromQueue, 10);
},
handleResponse: function(data, start, granularity, callback)
{
- debugLog("response: ", data);
+ debugLog("handleResponse: ", data);
var pool = (new AttribPool()).fromJsonable(data.apool);
for (var i = 0; i < data.forwardsChangesets.length; i++)
{
@@ -489,56 +489,44 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
}
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
},
- handleMessageFromServer: function (obj)
+ handle_COLLABROOM: function (obj)
{
- debugLog("handleMessage:", arguments);
-
- if (obj.type == "COLLABROOM")
+ debugLog("handle_COLLABROOM:", arguments);
+ if (obj.type == "NEW_CHANGES")
{
- obj = obj.data;
+ debugLog(obj);
+ var changeset = Changeset.moveOpsToNewPool(
+ obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
- if (obj.type == "NEW_CHANGES")
- {
- debugLog(obj);
- var changeset = Changeset.moveOpsToNewPool(
- obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
+ var changesetBack = Changeset.inverse(
+ obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
- var changesetBack = Changeset.inverse(
- obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
+ changesetBack = Changeset.moveOpsToNewPool(
+ changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
- var changesetBack = Changeset.moveOpsToNewPool(
- 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);
-
- var authors = _.map(padContents.getActiveAuthors(), function(name) {
- return authorData[name];
- });
-
- BroadcastSlider.setAuthors(authors);
- }
- else if (obj.type == "NEW_SAVEDREV")
- {
- var savedRev = obj.savedRev;
- BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
- }
+ loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
}
- else if(obj.type == "CHANGESET_REQ")
+ else if (obj.type == "NEW_AUTHORDATA")
{
- changesetLoader.handleSocketResponse(obj);
+ var authorMap = {};
+ authorMap[obj.author] = obj.data;
+ receiveAuthorData(authorMap);
+
+ var authors = _.map(padContents.getActiveAuthors(), function(name) {
+ return authorData[name];
+ });
+
+ BroadcastSlider.setAuthors(authors);
}
- else
+ else if (obj.type == "NEW_SAVEDREV")
{
- debugLog("Unknown message type: " + obj.type);
+ var savedRev = obj.savedRev;
+ BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
}
}
};
+ //tsclient.on("CHANGESET_REQ", changesetLoader.handle_CHANGESET_REQ);
+ //tsclient.on("COLLABROOM", changesetLoader.handle_COLLABROOM);
// to start upon window load, just push a function onto this array
//window['onloadFuncts'].push(setUpSocket);
@@ -547,12 +535,12 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
{
// set up the currentDivs and DOM
padContents.currentDivs = [];
- $("#padcontent").html("");
+ //$("#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);
+ //$("#padcontent").append(div);
}
debugLog(padContents.currentDivs);
});
@@ -570,7 +558,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
goToRevision.apply(goToRevision, arguments);
}
}
-
+
BroadcastSlider.onSlider(goToRevisionIfEnabled);
var dynamicCSS = makeCSSManager('dynamicsyntax');
@@ -581,7 +569,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
for (var author in newAuthorData)
{
var data = newAuthorData[author];
- var bgcolor = typeof data.colorId == "number" ? clientVars.colorPalette[data.colorId] : data.colorId;
+ var bgcolor = typeof data.colorId == "number" ? tsclient.clientVars.colorPalette[data.colorId] : data.colorId;
if (bgcolor && dynamicCSS)
{
var selector = dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author));
@@ -592,7 +580,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
}
}
- receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData);
+ receiveAuthorData(tsclient.clientVars.collab_client_vars.historicalAuthorData);
return changesetLoader;
}
diff --git a/src/static/js/broadcast_revisions.js b/src/static/js/broadcast_revisions.js
deleted file mode 100644
index 1980bdf30..000000000
--- a/src/static/js/broadcast_revisions.js
+++ /dev/null
@@ -1,128 +0,0 @@
-/**
- * This code is mostly from the old Etherpad. Please help us to comment this code.
- * This helps other people to understand this code better and helps them to improve it.
- * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
- */
-
-/**
- * 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.
- */
-// revision info is a skip list whos entries represent a particular revision
-// of the document. These revisions are connected together by various
-// changesets, or deltas, between any two revisions.
-
-function loadBroadcastRevisionsJS()
-{
- function Revision(revNum)
- {
- this.rev = revNum;
- this.changesets = [];
- }
-
- Revision.prototype.addChangeset = function(destIndex, changeset, timeDelta)
- {
- var changesetWrapper = {
- deltaRev: destIndex - this.rev,
- deltaTime: timeDelta,
- getValue: function()
- {
- return changeset;
- }
- };
- this.changesets.push(changesetWrapper);
- this.changesets.sort(function(a, b)
- {
- return (b.deltaRev - a.deltaRev)
- });
- }
-
- revisionInfo = {};
- revisionInfo.addChangeset = function(fromIndex, toIndex, changeset, backChangeset, timeDelta)
- {
- var startRevision = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex);
- var endRevision = revisionInfo[toIndex] || revisionInfo.createNew(toIndex);
- startRevision.addChangeset(toIndex, changeset, timeDelta);
- endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta);
- }
-
- revisionInfo.latest = clientVars.collab_client_vars.rev || -1;
-
- revisionInfo.createNew = function(index)
- {
- revisionInfo[index] = new Revision(index);
- if (index > revisionInfo.latest)
- {
- revisionInfo.latest = index;
- }
-
- return revisionInfo[index];
- }
-
- // assuming that there is a path from fromIndex to toIndex, and that the links
- // are laid out in a skip-list format
- revisionInfo.getPath = function(fromIndex, toIndex)
- {
- var changesets = [];
- var spans = [];
- var times = [];
- var elem = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex);
- if (elem.changesets.length != 0 && fromIndex != toIndex)
- {
- var reverse = !(fromIndex < toIndex)
- while (((elem.rev < toIndex) && !reverse) || ((elem.rev > toIndex) && reverse))
- {
- var couldNotContinue = false;
- var oldRev = elem.rev;
-
- for (var i = reverse ? elem.changesets.length - 1 : 0;
- reverse ? i >= 0 : i < elem.changesets.length;
- i += reverse ? -1 : 1)
- {
- if (((elem.changesets[i].deltaRev < 0) && !reverse) || ((elem.changesets[i].deltaRev > 0) && reverse))
- {
- couldNotContinue = true;
- break;
- }
-
- if (((elem.rev + elem.changesets[i].deltaRev <= toIndex) && !reverse) || ((elem.rev + elem.changesets[i].deltaRev >= toIndex) && reverse))
- {
- var topush = elem.changesets[i];
- changesets.push(topush.getValue());
- spans.push(elem.changesets[i].deltaRev);
- times.push(topush.deltaTime);
- elem = revisionInfo[elem.rev + elem.changesets[i].deltaRev];
- break;
- }
- }
-
- if (couldNotContinue || oldRev == elem.rev) break;
- }
- }
-
- var status = 'partial';
- if (elem.rev == toIndex) status = 'complete';
-
- return {
- 'fromRev': fromIndex,
- 'rev': elem.rev,
- 'status': status,
- 'changesets': changesets,
- 'spans': spans,
- 'times': times
- };
- }
-}
-
-exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS;
diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js
index 8179b7b5f..35da14198 100644
--- a/src/static/js/broadcast_slider.js
+++ b/src/static/js/broadcast_slider.js
@@ -1,9 +1,3 @@
-/**
- * This code is mostly from the old Etherpad. Please help us to comment this code.
- * This helps other people to understand this code better and helps them to improve it.
- * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
- */
-
/**
* Copyright 2009 Google Inc.
*
@@ -20,23 +14,19 @@
* limitations under the License.
*/
- // These parameters were global, now they are injected. A reference to the
- // Timeslider controller would probably be more appropriate.
var _ = require('./underscore');
var padmodals = require('./pad_modals').padmodals;
-function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
+function init(connection, fireWhenAllScriptsAreLoaded)
{
var BroadcastSlider;
(function()
{ // wrap this code in its own namespace
- var sliderLength = 1000;
- var sliderPos = 0;
- var sliderActive = false;
- var slidercallbacks = [];
- var savedRevisions = [];
- var sliderPlaying = false;
+
+
+
+ var clientVars = connection.clientVars;
function disableSelection(element)
{
@@ -48,113 +38,6 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
element.style.MozUserSelect = "none";
element.style.cursor = "default";
}
- var _callSliderCallbacks = function(newval)
- {
- sliderPos = newval;
- for (var i = 0; i < slidercallbacks.length; i++)
- {
- slidercallbacks[i](newval);
- }
- }
-
- var updateSliderElements = function()
- {
- for (var i = 0; i < savedRevisions.length; i++)
- {
- var position = parseInt(savedRevisions[i].attr('pos'));
- savedRevisions[i].css('left', (position * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)) - 1);
- }
- $("#ui-slider-handle").css('left', sliderPos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0));
- }
-
- var addSavedRevision = function(position, info)
- {
- var newSavedRevision = $('
');
- newSavedRevision.addClass("star");
-
- newSavedRevision.attr('pos', position);
- newSavedRevision.css('position', 'absolute');
- newSavedRevision.css('left', (position * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)) - 1);
- $("#timeslider-slider").append(newSavedRevision);
- newSavedRevision.mouseup(function(evt)
- {
- BroadcastSlider.setSliderPosition(position);
- });
- savedRevisions.push(newSavedRevision);
- };
-
- var removeSavedRevision = function(position)
- {
- var element = $("div.star [pos=" + position + "]");
- savedRevisions.remove(element);
- element.remove();
- return element;
- };
-
- /* Begin small 'API' */
-
- function onSlider(callback)
- {
- slidercallbacks.push(callback);
- }
-
- function getSliderPosition()
- {
- return sliderPos;
- }
-
- function setSliderPosition(newpos)
- {
- newpos = Number(newpos);
- if (newpos < 0 || newpos > sliderLength) return;
- if(!newpos){
- newpos = 0; // stops it from displaying NaN if newpos isn't set
- }
- window.location.hash = "#" + newpos;
- $("#ui-slider-handle").css('left', newpos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0));
- $("a.tlink").map(function()
- {
- $(this).attr('href', $(this).attr('thref').replace("%revision%", newpos));
- });
-
- $("#revision_label").html(html10n.get("timeslider.version", { "version": newpos}));
-
- if (newpos == 0)
- {
- $("#leftstar").css('opacity', .5);
- $("#leftstep").css('opacity', .5);
- }
- else
- {
- $("#leftstar").css('opacity', 1);
- $("#leftstep").css('opacity', 1);
- }
-
- if (newpos == sliderLength)
- {
- $("#rightstar").css('opacity', .5);
- $("#rightstep").css('opacity', .5);
- }
- else
- {
- $("#rightstar").css('opacity', 1);
- $("#rightstep").css('opacity', 1);
- }
-
- sliderPos = newpos;
- _callSliderCallbacks(newpos);
- }
-
- function getSliderLength()
- {
- return sliderLength;
- }
-
- function setSliderLength(newlength)
- {
- sliderLength = newlength;
- updateSliderElements();
- }
// just take over the whole slider screen with a reconnect message
@@ -163,117 +46,11 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
padmodals.showModal("disconnected");
}
+ //TODO: figure out what the hell this is for
var fixPadHeight = _.throttle(function(){
var height = $('#timeslider-top').height();
$('#editorcontainerbox').css({marginTop: height});
}, 600);
-
- function setAuthors(authors)
- {
- var authorsList = $("#authorsList");
- authorsList.empty();
- var numAnonymous = 0;
- var numNamed = 0;
- var colorsAnonymous = [];
- _.each(authors, function(author)
- {
- var authorColor = clientVars.colorPalette[author.colorId] || author.colorId;
- if (author.name)
- {
- if (numNamed !== 0) authorsList.append(', ');
-
- $('')
- .text(author.name || "unnamed")
- .css('background-color', authorColor)
- .addClass('author')
- .appendTo(authorsList);
-
- numNamed++;
- }
- else
- {
- numAnonymous++;
- if(authorColor) colorsAnonymous.push(authorColor);
- }
- });
- if (numAnonymous > 0)
- {
- var anonymousAuthorString = html10n.get("timeslider.unnamedauthors", { num: numAnonymous });
-
- if (numNamed !== 0){
- authorsList.append(' + ' + anonymousAuthorString);
- } else {
- authorsList.append(anonymousAuthorString);
- }
-
- if(colorsAnonymous.length > 0){
- authorsList.append(' (');
- _.each(colorsAnonymous, function(color, i){
- if( i > 0 ) authorsList.append(' ');
- $(' ')
- .css('background-color', color)
- .addClass('author author-anonymous')
- .appendTo(authorsList);
- });
- authorsList.append(')');
- }
-
- }
- if (authors.length == 0)
- {
- authorsList.append(html10n.get("timeslider.toolbar.authorsList"));
- }
-
- fixPadHeight();
- }
-
- BroadcastSlider = {
- onSlider: onSlider,
- getSliderPosition: getSliderPosition,
- setSliderPosition: setSliderPosition,
- getSliderLength: getSliderLength,
- setSliderLength: setSliderLength,
- isSliderActive: function()
- {
- return sliderActive;
- },
- playpause: playpause,
- addSavedRevision: addSavedRevision,
- showReconnectUI: showReconnectUI,
- setAuthors: setAuthors
- }
-
- function playButtonUpdater()
- {
- if (sliderPlaying)
- {
- if (getSliderPosition() + 1 > sliderLength)
- {
- $("#playpause_button_icon").toggleClass('pause');
- sliderPlaying = false;
- return;
- }
- setSliderPosition(getSliderPosition() + 1);
-
- setTimeout(playButtonUpdater, 100);
- }
- }
-
- function playpause()
- {
- $("#playpause_button_icon").toggleClass('pause');
-
- if (!sliderPlaying)
- {
- if (getSliderPosition() == sliderLength) setSliderPosition(0);
- sliderPlaying = true;
- playButtonUpdater();
- }
- else
- {
- sliderPlaying = false;
- }
- }
// assign event handlers to html UI elements after page load
//$(window).load(function ()
@@ -281,212 +58,15 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
{
disableSelection($("#playpause_button")[0]);
disableSelection($("#timeslider")[0]);
-
- $(document).keyup(function(e)
- {
- var code = -1;
- if (!e) var e = window.event;
- if (e.keyCode) code = e.keyCode;
- else if (e.which) code = e.which;
-
- if (code == 37)
- { // left
- if (!e.shiftKey)
- {
- setSliderPosition(getSliderPosition() - 1);
- }
- else
- {
- var nextStar = 0; // default to first revision in document
- for (var i = 0; i < savedRevisions.length; i++)
- {
- var pos = parseInt(savedRevisions[i].attr('pos'));
- if (pos < getSliderPosition() && nextStar < pos) nextStar = pos;
- }
- setSliderPosition(nextStar);
- }
- }
- else if (code == 39)
- {
- if (!e.shiftKey)
- {
- setSliderPosition(getSliderPosition() + 1);
- }
- else
- {
- var nextStar = sliderLength; // default to last revision in document
- for (var i = 0; i < savedRevisions.length; i++)
- {
- var pos = parseInt(savedRevisions[i].attr('pos'));
- if (pos > getSliderPosition() && nextStar > pos) nextStar = pos;
- }
- setSliderPosition(nextStar);
- }
- }
- else if (code == 32) playpause();
-
- });
-
- $(window).resize(function()
- {
- updateSliderElements();
- });
-
- $("#ui-slider-bar").mousedown(function(evt)
- {
- setSliderPosition(Math.floor((evt.clientX - $("#ui-slider-bar").offset().left) * sliderLength / 742));
- $("#ui-slider-handle").css('left', (evt.clientX - $("#ui-slider-bar").offset().left));
- $("#ui-slider-handle").trigger(evt);
- });
-
- // Slider dragging
- $("#ui-slider-handle").mousedown(function(evt)
- {
- this.startLoc = evt.clientX;
- this.currentLoc = parseInt($(this).css('left'));
- var self = this;
- sliderActive = true;
- $(document).mousemove(function(evt2)
- {
- $(self).css('pointer', 'move')
- var newloc = self.currentLoc + (evt2.clientX - self.startLoc);
- if (newloc < 0) newloc = 0;
- if (newloc > ($("#ui-slider-bar").width() - 2)) newloc = ($("#ui-slider-bar").width() - 2);
- $("#revision_label").html(html10n.get("timeslider.version", { "version": Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))}));
- $(self).css('left', newloc);
- if (getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))) _callSliderCallbacks(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2)))
- });
- $(document).mouseup(function(evt2)
- {
- $(document).unbind('mousemove');
- $(document).unbind('mouseup');
- sliderActive = false;
- var newloc = self.currentLoc + (evt2.clientX - self.startLoc);
- if (newloc < 0) newloc = 0;
- if (newloc > ($("#ui-slider-bar").width() - 2)) newloc = ($("#ui-slider-bar").width() - 2);
- $(self).css('left', newloc);
- // if(getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2)))
- setSliderPosition(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2)))
- if(parseInt($(self).css('left')) < 2){
- $(self).css('left', '2px');
- }else{
- self.currentLoc = parseInt($(self).css('left'));
- }
- });
- })
-
- // play/pause toggling
- $("#playpause_button").mousedown(function(evt)
- {
- var self = this;
-
- $(self).css('background-image', 'url(/static/img/crushed_button_depressed.png)');
- $(self).mouseup(function(evt2)
- {
- $(self).css('background-image', 'url(/static/img/crushed_button_undepressed.png)');
- $(self).unbind('mouseup');
- BroadcastSlider.playpause();
- });
- $(document).mouseup(function(evt2)
- {
- $(self).css('background-image', 'url(/static/img/crushed_button_undepressed.png)');
- $(document).unbind('mouseup');
- });
- });
-
- // next/prev saved revision and changeset
- $('.stepper').mousedown(function(evt)
- {
- var self = this;
- var origcss = $(self).css('background-position');
- if (!origcss)
- {
- origcss = $(self).css('background-position-x') + " " + $(self).css('background-position-y');
- }
- var origpos = parseInt(origcss.split(" ")[1]);
- var newpos = (origpos - 43);
- if (newpos < 0) newpos += 87;
-
- var newcss = (origcss.split(" ")[0] + " " + newpos + "px");
- if ($(self).css('opacity') != 1.0) newcss = origcss;
-
- $(self).css('background-position', newcss)
-
- $(self).mouseup(function(evt2)
- {
- $(self).css('background-position', origcss);
- $(self).unbind('mouseup');
- $(document).unbind('mouseup');
- if ($(self).attr("id") == ("leftstep"))
- {
- setSliderPosition(getSliderPosition() - 1);
- }
- else if ($(self).attr("id") == ("rightstep"))
- {
- setSliderPosition(getSliderPosition() + 1);
- }
- else if ($(self).attr("id") == ("leftstar"))
- {
- var nextStar = 0; // default to first revision in document
- for (var i = 0; i < savedRevisions.length; i++)
- {
- var pos = parseInt(savedRevisions[i].attr('pos'));
- if (pos < getSliderPosition() && nextStar < pos) nextStar = pos;
- }
- setSliderPosition(nextStar);
- }
- else if ($(self).attr("id") == ("rightstar"))
- {
- var nextStar = sliderLength; // default to last revision in document
- for (var i = 0; i < savedRevisions.length; i++)
- {
- var pos = parseInt(savedRevisions[i].attr('pos'));
- if (pos > getSliderPosition() && nextStar > pos) nextStar = pos;
- }
- setSliderPosition(nextStar);
- }
- });
- $(document).mouseup(function(evt2)
- {
- $(self).css('background-position', origcss);
- $(self).unbind('mouseup');
- $(document).unbind('mouseup');
- });
- })
if (clientVars)
{
$("#timeslider").show();
-
- var startPos = clientVars.collab_client_vars.rev;
- if(window.location.hash.length > 1)
- {
- var hashRev = Number(window.location.hash.substr(1));
- if(!isNaN(hashRev))
- {
- // this is necessary because of the socket.io-event which loads the changesets
- setTimeout(function() { setSliderPosition(hashRev); }, 1);
- }
- }
-
- setSliderLength(clientVars.collab_client_vars.rev);
- setSliderPosition(clientVars.collab_client_vars.rev);
-
- _.each(clientVars.savedRevisions, function(revision)
- {
- addSavedRevision(revision.revNum, revision);
- })
-
+
}
});
})();
- BroadcastSlider.onSlider(function(loc)
- {
- $("#viewlatest").html(loc == BroadcastSlider.getSliderLength() ? "Viewing latest content" : "View latest content");
- })
-
- return BroadcastSlider;
}
-exports.loadBroadcastSliderJS = loadBroadcastSliderJS;
+exports.init = init;
diff --git a/src/static/js/jquery.class.js b/src/static/js/jquery.class.js
new file mode 100644
index 000000000..4657c6ab4
--- /dev/null
+++ b/src/static/js/jquery.class.js
@@ -0,0 +1,836 @@
+(function( $ ) {
+ // Several of the methods in this plugin use code adapated from Prototype
+ // Prototype JavaScript framework, version 1.6.0.1
+ // (c) 2005-2007 Sam Stephenson
+ var regs = {
+ undHash: /_|-/,
+ colons: /::/,
+ words: /([A-Z]+)([A-Z][a-z])/g,
+ lowUp: /([a-z\d])([A-Z])/g,
+ dash: /([a-z\d])([A-Z])/g,
+ replacer: /\{([^\}]+)\}/g,
+ dot: /\./
+ },
+ getNext = function(current, nextPart, add){
+ return current[nextPart] || ( add && (current[nextPart] = {}) );
+ },
+ isContainer = function(current){
+ var type = typeof current;
+ return type && ( type == 'function' || type == 'object' );
+ },
+ getObject = function( objectName, roots, add ) {
+
+ var parts = objectName ? objectName.split(regs.dot) : [],
+ length = parts.length,
+ currents = $.isArray(roots) ? roots : [roots || window],
+ current,
+ ret,
+ i,
+ c = 0,
+ type;
+
+ if(length == 0){
+ return currents[0];
+ }
+ while(current = currents[c++]){
+ for (i =0; i < length - 1 && isContainer(current); i++ ) {
+ current = getNext(current, parts[i], add);
+ }
+ if( isContainer(current) ) {
+
+ ret = getNext(current, parts[i], add);
+
+ if( ret !== undefined ) {
+
+ if ( add === false ) {
+ delete current[parts[i]];
+ }
+ return ret;
+
+ }
+
+ }
+ }
+ },
+
+ /**
+ * @class jQuery.String
+ *
+ * A collection of useful string helpers.
+ *
+ */
+ str = $.String = $.extend( $.String || {} , {
+ /**
+ * @function
+ * Gets an object from a string.
+ * @param {String} name the name of the object to look for
+ * @param {Array} [roots] an array of root objects to look for the name
+ * @param {Boolean} [add] true to add missing objects to
+ * the path. false to remove found properties. undefined to
+ * not modify the root object
+ */
+ getObject : getObject,
+ /**
+ * Capitalizes a string
+ * @param {String} s the string.
+ * @return {String} a string with the first character capitalized.
+ */
+ capitalize: function( s, cache ) {
+ return s.charAt(0).toUpperCase() + s.substr(1);
+ },
+ /**
+ * Capitalizes a string from something undercored. Examples:
+ * @codestart
+ * jQuery.String.camelize("one_two") //-> "oneTwo"
+ * "three-four".camelize() //-> threeFour
+ * @codeend
+ * @param {String} s
+ * @return {String} a the camelized string
+ */
+ camelize: function( s ) {
+ s = str.classize(s);
+ return s.charAt(0).toLowerCase() + s.substr(1);
+ },
+ /**
+ * Like camelize, but the first part is also capitalized
+ * @param {String} s
+ * @return {String} the classized string
+ */
+ classize: function( s , join) {
+ var parts = s.split(regs.undHash),
+ i = 0;
+ for (; i < parts.length; i++ ) {
+ parts[i] = str.capitalize(parts[i]);
+ }
+
+ return parts.join(join || '');
+ },
+ /**
+ * Like [jQuery.String.classize|classize], but a space separates each 'word'
+ * @codestart
+ * jQuery.String.niceName("one_two") //-> "One Two"
+ * @codeend
+ * @param {String} s
+ * @return {String} the niceName
+ */
+ niceName: function( s ) {
+ str.classize(parts[i],' ');
+ },
+
+ /**
+ * Underscores a string.
+ * @codestart
+ * jQuery.String.underscore("OneTwo") //-> "one_two"
+ * @codeend
+ * @param {String} s
+ * @return {String} the underscored string
+ */
+ underscore: function( s ) {
+ return s.replace(regs.colons, '/').replace(regs.words, '$1_$2').replace(regs.lowUp, '$1_$2').replace(regs.dash, '_').toLowerCase();
+ },
+ /**
+ * Returns a string with {param} replaced values from data.
+ *
+ * $.String.sub("foo {bar}",{bar: "far"})
+ * //-> "foo far"
+ *
+ * @param {String} s The string to replace
+ * @param {Object} data The data to be used to look for properties. If it's an array, multiple
+ * objects can be used.
+ * @param {Boolean} [remove] if a match is found, remove the property from the object
+ */
+ sub: function( s, data, remove ) {
+ var obs = [];
+ obs.push(s.replace(regs.replacer, function( whole, inside ) {
+ //convert inside to type
+ var ob = getObject(inside, data, typeof remove == 'boolean' ? !remove : remove),
+ type = typeof ob;
+ if((type === 'object' || type === 'function') && type !== null){
+ obs.push(ob);
+ return "";
+ }else{
+ return ""+ob;
+ }
+ }));
+ return obs.length <= 1 ? obs[0] : obs;
+ }
+ });
+
+})(jQuery);
+(function( $ ) {
+
+ // if we are initializing a new class
+ var initializing = false,
+ makeArray = $.makeArray,
+ isFunction = $.isFunction,
+ isArray = $.isArray,
+ extend = $.extend,
+ concatArgs = function(arr, args){
+ return arr.concat(makeArray(args));
+ },
+ // tests if we can get super in .toString()
+ fnTest = /xyz/.test(function() {
+ xyz;
+ }) ? /\b_super\b/ : /.*/,
+ // overwrites an object with methods, sets up _super
+ // newProps - new properties
+ // oldProps - where the old properties might be
+ // addTo - what we are adding to
+ inheritProps = function( newProps, oldProps, addTo ) {
+ addTo = addTo || newProps
+ for ( var name in newProps ) {
+ // Check if we're overwriting an existing function
+ addTo[name] = isFunction(newProps[name]) &&
+ isFunction(oldProps[name]) &&
+ fnTest.test(newProps[name]) ? (function( name, fn ) {
+ return function() {
+ var tmp = this._super,
+ ret;
+
+ // Add a new ._super() method that is the same method
+ // but on the super-class
+ this._super = oldProps[name];
+
+ // The method only need to be bound temporarily, so we
+ // remove it when we're done executing
+ ret = fn.apply(this, arguments);
+ this._super = tmp;
+ return ret;
+ };
+ })(name, newProps[name]) : newProps[name];
+ }
+ },
+
+
+ /**
+ * @class jQuery.Class
+ * @plugin jquery/class
+ * @tag core
+ * @download dist/jquery/jquery.class.js
+ * @test jquery/class/qunit.html
+ *
+ * Class provides simulated inheritance in JavaScript. Use clss to bridge the gap between
+ * jQuery's functional programming style and Object Oriented Programming. It
+ * is based off John Resig's [http://ejohn.org/blog/simple-javascript-inheritance/|Simple Class]
+ * Inheritance library. Besides prototypal inheritance, it includes a few important features:
+ *
+ * - Static inheritance
+ * - Introspection
+ * - Namespaces
+ * - Setup and initialization methods
+ * - Easy callback function creation
+ *
+ *
+ * ## Static v. Prototype
+ *
+ * Before learning about Class, it's important to
+ * understand the difference between
+ * a class's __static__ and __prototype__ properties.
+ *
+ * //STATIC
+ * MyClass.staticProperty //shared property
+ *
+ * //PROTOTYPE
+ * myclass = new MyClass()
+ * myclass.prototypeMethod() //instance method
+ *
+ * A static (or class) property is on the Class constructor
+ * function itself
+ * and can be thought of being shared by all instances of the
+ * Class. Prototype propertes are available only on instances of the Class.
+ *
+ * ## A Basic Class
+ *
+ * The following creates a Monster class with a
+ * name (for introspection), static, and prototype members.
+ * Every time a monster instance is created, the static
+ * count is incremented.
+ *
+ * @codestart
+ * $.Class.extend('Monster',
+ * /* @static *|
+ * {
+ * count: 0
+ * },
+ * /* @prototype *|
+ * {
+ * init: function( name ) {
+ *
+ * // saves name on the monster instance
+ * this.name = name;
+ *
+ * // sets the health
+ * this.health = 10;
+ *
+ * // increments count
+ * this.Class.count++;
+ * },
+ * eat: function( smallChildren ){
+ * this.health += smallChildren;
+ * },
+ * fight: function() {
+ * this.health -= 2;
+ * }
+ * });
+ *
+ * hydra = new Monster('hydra');
+ *
+ * dragon = new Monster('dragon');
+ *
+ * hydra.name // -> hydra
+ * Monster.count // -> 2
+ * Monster.shortName // -> 'Monster'
+ *
+ * hydra.eat(2); // health = 12
+ *
+ * dragon.fight(); // health = 8
+ *
+ * @codeend
+ *
+ *
+ * Notice that the prototype init function is called when a new instance of Monster is created.
+ *
+ *
+ * ## Inheritance
+ *
+ * When a class is extended, all static and prototype properties are available on the new class.
+ * If you overwrite a function, you can call the base class's function by calling
+ * this._super
. Lets create a SeaMonster class. SeaMonsters are less
+ * efficient at eating small children, but more powerful fighters.
+ *
+ *
+ * Monster.extend("SeaMonster",{
+ * eat: function( smallChildren ) {
+ * this._super(smallChildren / 2);
+ * },
+ * fight: function() {
+ * this.health -= 1;
+ * }
+ * });
+ *
+ * lockNess = new SeaMonster('Lock Ness');
+ * lockNess.eat(4); //health = 12
+ * lockNess.fight(); //health = 11
+ *
+ * ### Static property inheritance
+ *
+ * You can also inherit static properties in the same way:
+ *
+ * $.Class.extend("First",
+ * {
+ * staticMethod: function() { return 1;}
+ * },{})
+ *
+ * First.extend("Second",{
+ * staticMethod: function() { return this._super()+1;}
+ * },{})
+ *
+ * Second.staticMethod() // -> 2
+ *
+ * ## Namespaces
+ *
+ * Namespaces are a good idea! We encourage you to namespace all of your code.
+ * It makes it possible to drop your code into another app without problems.
+ * Making a namespaced class is easy:
+ *
+ * @codestart
+ * $.Class.extend("MyNamespace.MyClass",{},{});
+ *
+ * new MyNamespace.MyClass()
+ * @codeend
+ * Introspection
+ * Often, it's nice to create classes whose name helps determine functionality. Ruby on
+ * Rails's [http://api.rubyonrails.org/classes/ActiveRecord/Base.html|ActiveRecord] ORM class
+ * is a great example of this. Unfortunately, JavaScript doesn't have a way of determining
+ * an object's name, so the developer must provide a name. Class fixes this by taking a String name for the class.
+ * @codestart
+ * $.Class.extend("MyOrg.MyClass",{},{})
+ * MyOrg.MyClass.shortName //-> 'MyClass'
+ * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass'
+ * @codeend
+ * The fullName (with namespaces) and the shortName (without namespaces) are added to the Class's
+ * static properties.
+ *
+ *
+ * Setup and initialization methods
+ *
+ * Class provides static and prototype initialization functions.
+ * These come in two flavors - setup and init.
+ * Setup is called before init and
+ * can be used to 'normalize' init's arguments.
+ *
+ * PRO TIP: Typically, you don't need setup methods in your classes. Use Init instead.
+ * Reserve setup methods for when you need to do complex pre-processing of your class before init is called.
+ *
+ *
+ * @codestart
+ * $.Class.extend("MyClass",
+ * {
+ * setup: function() {} //static setup
+ * init: function() {} //static constructor
+ * },
+ * {
+ * setup: function() {} //prototype setup
+ * init: function() {} //prototype constructor
+ * })
+ * @codeend
+ *
+ * Setup
+ * Setup functions are called before init functions. Static setup functions are passed
+ * the base class followed by arguments passed to the extend function.
+ * Prototype static functions are passed the Class constructor function arguments.
+ * If a setup function returns an array, that array will be used as the arguments
+ * for the following init method. This provides setup functions the ability to normalize
+ * arguments passed to the init constructors. They are also excellent places
+ * to put setup code you want to almost always run.
+ *
+ * The following is similar to how [jQuery.Controller.prototype.setup]
+ * makes sure init is always called with a jQuery element and merged options
+ * even if it is passed a raw
+ * HTMLElement and no second parameter.
+ *
+ * @codestart
+ * $.Class.extend("jQuery.Controller",{
+ * ...
+ * },{
+ * setup: function( el, options ) {
+ * ...
+ * return [$(el),
+ * $.extend(true,
+ * this.Class.defaults,
+ * options || {} ) ]
+ * }
+ * })
+ * @codeend
+ * Typically, you won't need to make or overwrite setup functions.
+ * Init
+ *
+ * Init functions are called after setup functions.
+ * Typically, they receive the same arguments
+ * as their preceding setup function. The Foo class's init
method
+ * gets called in the following example:
+ *
+ * @codestart
+ * $.Class.Extend("Foo", {
+ * init: function( arg1, arg2, arg3 ) {
+ * this.sum = arg1+arg2+arg3;
+ * }
+ * })
+ * var foo = new Foo(1,2,3);
+ * foo.sum //-> 6
+ * @codeend
+ * Callbacks
+ * Similar to jQuery's proxy method, Class provides a
+ * [jQuery.Class.static.callback callback]
+ * function that returns a callback to a method that will always
+ * have
+ * this
set to the class or instance of the class.
+ *
+ * The following example uses this.callback to make sure
+ * this.name
is available in show
.
+ * @codestart
+ * $.Class.extend("Todo",{
+ * init: function( name ) { this.name = name }
+ * get: function() {
+ * $.get("/stuff",this.callback('show'))
+ * },
+ * show: function( txt ) {
+ * alert(this.name+txt)
+ * }
+ * })
+ * new Todo("Trash").get()
+ * @codeend
+ * Callback is available as a static and prototype method.
+ * Demo
+ * @demo jquery/class/class.html
+ *
+ * @constructor Creating a new instance of an object that has extended jQuery.Class
+ * calls the init prototype function and returns a new instance of the class.
+ *
+ */
+
+ clss = $.Class = function() {
+ if (arguments.length) {
+ clss.extend.apply(clss, arguments);
+ }
+ };
+
+ /* @Static*/
+ extend(clss, {
+ /**
+ * @function callback
+ * Returns a callback function for a function on this Class.
+ * The callback function ensures that 'this' is set appropriately.
+ * @codestart
+ * $.Class.extend("MyClass",{
+ * getData: function() {
+ * this.showing = null;
+ * $.get("data.json",this.callback('gotData'),'json')
+ * },
+ * gotData: function( data ) {
+ * this.showing = data;
+ * }
+ * },{});
+ * MyClass.showData();
+ * @codeend
+ * Currying Arguments
+ * Additional arguments to callback will fill in arguments on the returning function.
+ * @codestart
+ * $.Class.extend("MyClass",{
+ * getData: function( callback ) {
+ * $.get("data.json",this.callback('process',callback),'json');
+ * },
+ * process: function( callback, jsonData ) { //callback is added as first argument
+ * jsonData.processed = true;
+ * callback(jsonData);
+ * }
+ * },{});
+ * MyClass.getData(showDataFunc)
+ * @codeend
+ * Nesting Functions
+ * Callback can take an array of functions to call as the first argument. When the returned callback function
+ * is called each function in the array is passed the return value of the prior function. This is often used
+ * to eliminate currying initial arguments.
+ * @codestart
+ * $.Class.extend("MyClass",{
+ * getData: function( callback ) {
+ * //calls process, then callback with value from process
+ * $.get("data.json",this.callback(['process2',callback]),'json')
+ * },
+ * process2: function( type,jsonData ) {
+ * jsonData.processed = true;
+ * return [jsonData];
+ * }
+ * },{});
+ * MyClass.getData(showDataFunc);
+ * @codeend
+ * @param {String|Array} fname If a string, it represents the function to be called.
+ * If it is an array, it will call each function in order and pass the return value of the prior function to the
+ * next function.
+ * @return {Function} the callback function.
+ */
+ callback: function( funcs ) {
+
+ //args that should be curried
+ var args = makeArray(arguments),
+ self;
+
+ funcs = args.shift();
+
+ if (!isArray(funcs) ) {
+ funcs = [funcs];
+ }
+
+ self = this;
+
+ return function class_cb() {
+ var cur = concatArgs(args, arguments),
+ isString,
+ length = funcs.length,
+ f = 0,
+ func;
+
+ for (; f < length; f++ ) {
+ func = funcs[f];
+ if (!func ) {
+ continue;
+ }
+
+ isString = typeof func == "string";
+ if ( isString && self._set_called ) {
+ self.called = func;
+ }
+ cur = (isString ? self[func] : func).apply(self, cur || []);
+ if ( f < length - 1 ) {
+ cur = !isArray(cur) || cur._use_call ? [cur] : cur
+ }
+ }
+ return cur;
+ }
+ },
+ /**
+ * @function getObject
+ * Gets an object from a String.
+ * If the object or namespaces the string represent do not
+ * exist it will create them.
+ * @codestart
+ * Foo = {Bar: {Zar: {"Ted"}}}
+ * $.Class.getobject("Foo.Bar.Zar") //-> "Ted"
+ * @codeend
+ * @param {String} objectName the object you want to get
+ * @param {Object} [current=window] the object you want to look in.
+ * @return {Object} the object you are looking for.
+ */
+ getObject: $.String.getObject,
+ /**
+ * @function newInstance
+ * Creates a new instance of the class. This method is useful for creating new instances
+ * with arbitrary parameters.
+ * Example
+ * @codestart
+ * $.Class.extend("MyClass",{},{})
+ * var mc = MyClass.newInstance.apply(null, new Array(parseInt(Math.random()*10,10))
+ * @codeend
+ * @return {class} instance of the class
+ */
+ newInstance: function() {
+ var inst = this.rawInstance(),
+ args;
+ if ( inst.setup ) {
+ args = inst.setup.apply(inst, arguments);
+ }
+ if ( inst.init ) {
+ inst.init.apply(inst, isArray(args) ? args : arguments);
+ }
+ return inst;
+ },
+ /**
+ * Setup gets called on the inherting class with the base class followed by the
+ * inheriting class's raw properties.
+ *
+ * Setup will deeply extend a static defaults property on the base class with
+ * properties on the base class. For example:
+ *
+ * $.Class("MyBase",{
+ * defaults : {
+ * foo: 'bar'
+ * }
+ * },{})
+ *
+ * MyBase("Inheriting",{
+ * defaults : {
+ * newProp : 'newVal'
+ * }
+ * },{}
+ *
+ * Inheriting.defaults -> {foo: 'bar', 'newProp': 'newVal'}
+ *
+ * @param {Object} baseClass the base class that is being inherited from
+ * @param {String} fullName the name of the new class
+ * @param {Object} staticProps the static properties of the new class
+ * @param {Object} protoProps the prototype properties of the new class
+ */
+ setup: function( baseClass, fullName ) {
+ this.defaults = extend(true, {}, baseClass.defaults, this.defaults);
+ return arguments;
+ },
+ rawInstance: function() {
+ initializing = true;
+ var inst = new this();
+ initializing = false;
+ return inst;
+ },
+ /**
+ * Extends a class with new static and prototype functions. There are a variety of ways
+ * to use extend:
+ * @codestart
+ * //with className, static and prototype functions
+ * $.Class.extend('Task',{ STATIC },{ PROTOTYPE })
+ * //with just classname and prototype functions
+ * $.Class.extend('Task',{ PROTOTYPE })
+ * //With just a className
+ * $.Class.extend('Task')
+ * @codeend
+ * @param {String} [fullName] the classes name (used for classes w/ introspection)
+ * @param {Object} [klass] the new classes static/class functions
+ * @param {Object} [proto] the new classes prototype functions
+ * @return {jQuery.Class} returns the new class
+ */
+ extend: function( fullName, klass, proto ) {
+ // figure out what was passed
+ if ( typeof fullName != 'string' ) {
+ proto = klass;
+ klass = fullName;
+ fullName = null;
+ }
+ if (!proto ) {
+ proto = klass;
+ klass = null;
+ }
+
+ proto = proto || {};
+ var _super_class = this,
+ _super = this.prototype,
+ name, shortName, namespace, prototype;
+
+ // Instantiate a base class (but only create the instance,
+ // don't run the init constructor)
+ initializing = true;
+ prototype = new this();
+ initializing = false;
+ // Copy the properties over onto the new prototype
+ inheritProps(proto, _super, prototype);
+
+ // The dummy class constructor
+
+ function Class() {
+ // All construction is actually done in the init method
+ if ( initializing ) return;
+
+ if ( this.constructor !== Class && arguments.length ) { //we are being called w/o new
+ return arguments.callee.extend.apply(arguments.callee, arguments)
+ } else { //we are being called w/ new
+ return this.Class.newInstance.apply(this.Class, arguments)
+ }
+ }
+ // Copy old stuff onto class
+ for ( name in this ) {
+ if ( this.hasOwnProperty(name) ) {
+ Class[name] = this[name];
+ }
+ }
+
+ // copy new props on class
+ inheritProps(klass, this, Class);
+
+ // do namespace stuff
+ if ( fullName ) {
+
+ var parts = fullName.split(/\./),
+ shortName = parts.pop(),
+ current = clss.getObject(parts.join('.'), window, true),
+ namespace = current;
+
+
+ current[shortName] = Class;
+ }
+
+ // set things that can't be overwritten
+ extend(Class, {
+ prototype: prototype,
+ namespace: namespace,
+ shortName: shortName,
+ constructor: Class,
+ fullName: fullName
+ });
+
+ //make sure our prototype looks nice
+ Class.prototype.Class = Class.prototype.constructor = Class;
+
+
+ /**
+ * @attribute fullName
+ * The full name of the class, including namespace, provided for introspection purposes.
+ * @codestart
+ * $.Class.extend("MyOrg.MyClass",{},{})
+ * MyOrg.MyClass.shortName //-> 'MyClass'
+ * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass'
+ * @codeend
+ */
+
+ var args = Class.setup.apply(Class, concatArgs([_super_class],arguments));
+
+ if ( Class.init ) {
+ Class.init.apply(Class, args || []);
+ }
+
+ /* @Prototype*/
+ return Class;
+ /**
+ * @function setup
+ * If a setup method is provided, it is called when a new
+ * instances is created. It gets passed the same arguments that
+ * were given to the Class constructor function ( new Class( arguments ... )
).
+ *
+ * $.Class("MyClass",
+ * {
+ * setup: function( val ) {
+ * this.val = val;
+ * }
+ * })
+ * var mc = new MyClass("Check Check")
+ * mc.val //-> 'Check Check'
+ *
+ * Setup is called before [jQuery.Class.prototype.init init]. If setup
+ * return an array, those arguments will be used for init.
+ *
+ * $.Class("jQuery.Controller",{
+ * setup : function(htmlElement, rawOptions){
+ * return [$(htmlElement),
+ * $.extend({}, this.Class.defaults, rawOptions )]
+ * }
+ * })
+ *
+ * PRO TIP:
+ * Setup functions are used to normalize constructor arguments and provide a place for
+ * setup code that extending classes don't have to remember to call _super to
+ * run.
+ *
+ *
+ * Setup is not defined on $.Class itself, so calling super in inherting classes
+ * will break. Don't do the following:
+ *
+ * $.Class("Thing",{
+ * setup : function(){
+ * this._super(); // breaks!
+ * }
+ * })
+ *
+ * @return {Array|undefined} If an array is return, [jQuery.Class.prototype.init] is
+ * called with those arguments; otherwise, the original arguments are used.
+ */
+ //break up
+ /**
+ * @function init
+ * If an init
method is provided, it gets called when a new instance
+ * is created. Init gets called after [jQuery.Class.prototype.setup setup], typically with the
+ * same arguments passed to the Class
+ * constructor: ( new Class( arguments ... )
).
+ *
+ * $.Class("MyClass",
+ * {
+ * init: function( val ) {
+ * this.val = val;
+ * }
+ * })
+ * var mc = new MyClass(1)
+ * mc.val //-> 1
+ *
+ * [jQuery.Class.prototype.setup Setup] is able to modify the arguments passed to init. Read
+ * about it there.
+ *
+ */
+ //Breaks up code
+ /**
+ * @attribute Class
+ * References the static properties of the instance's class.
+ * Quick Example
+ * @codestart
+ * // a class with a static classProperty property
+ * $.Class.extend("MyClass", {classProperty : true}, {});
+ *
+ * // a new instance of myClass
+ * var mc1 = new MyClass();
+ *
+ * //
+ * mc1.Class.classProperty = false;
+ *
+ * // creates a new MyClass
+ * var mc2 = new mc.Class();
+ * @codeend
+ * Getting static properties via the Class property, such as it's
+ * [jQuery.Class.static.fullName fullName] is very common.
+ */
+ }
+
+ })
+
+
+
+
+
+ clss.prototype.
+ /**
+ * @function callback
+ * Returns a callback function. This does the same thing as and is described better in [jQuery.Class.static.callback].
+ * The only difference is this callback works
+ * on a instance instead of a class.
+ * @param {String|Array} fname If a string, it represents the function to be called.
+ * If it is an array, it will call each function in order and pass the return value of the prior function to the
+ * next function.
+ * @return {Function} the callback function
+ */
+ callback = clss.callback;
+
+
+})(jQuery)
\ No newline at end of file
diff --git a/src/static/js/revisioncache.js b/src/static/js/revisioncache.js
new file mode 100644
index 000000000..bd77bb5a0
--- /dev/null
+++ b/src/static/js/revisioncache.js
@@ -0,0 +1,867 @@
+/**
+ * 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.
+ */
+require('./jquery.class');
+var libchangeset = require("./Changeset");
+
+function log () {
+ console.log.apply(console, arguments);
+};
+
+$.Class("Changeset",
+ {//statics
+ },
+ {//instance
+ init: function (from_revision, to_revision, deltatime, value) {
+ this.from_revision = from_revision;
+ this.to_revision = to_revision;
+ this.deltatime = deltatime;
+ this.value = value;
+ },
+ getValue: function () {
+ return this.value;
+ },
+ compose: function (other, pad) {
+ var newvalue = libchangeset.compose(this.value, other.value, pad.apool);
+ var newchangeset = new Changeset(this.from_revision, other.to_revision,
+ this.deltatime + other.deltatime, newvalue);
+ //TODO: insert new changeset into the graph somehow.
+ return newchangeset;
+ },
+ /**
+ * Apply this changeset to the passed pad.
+ * @param {PadClient} pad - The pad to apply the changeset to.
+ */
+ apply: function (pad) {
+ // must mutate attribution lines before text lines
+ libchangeset.mutateAttributionLines(this.value, pad.alines, pad.apool);
+
+ // Looks like this function can take a regular array of strings
+ libchangeset.mutateTextLines(this.value, /* padcontents */ /*this.lines */ pad.divs);
+ },
+ /**
+ * 'Follow' the Changeset in a given direction, returning the revision at
+ * the specified end of the edge.
+ * @param {bool} direction - If true, go to the 'from' revision, otherwise
+ * go to the 'to' revision.
+ * @returns {Revision}
+ */
+ follow: function () {
+ return this.to_revision;
+ }
+ }
+);
+
+/**
+ * Revision class. Represents a specific revision. Each instance has three
+ * possible edges in each direction. Each edge is essentially a Changeset.
+ * We store three edges at different granularities, to make skipping fast.
+ * e.g. to go from r1 to r251, you start at r1, use the big edge to go to
+ * r100, use the big edge again to go to r200, use the next 5 medium edges to
+ * go from r200 to r210, etc. until reaching r250, follow the next small edge
+ * to get to 251. A total of 8 edges are traversed (a.k.a. applied),
+ * making this significantly cheaper than applying all 250 changesets from r1
+ * to r251.
+ */
+$.Class("Revision",
+ {//statics
+ // we rely on the fact that granularities are always traversed biggest to
+ // smallest. Changing this will break lots of stuff.
+ granularities: {huge: 1000, big: 100, medium: 10, small: 1}
+ },
+ {//instance
+ /**
+ * Create a new revision for the specified revision number.
+ * @constructor
+ * @param {number} revnum - The revision number this object represents.
+ */
+ init: function (revnum) {
+ this.revnum = revnum;
+ // next/previous edges, granularityed as big, medium and small
+ this.next = {};
+ this.previous = {};
+ for (var granularity in this.granularties) {
+ this.next[granularity] = null;
+ this.previous[granularity] = null;
+ }
+ },
+ /**
+ * Add a changeset from this revision to the target.
+ * @param {Revision} target - The target revision.
+ * @param {object} changeset - The raw changeset data.
+ * @param {time} timedelta - The difference in time between this revision
+ * and the target.
+ * @returns {Changeset} - The new changeset object.
+ */
+ addChangeset: function (target, changeset, timedelta) {
+ if (this.revnum == target.revnum)
+ // This should really never happen, but if it does, let's short-circuit.
+ return;
+
+ var delta_revnum = target.revnum - this.revnum;
+ // select the right edge set:
+ var direction_edges = delta_revnum < 0 ? this.previous : this.next;
+
+ // find the correct granularity and add an edge (changeset) for that granularity
+ for (var granularity in Revision.granularities) {
+ if (Math.abs(delta_revnum) == Revision.granularities[granularity]) {
+ //TODO: should we check whether the edge exists?
+ direction_edges[granularity] = new Changeset(this, target, timedelta, changeset);
+ return direction_edges[granularity];
+ }
+ }
+ // our delta_revnum isn't one of the granularities. Something is wrong
+ //TODO: handle this case?
+ return null;
+ },
+ lt: function (other, is_reverse) {
+ if (is_reverse)
+ return this.gt(other);
+ return this.revnum < other.revnum;
+ },
+ gt: function (other, is_reverse) {
+ if (is_reverse)
+ return this.lt(other);
+ return this.revnum > other.revnum;
+ }
+ }
+);
+
+$.Class("RevisionCache",
+ {
+ VERBOSE: true,
+ },
+ {//instance
+ /**
+ * Create a new RevisionCache.
+ * @constructor
+ * @param {TimesliderClient} connection - The connection to be used for loading changesets.
+ * @param {number} head_revnum - The current head revision number. TODO: we can probably do away with this now.
+ */
+ init: function (connection, head_revnum) {
+ this.log = RevisionCache.VERBOSE ? log : function () {};
+ this.connection = connection;
+ this.loader = new ChangesetLoader(connection);
+ this.revisions = {};
+ this.head_revision = this.getRevision(head_revnum || 0);
+ this.loader.start();
+ },
+ /**
+ * Get the head revision.
+ * @returns {Revision} - the head revision.
+ */
+ getHeadRevision: function () {
+ return this.head_revision;
+ },
+ /**
+ * Get a Revision instance for the specified revision number.
+ * If we don't yet have a Revision instance for this revision, create a
+ * new one. Also make sure that the head_revision attribute always refers
+ * to the instance for the pad's head revision.
+ * @param {number} revnum - The revision number for which we want a
+ * Revision object.
+ * @returns {Revision}
+ */
+ getRevision: function (revnum) {
+ if (revnum in this.revisions)
+ return this.revisions[revnum];
+ var revision = new Revision(revnum);
+ this.revisions[revnum] = revision;
+ if (this.head_revision && revnum > this.head_revision.revnum) {
+ this.head_revision = revision;
+ }
+ return revision;
+ },
+ /**
+ * Add a new revision at the head.
+ * @param {number} revnum - the revision number of the new revision.
+ * @param {string} forward - the forward changeset to get here from previous head.
+ * @param {string} reverse - the reverse changeset.
+ * @param {string} timedelta - the time difference.
+ */
+ appendHeadRevision: function (revnum, forward, reverse, timedelta) {
+ this.addChangesetPair(this.head_revision.revnum, revnum, forward, reverse, timedelta);
+ this.head_revision = this.getRevision(revnum);
+ //TODO: render it if we are currently at the head_revision?
+ },
+ /**
+ * Links two revisions, specified by from and to with the changeset data
+ * in value and reverseValue respectively.
+ * @param {number} from - The revision number from which the forward
+ * changeset originates.
+ * @param {number} to - The revision number to which the forward changeset
+ * bring us.
+ * @param {changeset} forwardValue - The forward changeset data.
+ * @param {changeset} reverseValue - The reverse changeset data.
+ * @param {time} timedelta - The difference in time between the from and
+ * to revisions.
+ */
+ addChangesetPair: function (from, to, value, reverseValue, timedelta) {
+ var from_rev = this.getRevision(from);
+ var to_rev = this.getRevision(to);
+ from_rev.addChangeset(to_rev, value, timedelta);
+ to_rev.addChangeset(from_rev, reverseValue, -timedelta);
+ },
+ /**
+ * Find a (minimal) path from a given revision to another revision. If a
+ * complete path cannot be found, return a path which comes as close as
+ * possible to the to revision.
+ * @param {Revision} from - The revision from which to start.
+ * @param {Revision} to - The revision which the path should try to reach.
+ * @returns {object} - A list of Changesets which describe a (partial) path
+ * from 'from' to 'to', and the last revision reached.
+ */
+ findPath: function (from, to) {
+ /*
+ *TODO: currently we only ever move in the direction of sign(to-from).
+ *It might be worth implementing 'jitter' movements, so that if you,
+ *for example, you are trying to go from 0 to 99, and you have the
+ *following edges:
+ * 0 -> 100
+ * 100 -> 99
+ *The algorithm would be smart enough to provide you with that as a path
+ */
+ var path = [];
+ var found_discontinuity = false;
+ var current = from;
+ var direction = (to.revnum - from.revnum) < 0;
+ var granularity = 0;
+
+ //log("[findpath] from: %d, to: %d", from.revnum, to.revnum);
+ while (current.lt(to, direction) && !found_discontinuity) {
+ //log("\t[findPath] while current: ", current.revnum);
+ var delta_revnum = to.revnum - current.revnum;
+ var direction_edges = direction ? current.previous : current.next;
+ for (granularity in Revision.granularities) {
+ if (Math.abs(delta_revnum) >= Revision.granularities[granularity]) {
+ //log("\t\t[findPath] for delta: %d, granularity: %d", delta_revnum, Revision.granularities[granularity]);
+ /*
+ * the delta is larger than the granularity, let's use the granularity
+ *TODO: what happens if we DON'T have the edge?
+ * in theory we need to fetch it (and this is certainly the case for playback
+ * at granularity = 1). However, when skipping, we might try to find the NEXT
+ * Revision (which is not linked by the graph to current) and request revisions
+ * from current to that Revision (at the largest possible granularity)
+ */
+ var edge = direction_edges[granularity];
+ //log("\t\t[findpath] edge:", edge);
+ if (edge) {
+ // add this edge to our path
+ path.push(edge);
+ // follow the edge to the next Revision node
+ current = edge.follow();
+ // no need to look for smaller granularities
+ break;
+ } else {
+ // we don't have an edge. Normally we can just continue to the
+ // next granularity level. BUT, if we are at the lowest
+ // granularity and don't have an edge, we've reached a DISCONTINUITY
+ // and can no longer continue.
+ if (Revision.granularities[granularity] == 1)
+ found_discontinuity = true;
+ }
+ }
+ }
+ }
+
+ //log("[findpath] ------------------");
+ // return either a full path, or a path ending as close as we can get to
+ // the target revision.
+ return {path: path, end_revision: current, granularity: granularity};
+ },
+ //TODO: horrible name!
+ transition: function (from_revnum, to_revnum, applyChangeset_callback) {
+ var path = [];
+ var current_revision = this.getRevision(from_revnum);
+ var target_revision = this.getRevision(to_revnum);
+ this.log("[revisioncache > transition] from %d -> %d", from_revnum, to_revnum);
+ // For debugging:
+ function print_path(path) {
+ var res = "[";
+ for (var p in path) {
+ res += path[p].from_revision.revnum + "->" + path[p].to_revision.revnum + ", ";
+ }
+ res += "]";
+ return res;
+ }
+
+ // lets just keep a 'final' path, which is a list of changesets.
+ // The transition should complete when the first element's from revnum is
+ // from_revnum, and the last element's to revnum is to_revnum.
+ // (Assuming that there are no discontinuities.
+ var thePath = [];
+
+ function is_complete() {
+ console.log(thePath);
+ if (thePath.length && thePath[0].from_revision.revnum == from_revnum
+ && thePath.slice(-1)[0].to_revision.revnum == to_revnum) {
+ return true;
+ }
+ return false;
+ }
+
+ var thePath = [];
+
+ var _this = this;
+ function partialTransition (cur_start, cur_end) {
+ _this.log("[partialTransition] from: %d, to: %d, cur_start: %d, cur_end: %d", from_revnum, to_revnum, cur_start, cur_end);
+ var cur_start_rev = _this.getRevision(cur_start);
+ var res = _this.findPath(cur_start_rev, _this.getRevision(cur_end));
+ log("find: ", print_path(res.path));
+
+ if (!res.path.length) {
+ // we got nutting, request changesets for the full path.
+ _this.requestChangesets(cur_start_rev, target_revision, partialTransition);
+ return;
+ }
+
+ //TODO: we should probably check for discontinuities which indicate a real WTF condition.
+
+ // just prepend the found path to thePath; we assume that we get further from
+ // the original target as we go. this is because we set the new target to be
+ // the head of our
+ thePath = res.path.concat(thePath);
+
+ log("THE PATH: ", print_path(thePath));
+
+ //FIXME: this should test for 'completeness'!
+ if (is_complete()) {
+ //log("found: ", print_path(res.path));
+ if(applyChangeset_callback) {
+ applyChangeset_callback(thePath);
+ }
+ return;
+ }
+ // next iteration, we want to find a path that reaches the beginning of our
+ // current path, as we assume that the 'tail' of the path is always correct.
+ target_revision = thePath[0].from_revision;
+ }
+
+ partialTransition(from_revnum, to_revnum);
+
+ },
+ /**
+ * Request changesets which will allow transitioning from 'from' to 'to'
+ * from the server.
+ * @param {Revision} from - The start revision.
+ * @param {Revision} to - The end revision.
+ * @param {function} changesetsProcessed_callback - A callback triggered
+ * when the requested changesets have been
+ * received and processed (added to the graph)
+ */
+ requestChangesets: function (from, to, changesetsProcessed_callback) {
+ this.log("[revisioncache] requestChangesets: %d -> %d", from.revnum, to.revnum);
+ var delta = to.revnum - from.revnum;
+ var sign = delta > 0 ? 1 : -1;
+ var start = delta > 0 ? from.revnum : to.revnum;
+ var end = delta > 0 ? to.revnum : from.revnum;
+ var adelta = Math.abs(delta);
+
+ var _this = this;
+ function process_received_changesets (data) {
+ _this.log("[revisioncache] received changesets {from: %d, to: %d} @ granularity: %d", data.start, data.actualEndNum, data.granularity);
+ var start = data.start;
+ for (var i = 0; i < data.timeDeltas.length; i++, start += data.granularity) {
+ _this.addChangesetPair(start, start + data.granularity, data.forwardsChangesets[i], data.backwardsChangesets[i], data.timeDeltas[i]);
+ }
+ if (changesetsProcessed_callback) {
+ if (sign == 1)
+ changesetsProcessed_callback(data.start, data.start + (data.granularity*data.timeDeltas.length));
+ else
+ changesetsProcessed_callback(data.start + (data.granularity*data.timeDeltas.length), data.start);
+ }
+ }
+
+ var rounddown = function (a, b) {
+ return Math.floor(a / b) * b;
+ };
+ var roundup = function (a, b) {
+ return (Math.floor(a / b)+1) * b;
+ };
+
+ this.log("[requestChangesets] start: %d, end: %d, delta: %d, adelta: %d", start, end, delta, adelta);
+ for (var g in Revision.granularities) {
+ var granularity = Revision.granularities[g];
+ var remainder = Math.floor(adelta / granularity);
+ this.log("\t[requestChangesets] start: %d, granularity: %d, adelta: %d, //: %d", start, granularity, adelta, remainder);
+ this.log("\ttest: start: %d, end: %d", rounddown(start,granularity), roundup(end,granularity));
+ this.log("\trounddown delta: %d, start: %d", rounddown(adelta, granularity), rounddown(start, granularity));
+ if (remainder) {
+ //this.loader.enqueue(start, granularity, process_received_changesets);
+ this.log("\t[requestChangesets] REQUEST start: %d, end: %d, granularity: %d", rounddown(start, granularity), roundup(adelta, granularity), granularity);
+ this.loader.enqueue(rounddown(start, granularity), granularity, process_received_changesets);
+ // for the next granularity, we assume that we have now successfully navigated
+ // as far as required for this granularity. We should also make sure that only
+ // the significant part of the adelta is used in the next granularity.
+ start = rounddown(start, granularity) + rounddown(adelta, granularity);
+ adelta = adelta - rounddown(adelta, granularity);
+ this.log("\t\tnew start: %d, delta: %d", start, adelta);
+ }
+ }
+ },
+ }
+);
+
+$.Class("Thread",
+ {//statics
+ VERBOSE: true
+ },
+ {//instance
+ init: function (interval) {
+ this._is_running = false;
+ this._is_stopping = false;
+ this._interval_id = null;
+ this._interval = interval ? interval : 1000;
+ this.log = Thread.VERBOSE ? log : function () {};
+ },
+ _run: function () {
+ this.log("[thread] tick");
+ },
+ // start the run loop
+ start: function () {
+ var _this = this;
+ this.log("[thread] starting");
+ var wrapper = function () {
+ if (_this._is_running && _this._is_stopping) {
+ this.log("[thread] shutting down");
+ clearInterval(_this._interval_id);
+ _this._is_running = false;
+ return;
+ }
+ _this._run.apply(_this);
+ };
+ this._is_running = true;
+ this._is_stopping = false;
+ this._interval_id = setInterval(wrapper, this._interval);
+ },
+ // stop the run loop
+ stop: function () {
+ this._is_stopping = true;
+ this.log("[thread] request stop");
+ // TODO: consider finding a way to make this block
+ // or alternatively, having a callback which is called
+ // when the thread stops
+ }
+ }
+);
+
+$.Class("ChangesetRequest",
+ {//statics
+ },
+ {//instance
+ init: function (start, granularity, callback) {
+ this.log = ChangesetLoader.VERBOSE ? log : function () {};
+ this.start = start;
+ this.granularity = granularity;
+ this.request_id = (this.granularity << 16) + this.start;
+ this.fulfill_callback = callback;
+ },
+ getRequestID: function () {
+ return this.request_id;
+ },
+ fulfill: function (data) {
+ var id = this.getRequestID();
+ this.log("[changesetrequest] Fulfilling request: %d, start: %d, granularity: %d", id, id & 0xffff, id >> 16);
+ if (this.fulfill_callback)
+ this.fulfill_callback(data);
+ }
+
+ }
+);
+
+Thread("ChangesetLoader",
+ {//statics
+ VERBOSE: false,
+ },
+ {//instance
+ /**
+ * Create a new ChangesetLoader.
+ * @constructor
+ * @param {TimesliderClient} connection - a TimesliderClient object to be used
+ * for communication with the server.
+ */
+ init: function (connection) {
+ this._super(200);
+ this.connection = connection;
+ this.queues = {};
+ for (var granularity in Revision.granularities) {
+ this.queues[granularity] = [];
+ }
+ this.pending = {};
+ var _this = this;
+ this.connection.on("CHANGESET_REQ", function () {
+ _this.on_response.apply(_this, arguments);
+ });
+ this.log = ChangesetLoader.VERBOSE ? log : function () {};
+ },
+ /**
+ * Enqueue a request for changesets. The changesets will be retrieved
+ * asynchronously.
+ * @param {number} start - The revision from which to start.
+ *
+ * @param {number} granularity - The granularity of the changesets. If this
+ * is 1, the response will include changesets which
+ * can be applied to go from revision r to r+1.
+ * If 10 is specified, the resulting changesets will
+ * be 'condensed', so that each changeset will go from
+ * r to r+10.
+ * If any other number is specified, that granularity will
+ * apply.
+ *
+ * TODO: there is currently no 'END' revision implemented
+ * in the server. The 'END' calculated at the server is:
+ * start + (100 * granularity)
+ * We should probably fix this so that you can specify
+ * exact ranges. Right now, the minimum number of
+ * changesets/revisions you can retrieve is 100, which
+ * feels broken.
+ * @param {function} callback - A callback which will be triggered when the request has
+ * been fulfilled. The context of the callback will be the
+ * ChangesetRequest object, so you can check what you actually
+ * asked for.
+ */
+ enqueue: function (start, granularity, callback) {
+ this.log("[changeset_loader] enqueue: %d, %d", start, granularity);
+ //TODO: check cache to see if we really need to fetch this
+ // maybe even do splices if we just need a smaller range
+ // in the middle
+ var queue = null;
+ for (var g in Revision.granularities) {
+ if (granularity == Revision.granularities[g]) {
+ queue = this.queues[g];
+ break;
+ }
+ }
+
+ var request = new ChangesetRequest(start, granularity, callback);
+ if (! (request.getRequestID() in this.pending)) {
+ queue.push(request);
+ this.log("[changesetloader] enqueued request:", request.getRequestID())
+ }
+
+ },
+ _run: function () {
+ var _this = this;
+ function addToPending () {
+ _this.pending[request.getRequestID()] = request;
+ }
+ //TODO: pop an item from the queue and perform a request.
+ for (var q in this.queues) {
+ var queue = this.queues[q];
+ if (queue.length > 0) {
+ // TODO: pop and handle
+ var request = queue.pop();
+ if (request.getRequestID() in this.pending) {
+ //this request is already pending!
+ var id = request.getRequestID();
+ this.log("ALREADY PENDING REQUEST: %d, start: %d, granularity: %d", id, id & 0xffff, id >> 16);
+ continue;
+ }
+ //TODO: test AGAIN to make sure that it hasn't been retrieved and cached by
+ //a previous request. This should handle the case when two requests for the
+ //same changesets are enqueued (which would be fine, as at enqueue time, we
+ //only check the cache of AVAILABLE changesets, not the pending requests),
+ //the first one is fulfilled, and then we pop the second one, and don't
+ //need to perform a server request. Note: it might be worth changing enqueue
+ //to check the pending requests queue to avoid this situation entirely.
+
+ this.connection.sendMessage("CHANGESET_REQ", {
+ start: request.start,
+ granularity: request.granularity,
+ requestID: request.getRequestID(),
+ }, addToPending);
+ }
+ }
+ //TODO: this stop is just for debugging!!!!
+ //FIXME: remove when done testing
+ //this.stop();
+ },
+ on_response: function (data) {
+ this.log("[changesetloader] on_response: ", data);
+ if (!(data.requestID in this.pending)) {
+ this.log("[changesetloader] WTF? changeset not pending: ", data.requestID);
+ return;
+ }
+
+ // pop it from the pending list:
+ var request = this.pending[data.requestID];
+ delete this.pending[data.requestID];
+ this.log("[changesetloader] still pending: ", this.pending);
+ //fulfill the request
+ request.fulfill(data);
+ },
+ }
+);
+
+var libcssmanager = require("./cssmanager");
+var linestylefilter = require("./linestylefilter").linestylefilter;
+var libcolorutils = require('./colorutils').colorutils;
+$.Class("Author",
+ {//static
+ },
+ {//instance
+ init: function (id, data, palette) {
+ this.id = id;
+ this.name = data.name;
+ this.is_anonymous = !this.name;
+ // if the colorId is an integer, it's an index into the color palette,
+ // otherwise we assume it is a valid css color string
+ this.background_color = typeof data.colorId == "number" ? palette[data.colorId] : data.colorId;
+ // foreground color should be black unless the luminosity of the
+ // background color is lower than 0.5. This effectively makes sure
+ // that the text is readable.
+ this.color = (libcolorutils.luminosity(libcolorutils.css2triple(this.background_color)) < 0.5 ? "#ffffff" : "#000000");
+ // generate a css class name for this author.
+ this.cssclass = linestylefilter.getAuthorClassName(this.id);
+ },
+ /**
+ * Create and add a rule to the stylesheet setting the foreground and
+ * background colors for this authors cssclass. This class can then be
+ * applied to any span authored by this author, and the colors will just work.
+ * @param {object} cssmanager - A cssmanager wrapper for the stylesheet to
+ * which the rules should be added.
+ */
+ addStyleRule: function (cssmanager) {
+ // retrieve a style selector for '.' class, which is applied
+ // to blobs which were authored by that .
+ var selector = cssmanager.selectorStyle("." + this.cssclass);
+ // apply the colors
+ selector.backgroundColor = this.background_color;
+ selector.color = this.color;
+ },
+ /**
+ * Retrieve the name of this user.
+ */
+ getName: function () {
+ return this.is_anonymous ? "anonymous" : this.name;
+ },
+ /**
+ * Retrieve the cssclass for this user.
+ */
+ getCSSClass: function () {
+ return this.cssclass;
+ },
+ }
+);
+
+var AttribPool = require("./AttributePool");
+var domline = require("./domline").domline;
+$.Class("PadClient",
+ {//static
+ USE_COMPOSE: false,
+ VERBOSE: true,
+ },
+ {//instance
+ /**
+ * Create a PadClient.
+ * @constructor
+ * @param {RevisionCache} revisionCache - A RevisionCache object to use.
+ * @param {dict} options - All the necessary options. TODO: document this.
+ */
+ init: function (revisionCache, options) {
+ this.revisionCache = revisionCache;
+ this.revision = this.revisionCache.getRevision(options.revnum);
+ this.timestamp = options.timestamp;
+ this.alines = libchangeset.splitAttributionLines(options.atext.attributes, options.atext.text);
+ this.apool = (new AttribPool()).fromJsonable(options.atext.apool);
+ this.lines = libchangeset.splitTextLines(options.atext.text);
+ this.authors = {};
+ this.dynamicCSS = libcssmanager.makeCSSManager('dynamicsyntax');
+ this.palette = options.palette;
+ this.log = PadClient.VERBOSE ? log : function () {};
+
+ this.updateAuthors(options.author_info);
+
+ //TODO: this is a kludge! we should receive the padcontent as an
+ //injected dependency
+ this.divs = [];
+ this.padcontent = $("#padcontent");
+ for (var i in this.lines) {
+ var div = this._getDivForLine(this.lines[i], this.alines[i]);
+ this.divs.push(div);
+ this.padcontent.append(div);
+ }
+
+ //TODO: monkey patch divs.splice to use our custom splice function
+ this.divs.original_splice = this.divs.splice;
+ var _this = this;
+ this.divs.splice = function () {
+ return _this._spliceDivs.apply(_this, arguments);
+ };
+ // we need to provide a get, as we want to give
+ // libchangeset the text of a div, not the div itself
+ this.divs.get = function (index) {
+ return this[index].data('text');
+ };
+ },
+ goToRevision: function (revnum, atRevision_callback) {
+ this.log("[padclient > goToRevision] revnum: %d", revnum);
+ var _this = this;
+ if (this.revision.revnum == revnum) {
+ if (atRevision_callback)
+ atRevision_callback.call(this, this.revision, this.timestamp);
+ return;
+ }
+
+ this.revisionCache.transition(this.revision.revnum, revnum, function (path) {
+ _this.log("[padclient > applyChangeset_callback] path:", path);
+ var time = _this.timestamp;
+ var p, changeset = null; //pre-declare, because they're used in both blocks.
+ if (PadClient.USE_COMPOSE) {
+ var composed = path[0];
+ var _path = path.slice(1);
+ for (p in _path) {
+ changeset = _path[p];
+ composed = composed.compose(changeset, _this);
+ }
+ composed.apply(_this);
+ time += composed.deltatime * 1000;
+ } else { // Don't compose, just apply
+ for (p in path) {
+ changeset = path[p];
+ time += changeset.deltatime * 1000;
+ //try {
+ _this.log("[transition] %d -> %d, changeset: %s", changeset.from_revision.revnum, changeset.to_revision.revnum, changeset.value);
+ changeset.apply(_this);
+ /*} catch (err) {
+ log("Error applying changeset: ");
+ log("\t", changeset.value);
+ log("\t %d -> %d ", changeset.from_revision.revnum, changeset.to_revision.revnum);
+ log(err);
+ log("--------------");
+ }*/
+ }
+ }
+
+ // set revision and timestamp
+ _this.revision = path.slice(-1)[0].to_revision;
+ _this.timestamp = time;
+ // fire the callback
+ if (atRevision_callback) {
+ _this.log("[padclient] about to call atRevision_callback", _this.revision, _this.timestamp);
+ atRevision_callback.call(_this, _this.revision, _this.timestamp);
+ }
+ });
+ },
+ /**
+ * Update the authors of this pad.
+ * @param {object} author_info - The author info object sent by the server
+ */
+ updateAuthors: function (author_info) {
+ var authors = author_info;
+ this.log("[updateAuthors]: ", authors);
+ for (var authorid in authors) {
+ if (authorid in this.authors) {
+ // just dispose of existing ones instead of trying to update existing
+ // objects.
+ delete this.authors[authorid];
+ }
+ var author = new Author(authorid, authors[authorid], this.palette);
+ this.authors[authorid] = author;
+ author.addStyleRule(this.dynamicCSS);
+ }
+ },
+ /**
+ * Merge a foreign (forward) changeset into our data. This involves rebuilding
+ * the forward changeset in our apool and building a reverse changeset.
+ * This is used to move new upstream changesets/revisions into our apool context.
+ * @param {string} changeset - The foreign changeset to merge
+ * @param {object} apool - The apool for that changeset
+ * @returns {object} - A values object with forward and reverse changesets.
+ */
+ mergeForeignChangeset: function (changeset, apool) {
+ var values = {};
+ values.forward = libchangeset.moveOpsToNewPool(
+ changeset,
+ (new AttribPool()).fromJsonable(apool),
+ this.apool
+ );
+ var reverseValue = libchangeset.inverse(
+ changeset,
+ this.divs,
+ this.alines,
+ this.apool
+ );
+ values.reverse = libchangeset.moveOpsToNewPool(
+ reverseValue,
+ (new AttribPool()).fromJsonable(apool),
+ this.apool
+ );
+ return values;
+ },
+ /**
+ * Get a div jquery element for a given attributed text line.
+ * @param {string} text - The text content of the line.
+ * @param {string} atext - The attributes string.
+ * @return {jquery object} - The div element ready for insertion into the DOM.
+ */
+ _getDivForLine: function (text, atext) {
+ //this.log("[_getDivsForLine] %s; %s", text, atext);
+ var dominfo = domline.createDomLine(text != '\n', true);
+
+ // Here begins the magic invocation:
+ linestylefilter.populateDomLine(text, atext, this.apool, dominfo);
+ dominfo.prepareForAdd();
+
+ var div = $("" +
+ dominfo.node.innerHTML + "
");
+ return div;
+ },
+ /**
+ * we need a customized splice function for our divs array, because we
+ * need to be able to:
+ * - remove elements from the DOM when they are spliced out
+ * - create a new div for line elements and add them to the array
+ * instead of the raw line
+ * - add the new divs to the DOM
+ * this function is fully compliant with the Array.prototype.splice
+ * spec, as we're monkey-patching it on to the divs array.
+ * @param {number} index - Index at which to start changing the array.
+ * @param {number} howMany - An integer indicating the number of old array elements to remove.
+ * @param {array} elements - The elements to add to the array. In our case, these are lines.
+ */
+ _spliceDivs: function (index, howMany, elements) {
+ elements = Array.prototype.slice.call(arguments, 2);
+ // remove howMany divs starting from index. We need to remove them from
+ // the DOM.
+ for (var i = index; i < index + howMany && i < this.divs.length; i++)
+ this.divs[i].remove();
+
+ // generate divs for the new elements:
+ var newdivs = [];
+ for (i in elements)
+ newdivs.push(this._getDivForLine(elements[i], this.alines[index + i]));
+
+ // if we are splicing at the beginning of the array, we need to prepend
+ // to the padcontent DOM element
+ if (!this.divs[index - 1])
+ this.padcontent.prepend(newdivs);
+ // otherwise just add the new divs after the index-th div
+ else
+ this.divs[index - 1].after(newdivs);
+ // super primitive scrollIntoView
+ if (newdivs.length) {
+ newdivs[0][0].scrollIntoView(false);
+ }
+
+ // perform the splice on our array itself
+ // TODO: monkey patching divs.splice, so use divs.original_splice or something
+ args = [index, howMany].concat(newdivs);
+ return this.divs.original_splice.apply(this.divs, args);
+ },
+ }
+);
diff --git a/src/static/js/revisionslider.js b/src/static/js/revisionslider.js
new file mode 100644
index 000000000..40ecbfbae
--- /dev/null
+++ b/src/static/js/revisionslider.js
@@ -0,0 +1,259 @@
+var sliderui = require('./sliderui');
+require('./jquery.class');
+
+$.Class("RevisionSlider",
+ {//statics
+ /**
+ * The number of milliseconds to wait between revisions when playing back.
+ */
+ PLAYBACK_DELAY: 400,
+ },
+ {//instance
+ /**
+ * Create a new RevisionSlider, given a connection to the server and a root
+ * element.
+ * @constructor
+ * @param {TimesliderClient} connection - The connection to the server.
+ * @param {jquery object} root_element - The element to build the slider on.
+ */
+ init: function (connection, root_element) {
+ this.connection = connection;
+ this.revision_number = this.connection.getCurrentRevision().revnum;
+ this.timestamp = 0;
+ this.is_playing = false;
+ // if there was a revision specified in the 'location.hash', jump to it.
+ if (window.location.hash.length > 1) {
+ var rev = Number(window.location.hash.substr(1));
+ if(!isNaN(rev))
+ this.revision_number = rev;
+ }
+
+ console.log("New RevisionSlider, current_revision = %d", this.revision_number);
+ // parse the various elements we need:
+ this.elements = {};
+ this.loadElements(root_element);
+ var _this = this;
+ this.slider = new SliderUI(this.elements.slider_bar,
+ options = {
+ value: this.revision_number,
+ max: this.connection.getHeadRevision(),
+ change: function () { _this.onChange.apply(_this, arguments); },
+ slide: function () { _this.onSlide.apply(_this, arguments); },
+ });
+ this.loadSavedRevisionHandles();
+ this.slider.render();
+
+ this._mouseInit();
+
+ this.goToRevision(this.revision_number);
+ },
+ onChange: function (value) {
+ console.log("in change handler:", value);
+ if (!this.is_playing)
+ this.goToRevision(value);
+ },
+ onSlide: function (value) {
+ console.log("in slide handler:", value);
+ if (!this.is_playing)
+ this.goToRevision(value);
+ },
+ /**
+ * Populate the elements dictionary with the various elements we might want
+ * to use.
+ * @param {jquery object} root_element - The root element of this slider.
+ */
+ loadElements: function (root_element) {
+ this.elements.root = root_element;
+ this.elements.slider_bar = root_element.find("#ui-slider-bar");
+ this.elements.slider = root_element.find("#timeslider-slider");
+ this.elements.button_left = root_element.find("#leftstep");
+ this.elements.button_right = root_element.find("#rightstep");
+ this.elements.button_play = root_element.find("#playpause_button");
+ this.elements.timestamp = root_element.find("#timer");
+ this.elements.revision_label = root_element.find("#revision_label");
+ this.elements.revision_date = root_element.find("#revision_date");
+ this.elements.authors = root_element.find("#authorsList");
+ },
+ /**
+ * Create 'star' handles on the slider for each saved revision.
+ */
+ loadSavedRevisionHandles: function () {
+ for (var r in this.connection.savedRevisions) {
+ var rev = this.connection.savedRevisions[r];
+ this.slider.createHandle(rev.revNum, "star");
+ }
+ },
+ /**
+ * Toggle (and execute) the playback mode.
+ */
+ playpause: function () {
+ if (this.is_playing) {
+ this.is_playing = false;
+ return;
+ }
+
+ var revnum = this.revision_number;
+ if (revnum == this.connection.getHeadRevision())
+ revnum = 0;
+
+ var _this = this;
+ var keepPlaying = function (current_revnum) {
+ if (current_revnum == _this.connection.getHeadRevision())
+ _this.is_playing = false;
+ if (!_this.is_playing) {
+ _this.render();
+ return;
+ }
+ setTimeout(function () {
+ _this.goToRevision(current_revnum + 1, keepPlaying);
+ }, RevisionSlider.PLAYBACK_DELAY);
+ };
+
+ this.is_playing = true;
+ this.goToRevision(revnum, keepPlaying);
+ },
+ /**
+ * Update the UI elements to the current revision
+ */
+ render: function () {
+ this.elements.revision_label.html(html10n.get("timeslider.version", { "version": this.revision_number }));
+ this.slider.setMax(this.connection.getHeadRevision());
+ this.slider.setValue(this.revision_number);
+ window.location.hash = "#" + this.revision_number;
+ this.setTimestamp(this.timestamp);
+ if (this.is_playing)
+ this.elements.button_play.find("div").addClass("pause");
+ else
+ this.elements.button_play.find("div").removeClass("pause");
+ if (this.revision_number == this.connection.getHeadRevision())
+ this.elements.button_right.addClass("disabled");
+ else
+ this.elements.button_right.removeClass("disabled");
+ if (this.revision_number === 0)
+ this.elements.button_left.addClass("disabled");
+ else
+ this.elements.button_left.removeClass("disabled");
+
+ this.renderAuthors();
+ },
+
+
+ /**
+ * Render the authors line.
+ */
+ renderAuthors: function () {
+ //TODO: consider alphabetizing the authors?
+ var authors = this.connection.getAuthors();
+ this.elements.authors.empty();
+ if ($.isEmptyObject(authors)) {
+ this.elements.authors.append("No authors");
+ return;
+ }
+ for (var authorid in authors) {
+ var author = authors[authorid];
+ var span = $("")
+ .text(author.getName())
+ .addClass('author')
+ .addClass(author.getCSSClass());
+ this.elements.authors.append(span);
+ }
+ },
+ /**
+ * Go to a specific revision number. This will perform the actual
+ * transition to the revision and set the UI elements as required
+ * once the transition is done. The callback can be used to perform
+ * actions after the transition is complete and the UI has been
+ * updated.
+ * @param {number} revnum - The revision to transition to.
+ * @param {callback} atRevision_callback - The callback.
+ */
+ goToRevision: function (revnum, atRevision_callback) {
+ if (revnum > this.connection.getHeadRevision())
+ revnum = this.connection.latest_revision;
+ if (revnum < 0)
+ revnum = 0;
+
+ var _this = this;
+ this.connection.goToRevision(revnum, function (revision, timestamp) {
+ console.log("[revisionslider > goToRevision > callback]", revision, timestamp);
+ //update UI elements:
+ _this.revision_number = revision.revnum;
+ _this.timestamp = timestamp;
+ _this.render.call(_this);
+ //TODO: set the enabled/disabled for button-left and button-right
+ if (atRevision_callback) {
+ atRevision_callback(revnum);
+ }
+ });
+ },
+ /**
+ * Set the timestamp and revision date displays
+ * @param {number} timestamp - The timestamp of the current revision.
+ */
+ setTimestamp: function (timestamp) {
+ var zeropad = function (str, length) {
+ str = str + "";
+ while (str.length < length)
+ str = '0' + str;
+ return str;
+ };
+ var months = [
+ html10n.get("timeslider.month.january"),
+ html10n.get("timeslider.month.february"),
+ html10n.get("timeslider.month.march"),
+ html10n.get("timeslider.month.april"),
+ html10n.get("timeslider.month.may"),
+ html10n.get("timeslider.month.june"),
+ html10n.get("timeslider.month.july"),
+ html10n.get("timeslider.month.august"),
+ html10n.get("timeslider.month.september"),
+ html10n.get("timeslider.month.october"),
+ html10n.get("timeslider.month.november"),
+ html10n.get("timeslider.month.december")
+ ];
+ var date = new Date(timestamp);
+ var timestamp_format = html10n.get("timeslider.dateformat",
+ {
+ "day": zeropad(date.getDate(), 2),
+ "month": zeropad(date.getMonth() + 1, 2),
+ "year": date.getFullYear(),
+ "hours": zeropad(date.getHours(), 2),
+ "minutes": zeropad(date.getMinutes(), 2),
+ "seconds": zeropad(date.getSeconds(), 2),
+ });
+ this.elements.timestamp.html(timestamp_format);
+
+ var revisionDate = html10n.get("timeslider.saved", {
+ "day": date.getDate(),
+ "month": months[date.getMonth()],
+ "year": date.getFullYear()
+ });
+
+ this.elements.revision_date.html(revisionDate);
+ },
+ /**
+ * Initialize mouse events and handlers
+ */
+ _mouseInit: function () {
+ var _this = this;
+ this.elements.button_left.on("click", function (event) {
+ if ($(this).hasClass("disabled"))
+ return;
+ _this.is_playing = false;
+ _this.goToRevision(_this.revision_number - 1);
+ });
+
+ this.elements.button_right.on("click", function (event) {
+ if ($(this).hasClass("disabled"))
+ return;
+ _this.is_playing = false;
+ _this.goToRevision(_this.revision_number + 1);
+ });
+
+ this.elements.button_play.on("click", function (event) {
+ _this.playpause();
+ });
+ }
+
+ }
+);
diff --git a/src/static/js/sliderui.js b/src/static/js/sliderui.js
new file mode 100644
index 000000000..ec98fbed2
--- /dev/null
+++ b/src/static/js/sliderui.js
@@ -0,0 +1,168 @@
+/**
+ * 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.
+ */
+require("./jquery.class");
+
+/**
+ * This is an implementation of a (very) simple Slider UI.
+ *
+ * Create a slider by doing:
+ * slider = new SliderUI(sliderbar_element, options);
+ * sliderbar_element should be a jquery wraper of the element which will serve as
+ * the bar on which the handles will be hung.
+ * optionalions is an optional dictionary which currently supports the following:
+ * value = the initial value for the default handle (default: 0)
+ * max = the maximum value for the slider (default: 100)
+ */
+
+
+/**
+ * A class for anything which can be hung off of the slider bar.
+ * I.e. this is for the handle, or saved-revisions (stars).
+ */
+$.Class("SliderHandleUI",
+ {//statics
+ },
+ {//instance
+ /**
+ * Construct the SliderHandle.
+ * @param {SliderUI} slider The slider from which this handle will be hung.
+ * @param {Number} position The initial position for this handle.
+ */
+ init: function (slider, value, type) {
+ //console.log("New SliderHandle(%d, %s)", value, type);
+ this.slider = slider;
+ this.value = value;
+ //create the element:
+ this.element = $("");
+ if (type === "")
+ type = "handle";
+ this.element.addClass("ui-slider-handle-" + type);
+ this._mouseInit();
+ },
+ _mouseInit: function () {
+ this.element.on("mousedown.sliderhandle", null, this, function(event) {
+ //console.log("sliderhandleui - mousedown");
+ });
+ },
+ }
+);
+
+//TODO:
+// - window resizing is currently broken!
+// - keyboard events
+$.Class("SliderUI",
+ {//statics
+ defaults: {
+ min: 0,
+ max: 100,
+ value: 0,
+ }
+ },
+ {//instance
+ init: function (element, options) {
+ this.options = $.extend({}, this.defaults, options);
+ this.element = element;
+ this.current_value = this.options.value;
+ this.handles = [];
+ this.createHandle(this.current_value, 'handle');
+ this._mouseInit();
+
+ // handle window resize
+ var _this = this;
+ $(window).resize(function() {
+ _this.render();
+ });
+ },
+ _getStep: function () {
+ return (this.element.width()) / (this.options.max * 1.0);
+ },
+ render: function () {
+ for(var h in this.handles) {
+ handle = this.handles[h];
+ handle.element.css('left', (handle.value * this._getStep()) );
+ }
+ },
+ /**
+ * Update the value in the UI. This should never be called internally.
+ * It should only be called by an event handler after a transition
+ * has completed.
+ * @param {number} value - The value to set.
+ */
+ setValue: function (value) {
+ if (value < 0)
+ value = 0;
+ if (value > this.options.max)
+ value = this.options.max;
+ this.handles[0].value = value;
+ this.current_value = value;
+ this.render();
+ },
+ setMax: function (max) {
+ this.options.max = max;
+ this.render();
+ },
+ createHandle: function (value, type) {
+ //console.log("createHandle(%d, %s)", value, type);
+ var handle = new SliderHandleUI(this, value, type);
+ this.handles.push(handle);
+ this.element.append(handle.element);
+ return handle;
+ },
+ _trigger: function (eventname, value) {
+ //console.log("triggering event: ", eventname);
+ if (eventname in this.options) {
+ return this.options[eventname](value);
+ }
+ },
+ _mouseInit: function () {
+ // handle all mouse events for the slider and handles right here
+ var _this = this;
+ this.element.on("mousedown.slider", function (event) {
+ if (event.target == _this.element[0] || $(event.target).hasClass("ui-slider-handle")) {
+ // the click is on the slider bar itself.
+ var start_value = Math.floor((event.clientX-_this.element.offset().left) / _this._getStep());
+ //console.log("sliderbar mousedown, value:", start_value);
+ if (_this.current_value != start_value) {
+ //_this.setValue(start_value);
+ }
+ var prev_value = start_value;
+
+ $(document).on("mousemove.slider", function (event) {
+ var current_value = Math.floor((event.clientX-_this.element.offset().left) / _this._getStep());
+ //console.log("sliderbar mousemove, value:", current_value);
+ // don't change the value if it hasn't actually changed!
+ if (prev_value != current_value) {
+ _this._trigger("slide", current_value);
+ prev_value = current_value;
+ }
+ });
+
+ $(document).on("mouseup.slider", function (event) {
+ // make sure to get rid of the handlers on document,
+ // we don't need them after this 'slide' session is done.
+ $(document).off("mouseup.slider mousemove.slider");
+ var end_value = Math.floor((event.clientX-_this.element.offset().left) / _this._getStep());
+ //console.log("sliderbar mouseup, value:", end_value);
+ // always change the value at mouseup
+ _this._trigger("change", end_value);
+
+ });
+ } else {
+ console.log("We shouldn't be here!");
+ console.log(event.target);
+ }
+ });
+ },
+ }
+);
diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js
index fd22c69a3..481a05c95 100644
--- a/src/static/js/timeslider.js
+++ b/src/static/js/timeslider.js
@@ -1,9 +1,3 @@
-/**
- * This code is mostly from the old Etherpad. Please help us to comment this code.
- * This helps other people to understand this code better and helps them to improve it.
- * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
- */
-
/**
* Copyright 2009 Google Inc.
*
@@ -23,7 +17,8 @@
// These jQuery things should create local references, but for now `require()`
// assigns to the global `$` and augments it with plugins.
require('./jquery');
-JSON = require('./json2');
+require('./jquery.class');
+//JSON = require('./json2');
var createCookie = require('./pad_utils').createCookie;
var readCookie = require('./pad_utils').readCookie;
@@ -32,7 +27,290 @@ var hooks = require('./pluginfw/hooks');
var token, padId, export_links;
-function init() {
+$.Class("SocketClient",
+ { //statics
+ VERBOSE: false,
+ },
+ { //instance
+ init: function (baseurl) {
+ this.baseurl = baseurl;
+ this.log = SocketClient.VERBOSE ? console.log : function () {};
+ // connect to the server
+ this.log("[socket_client] connecting to:", this.baseurl);
+ this.socket = io.connect(this.baseurl, {resource: "socket.io"});
+ // setup the socket callbacks:
+ _this = this;
+
+ this.socket.on("connect", function() {
+ _this.onConnect.apply(_this, arguments);
+ });
+ this.socket.on("disconnect", function() {
+ _this.onDisconnect.apply(_this, arguments);
+ });
+ this.socket.on("message", function(message) {
+ _this.onMessage.apply(_this, arguments);
+ });
+ },
+
+ onConnect: function() {
+ this.log("[socket_client] > onConnect");
+ },
+
+ onDisconnect: function() {
+ this.log("[socket_client] > onDisconnect");
+ },
+
+ /**
+ * Triggered when a new message arrives from the server.
+ * @param {object} message - The message.
+ */
+ onMessage: function(message) {
+ this.log("[socket_client] > onMessage: ", message);
+ },
+
+ /**
+ * Sends a message to the server.
+ * @param {object} message - The message to send
+ * @param {function} callback - A callback function which will be called after
+ * the message has been sent to the socket.
+ */
+ sendMessage: function(message, callback) {
+ this.log("[socket_client] > sendMessage: ", message);
+ this.socket.json.send(message);
+ if (callback)
+ callback();
+ },
+ }
+);
+
+SocketClient("AuthenticatedSocketClient",
+ { //statics
+ VERBOSE: false,
+ },
+ { //instance
+ init: function (baseurl, padID) {
+ this.log = AuthenticatedSocketClient.VERBOSE ? console.log : function () {};
+
+ //make sure we have a token
+ this.token = readCookie("token");
+ if(this.token === null)
+ {
+ this.token = "t." + randomString();
+ createCookie("token", this.token, 60);
+ }
+ this.padID = padID;
+ this.sessionID = decodeURIComponent(readCookie("sessionID"));
+ this.password = readCookie("password");
+ this.handlers = {};
+
+ this._super(baseurl);
+ },
+
+ /**
+ * Sends a pad message to the server, including all the neccessary
+ * session info and tokens.
+ * @param {string} type - The message type to send. See the server code for
+ * valid message types.
+ * @param {object} data - The data payload to be sent to the server.
+ * @param {function} callback - A callback function which will be called after
+ * the message has been sent to the socket.
+ */
+ sendMessage: function (type, data, callback) {
+ this.sessionID = decodeURIComponent(readCookie("sessionID"));
+ this.password = readCookie("password");
+ var msg = { "component" : "pad", // FIXME: Remove this stupidity!
+ "type": type,
+ "data": data,
+ "padId": this.padID,
+ "token": this.token,
+ "sessionID": this.sessionID,
+ "password": this.password,
+ "protocolVersion": 2};
+ this._super(msg, callback);
+ },
+
+ onMessage: function (message) {
+ this.log("[authorized_client] > onMessage:", message);
+ if (message.accessStatus)
+ { //access denied?
+ //TODO raise some kind of error?
+ this.log("ACCESS ERROR!");
+ }
+ this.dispatchMessage(message.type, message.data);
+ },
+
+ /**
+ * Dispatch incoming messages to handlers in subclasses or registered
+ * as event handlers.
+ * @param {string} type - The type of the message. See the server code
+ * for possible values.
+ * @param {object} data - The message payload.
+ */
+ dispatchMessage: function(type, data) {
+ this.log("[authorized_client] > dispatchMessage('%s', %s)", type, data);
+ // first call local handlers
+ if ("handle_" + type in this)
+ this["handle_" + type](data);
+ // then call registered handlers
+ if (type in this.handlers)
+ for(var h in this.handlers[type])
+ { //TODO: maybe chain the handlers into some kind of chain-of-responsibility?
+ var handler = this.handlers[type][h];
+ handler.handler.call(this, data, handler.context);
+ }
+ },
+
+ /**
+ * Register an event handler for a given message type.
+ * @param {string} type - The message type.
+ * @param {function} handler - The handler function.
+ * @param {object} context - Optionally, some context to be passed to the handler.
+ */
+ on: function(type, handler, context) {
+ if (!(type in this.handlers))
+ this.handlers[type] = [];
+ this.handlers[type].push({handler: handler, context: context});
+ return this;
+ },
+
+ /**
+ * Dispatch COLLABROOM messages.
+ * @param {object} data - The data received from the server.
+ */
+ handle_COLLABROOM: function(data) {
+ //this.log("[authsocket_client] handle_COLLABROOM: ", data);
+ this.dispatchMessage(data.type, data);
+ },
+
+ }
+);
+
+require('./revisioncache');
+require('./revisionslider');
+AuthenticatedSocketClient("TimesliderClient",
+ { //statics
+ VERBOSE: false,
+ },
+ { //instance
+ init: function (baseurl, padID) {
+ this.log = TimesliderClient.VERBOSE ? console.log : function () {};
+ this._super(baseurl, padID);
+ },
+
+ onConnect: function () {
+ this.sendMessage("CLIENT_READY", {});
+ },
+
+ initialize: function (clientVars) {
+ if (this.is_initialized)
+ return;
+ this.clientVars = clientVars;
+ var collabClientVars = this.clientVars.collab_client_vars;
+ this.savedRevisions = this.clientVars.savedRevisions;
+
+ this.revisionCache = new RevisionCache(this, collabClientVars.rev || 0);
+
+ this.padClient = new PadClient(this.revisionCache,
+ {
+ revnum: collabClientVars.rev,
+ timestamp: collabClientVars.time,
+ atext: {
+ text: collabClientVars.initialAttributedText.text,
+ attributes: collabClientVars.initialAttributedText.attribs,
+ apool: collabClientVars.apool,
+ },
+ author_info: collabClientVars.historicalAuthorData,
+ palette: this.clientVars.colorPalette,
+ });
+
+ //TODO: not wild about the timeslider-top selector being hard-coded here.
+ this.ui = new RevisionSlider(this, $("#timeslider-top"));
+ this.is_initialized = true;
+ },
+
+ // ------------------------------------------
+ // Handling events
+ handle_CLIENT_VARS: function(data) {
+ this.log("[timeslider_client] handle_CLIENT_VARS: ", data);
+ this.initialize(data);
+ },
+
+ /**
+ * Handle USER_NEWINFO messages, which let us know that a (new) user
+ * has connected to this pad.
+ * @param {object} data - the data received from the server.
+ */
+ handle_USER_NEWINFO: function (data) {
+ this.log("[timeslider_client] handle_USER_NEWINFO: ", data.userInfo);
+ //TODO: we might not want to add EVERY new user to the users list,
+ //possibly only active users?
+ var authors = {};
+ authors[data.userInfo.userId] = data.userInfo;
+ this.padClient.updateAuthors(authors);
+ this.ui.render();
+ },
+
+ /**
+ * Handle USER_LEAVE messages, which lets us know that a user has left the
+ * pad.
+ * @param {object} data - The data received from the server.
+ */
+ handle_USER_LEAVE: function (data) {
+ this.log("[timeslider_client] handle_USER_LEAVE ", data.userInfo);
+ },
+
+ /**
+ * Handle NEW_CHANGES messages, which lets us know that a new revision has
+ * been added to the pad.
+ * @param {object} data - The data received from the server.
+ */
+ handle_NEW_CHANGES: function (data) {
+ this.log("[timeslider_client] handle_NEW_CHANGES: ", data);
+ var changesets = this.padClient.mergeForeignChangeset(data.changeset, data.apool);
+ //TODO: handle calculation of real timedela based on currenttime
+ //TODO: deal with author?
+ this.revisionCache.appendHeadRevision(data.newRev, changesets.forward, changesets.reverse, data.timeDelta);
+ this.ui.render();
+ },
+
+
+ /**
+ * Go to the specified revision number. This abstracts the implementation
+ * of the actual goToRevision in the padClient.
+ * @param {number} revision_number - The revision to go to.
+ * @param {callback} atRevision_callback - Called when the transition to the
+ * revision has completed (i.e.
+ * changesets have been applied).
+ */
+ goToRevision: function (revision_number, atRevision_callback) {
+ this.padClient.goToRevision(revision_number, atRevision_callback);
+ },
+ /**
+ * Get the authors for the current pad.
+ * @return {dict} - A dictionary of author-id to Author objects.
+ */
+ getAuthors: function () {
+ return this.padClient.authors;
+ },
+ /**
+ * Get the current revision.
+ * @return {Revision} - the current revision.
+ */
+ getCurrentRevision: function () {
+ return this.padClient.revision;
+ },
+ /**
+ * Get the head revision.
+ * @return {Revision} - the head revision.
+ */
+ getHeadRevision: function () {
+ return this.revisionCache.getHeadRevision().revnum;
+ },
+ }
+);
+
+function init(baseURL) {
+ var timesliderclient;
$(document).ready(function ()
{
// start the custom js
@@ -47,7 +325,7 @@ function init() {
//ensure we have a token
token = readCookie("token");
- if(token == null)
+ if(token === null)
{
token = "t." + randomString();
createCookie("token", token, 60);
@@ -55,45 +333,43 @@ function init() {
var loc = document.location;
//get the correct port
- var port = loc.port == "" ? (loc.protocol == "https:" ? 443 : 80) : loc.port;
+ var port = loc.port === "" ? (loc.protocol == "https:" ? 443 : 80) : loc.port;
//create the url
var url = loc.protocol + "//" + loc.hostname + ":" + port + "/";
//find out in which subfolder we are
- var resource = exports.baseURL.substring(1) + 'socket.io';
-
- //build up the socket io connection
- socket = io.connect(url, {resource: resource});
+ var resource = baseURL.substring(1) + 'socket.io';
- //send the ready message once we're connected
- socket.on('connect', function()
- {
- sendSocketMsg("CLIENT_READY", {});
- });
+ var cl;
+ console.log(url, baseURL, resource, padId);
+ timesliderclient = new TimesliderClient(url, padId)
+ .on("CLIENT_VARS", function(data, context, callback) {
+ //load all script that doesn't work without the clientVars
+ BroadcastSlider = require('./broadcast_slider').init(this,fireWhenAllScriptsAreLoaded);
- socket.on('disconnect', function()
- {
- BroadcastSlider.showReconnectUI();
- });
+ //initialize export ui
+ require('./pad_impexp').padimpexp.init();
- //route the incoming messages
- socket.on('message', function(message)
- {
- if(window.console) console.log(message);
+ //change export urls when the slider moves
+ //TODO: fix this to use the slider.change event
+ //BroadcastSlider.onSlider(function(revno)
+ //{
+ //// export_links is a jQuery Array, so .each is allowed.
+ //export_links.each(function()
+ //{
+ //this.setAttribute('href', this.href.replace( /(.+?)\/\w+\/(\d+\/)?export/ , '$1/' + padId + '/' + revno + '/export'));
+ //});
+ //});
- if(message.type == "CLIENT_VARS")
- {
- handleClientVars(message);
- }
- else if(message.accessStatus)
- {
- $("body").html("You have no permission to access this pad
")
- } else {
- changesetLoader.handleMessageFromServer(message);
- }
- });
+ //fire all start functions of these scripts, formerly fired with window.load
+ for(var i=0;i < fireWhenAllScriptsAreLoaded.length;i++)
+ {
+ fireWhenAllScriptsAreLoaded[i]();
+ }
+ //$("#ui-slider-handle").css('left', $("#ui-slider-bar").width() - 2);
+ });
//get all the export links
- export_links = $('#export > .exportlink')
+ export_links = $('#export > .exportlink');
if(document.referrer.length > 0 && document.referrer.substring(document.referrer.lastIndexOf("/")-1,document.referrer.lastIndexOf("/")) === "p") {
$("#returnbutton").attr("href", document.referrer);
@@ -106,64 +382,14 @@ function init() {
window.location.reload();
});
- exports.socket = socket; // make the socket available
+ //exports.socket = socket; // make the socket available
exports.BroadcastSlider = BroadcastSlider; // Make the slider available
hooks.aCallAll("postTimesliderInit");
});
-}
-
-//sends a message over the socket
-function sendSocketMsg(type, data)
-{
- var sessionID = decodeURIComponent(readCookie("sessionID"));
- var password = readCookie("password");
-
- var msg = { "component" : "pad", // FIXME: Remove this stupidity!
- "type": type,
- "data": data,
- "padId": padId,
- "token": token,
- "sessionID": sessionID,
- "password": password,
- "protocolVersion": 2};
-
- socket.json.send(msg);
+ return timesliderclient;
}
var fireWhenAllScriptsAreLoaded = [];
-
-var changesetLoader;
-function handleClientVars(message)
-{
- //save the client Vars
- clientVars = message.data;
-
- //load all script that doesn't work without the clientVars
- BroadcastSlider = require('./broadcast_slider').loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
- require('./broadcast_revisions').loadBroadcastRevisionsJS();
- changesetLoader = require('./broadcast').loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider);
- //initialize export ui
- require('./pad_impexp').padimpexp.init();
-
- //change export urls when the slider moves
- BroadcastSlider.onSlider(function(revno)
- {
- // export_links is a jQuery Array, so .each is allowed.
- export_links.each(function()
- {
- this.setAttribute('href', this.href.replace( /(.+?)\/\w+\/(\d+\/)?export/ , '$1/' + padId + '/' + revno + '/export'));
- });
- });
-
- //fire all start functions of these scripts, formerly fired with window.load
- for(var i=0;i < fireWhenAllScriptsAreLoaded.length;i++)
- {
- fireWhenAllScriptsAreLoaded[i]();
- }
- $("#ui-slider-handle").css('left', $("#ui-slider-bar").width() - 2);
-}
-
-exports.baseURL = '';
exports.init = init;
diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html
index 2e00b8c2a..786af23d8 100644
--- a/src/templates/timeslider.html
+++ b/src/templates/timeslider.html
@@ -52,12 +52,11 @@
<% e.begin_block("timesliderTop"); %>
-
+
-
@@ -90,7 +88,7 @@
<% e.end_block(); %>
-