Merge remote-tracking branch 'remotes/origin/pr/2040' into fix-timeslider
commit
966fc25145
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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 = $('<div></div>');
|
||||
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(', ');
|
||||
|
||||
$('<span />')
|
||||
.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(' ');
|
||||
$('<span> </span>')
|
||||
.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;
|
||||
|
|
|
@ -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 <b>init</b> 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
|
||||
* <code>this._super</code>. 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
|
||||
* <h2 id='introspection'>Introspection</h2>
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* <h2>Setup and initialization methods</h2>
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* <div class='whisper'>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.
|
||||
*
|
||||
* </div>
|
||||
* @codestart
|
||||
* $.Class.extend("MyClass",
|
||||
* {
|
||||
* setup: function() {} //static setup
|
||||
* init: function() {} //static constructor
|
||||
* },
|
||||
* {
|
||||
* setup: function() {} //prototype setup
|
||||
* init: function() {} //prototype constructor
|
||||
* })
|
||||
* @codeend
|
||||
*
|
||||
* <h3>Setup</h3>
|
||||
* <p>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.</p>
|
||||
* <p>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.</p>
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* @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.
|
||||
* <h3>Init</h3>
|
||||
*
|
||||
* <p>Init functions are called after setup functions.
|
||||
* Typically, they receive the same arguments
|
||||
* as their preceding setup function. The Foo class's <code>init</code> method
|
||||
* gets called in the following example:
|
||||
* </p>
|
||||
* @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
|
||||
* <h2>Callbacks</h2>
|
||||
* <p>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
|
||||
* <code>this</code> set to the class or instance of the class.
|
||||
* </p>
|
||||
* The following example uses this.callback to make sure
|
||||
* <code>this.name</code> is available in <code>show</code>.
|
||||
* @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
|
||||
* <p>Callback is available as a static and prototype method.</p>
|
||||
* <h2>Demo</h2>
|
||||
* @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
|
||||
* <h2>Currying Arguments</h2>
|
||||
* Additional arguments to callback will fill in arguments on the returning function.
|
||||
* @codestart
|
||||
* $.Class.extend("MyClass",{
|
||||
* getData: function( <b>callback</b> ) {
|
||||
* $.get("data.json",this.callback('process',<b>callback</b>),'json');
|
||||
* },
|
||||
* process: function( <b>callback</b>, jsonData ) { //callback is added as first argument
|
||||
* jsonData.processed = true;
|
||||
* callback(jsonData);
|
||||
* }
|
||||
* },{});
|
||||
* MyClass.getData(showDataFunc)
|
||||
* @codeend
|
||||
* <h2>Nesting Functions</h2>
|
||||
* 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.
|
||||
* <h3>Example</h3>
|
||||
* @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 (<code> new Class( arguments ... )</code>).
|
||||
*
|
||||
* $.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 )]
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* <div class='whisper'>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.
|
||||
* </div>
|
||||
*
|
||||
* 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 <code>init</code> 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: (<code> new Class( arguments ... )</code>).
|
||||
*
|
||||
* $.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.
|
||||
* <h3>Quick Example</h3>
|
||||
* @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)
|
|
@ -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 '.<authorid>' class, which is applied
|
||||
// to blobs which were authored by that <authorid>.
|
||||
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 = $("<div class='" + dominfo.node.className + "' " +
|
||||
"id='" + Math.random() + "' " +
|
||||
"data-text='" + text + "'>" +
|
||||
dominfo.node.innerHTML + "</div>");
|
||||
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);
|
||||
},
|
||||
}
|
||||
);
|
|
@ -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 = $("<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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
);
|
|
@ -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 = $("<div class='ui-slider-handle'></div>");
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
|
@ -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("<h2>You have no permission to access this pad</h2>")
|
||||
} 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;
|
||||
|
|
|
@ -52,12 +52,11 @@
|
|||
<div id="timeslider-top">
|
||||
<% e.begin_block("timesliderTop"); %>
|
||||
<div id="timeslider-wrapper">
|
||||
<div id="timeslider" unselectable="on" style="display: none">
|
||||
<div id="timeslider" unselectable="on"><!--style="display: none" -->
|
||||
<div id="timeslider-left"></div>
|
||||
<div id="timeslider-right"></div>
|
||||
<div id="timer"></div>
|
||||
<div id="timeslider-slider">
|
||||
<div id="ui-slider-handle"></div>
|
||||
<div id="ui-slider-bar"></div>
|
||||
</div>
|
||||
<div id="playpause_button">
|
||||
|
@ -68,7 +67,6 @@
|
|||
<div class="stepper" id="rightstep"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="overlay">
|
||||
<div id="overlay-inner">
|
||||
<!-- -->
|
||||
|
@ -90,7 +88,7 @@
|
|||
<% e.end_block(); %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div id="timeslider-metadata">
|
||||
<h1>
|
||||
<span id="revision_label"></span>
|
||||
<span id="revision_date"></span>
|
||||
|
@ -207,14 +205,14 @@
|
|||
<!-- Bootstrap -->
|
||||
<script type="text/javascript" >
|
||||
var clientVars = {};
|
||||
var BroadcastSlider;
|
||||
var BroadcastSlider;
|
||||
var timeslider;
|
||||
(function () {
|
||||
var pathComponents = location.pathname.split('/');
|
||||
|
||||
|
||||
// Strip 'p', the padname and 'timeslider' from the pathname and set as baseURL
|
||||
var baseURL = pathComponents.slice(0,pathComponents.length-3).join('/') + '/';
|
||||
|
||||
|
||||
|
||||
require.setRootURI(baseURL + "javascripts/src");
|
||||
require.setLibraryURI(baseURL + "javascripts/lib");
|
||||
require.setGlobalKeyPath("require");
|
||||
|
@ -224,19 +222,17 @@
|
|||
if ((!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) {
|
||||
document.domain = document.domain; // for comet
|
||||
}
|
||||
|
||||
var plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
||||
var socket = require('ep_etherpad-lite/static/js/timeslider').socket;
|
||||
BroadcastSlider = require('ep_etherpad-lite/static/js/timeslider').BroadcastSlider;
|
||||
var _timeslider = require('ep_etherpad-lite/static/js/timeslider')
|
||||
var socket = _timeslider.socket;
|
||||
BroadcastSlider = _timeslider.BroadcastSlider;
|
||||
plugins.baseURL = baseURL;
|
||||
|
||||
|
||||
plugins.update(function () {
|
||||
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
||||
hooks.plugins = plugins;
|
||||
|
||||
var timeslider = require('ep_etherpad-lite/static/js/timeslider')
|
||||
timeslider.baseURL = baseURL;
|
||||
timeslider.init();
|
||||
timeslider = _timeslider.init(baseURL);
|
||||
|
||||
/* TODO: These globals shouldn't exist. */
|
||||
padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar;
|
||||
|
|
Loading…
Reference in New Issue