diff --git a/src/static/css/timeslider.css b/src/static/css/timeslider.css index b3c201847..c354858f7 100644 --- a/src/static/css/timeslider.css +++ b/src/static/css/timeslider.css @@ -1,7 +1,115 @@ +/* + * slider handles (SliderHandle) + */ + +.ui-slider-handle { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + cursor: pointer; + position: absolute; +} + +.ui-slider-handle-star { + background-image: url(../../static/img/star.png); + height: 16px; + top: 20px; + width: 15px; +} + +.ui-slider-handle-handle { + background-image: url(../../static/img/crushed_current_location.png); + height: 61px; + left: 0; + top: -14px; + width: 13px; +} + +/* + * Steppers + */ + +.stepper { + background: url(../../static/img/stepper_buttons.png) 0 0 no-repeat; + height: 21px; + overflow: hidden; + position: absolute; +} +#leftstar { + background-position: 0 -44px; + right: 34px; + top: 8px; + width: 30px; +} +#rightstar { + background-position: -29px -44px; + right: 5px; + top: 8px; + width: 30px; +} +#leftstep:active { + background-position: 0 -22px; + right: 34px; + top: 20px; + width: 30px; +} +#leftstep { + background-position: 0 -66px; + right: 34px; + top: 20px; + width: 30px; +} +#rightstep:active { + background-position: -29px -22px; + right: 5px; + top: 20px; + width: 30px; +} +#rightstep { + background-position: -29px -66px; + right: 5px; + top: 20px; + width: 30px; +} + +#playpause_button, +#playpause_button_icon { + height: 47px; + position: absolute; + width: 47px; +} +#playpause_button { + background-image: url(../../static/img/crushed_button_undepressed.png); + right: 77px; + top: 9px; +} +#playpause_button:active { + background-image: url(../../static/img/crushed_button_depressed.png); + right: 77px; + top: 9px; +} +#playpause_button_icon { + background-image: url(../../static/img/play.png); + left: 0; + top: 0; +} +.pause#playpause_button_icon { + background-image: url(../../static/img/pause.png) +} + + + + + + + +#timeslider .slider-handle { + position: absolute; +} #editorcontainerbox { overflow: auto; - top: 40px; - position: static; + top: 120px; + position: absolute; } #padcontent { font-size: 12px; @@ -46,18 +154,7 @@ top: 1px; width: 100%; } -#ui-slider-handle { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - background-image: url(../../static/img/crushed_current_location.png); - cursor: pointer; - height: 61px; - left: 0; - position: absolute; - top: 0; - width: 13px; -} + #ui-slider-bar { -webkit-user-select: none; -moz-user-select: none; @@ -69,66 +166,6 @@ position: relative; top: 20px; } -#playpause_button, -#playpause_button_icon { - height: 47px; - position: absolute; - width: 47px; -} -#playpause_button { - background-image: url(../../static/img/crushed_button_undepressed.png); - right: 77px; - top: 9px; -} -#playpause_button_icon { - background-image: url(../../static/img/play.png); - left: 0; - top: 0; -} -.pause#playpause_button_icon { - background-image: url(../../static/img/pause.png) -} -#leftstar, -#rightstar, -#leftstep, -#rightstep { - background: url(../../static/img/stepper_buttons.png) 0 0 no-repeat; - height: 21px; - overflow: hidden; - position: absolute; -} -#leftstar { - background-position: 0 -44px; - right: 34px; - top: 8px; - width: 30px; -} -#rightstar { - background-position: -29px -44px; - right: 5px; - top: 8px; - width: 29px; -} -#leftstep { - background-position: 0 -22px; - right: 34px; - top: 20px; - width: 30px; -} -#rightstep { - background-position: -29px -22px; - right: 5px; - top: 20px; - width: 30px; -} -#timeslider .star { - background-image: url(../../static/img/star.png); - cursor: pointer; - height: 16px; - position: absolute; - top: 40px; - width: 15px; -} #timeslider #timer { color: #fff; font-family: Arial, sans-serif; @@ -188,6 +225,7 @@ overflow: hidden; padding-top: 3px; width: 100%; + border-bottom: thin solid #cccccc; } .timeslider-bar #editbar { border-bottom: none; diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index d4bda1110..e75e243e0 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -1,5 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -27,13 +27,12 @@ var Changeset = require('./Changeset'); var linestylefilter = require('./linestylefilter').linestylefilter; var colorutils = require('./colorutils').colorutils; var _ = require('./underscore'); - +require("./jquery.class"); // These parameters were global, now they are injected. A reference to the // Timeslider controller would probably be more appropriate. -function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) +function loadBroadcastJS(tsclient, fireWhenAllScriptsAreLoaded, BroadcastSlider) { - var changesetLoader = undefined; - + var changesetLoader; // Below Array#indexOf code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_indexof.htm if (!Array.prototype.indexOf) { @@ -83,14 +82,14 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro var appLevelDisconnectReason = null; var padContents = { - currentRevision: clientVars.collab_client_vars.rev, - currentTime: clientVars.collab_client_vars.time, - currentLines: Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text), + currentRevision: tsclient.clientVars.collab_client_vars.rev, + currentTime: tsclient.clientVars.collab_client_vars.time, + currentLines: Changeset.splitTextLines(tsclient.clientVars.collab_client_vars.initialAttributedText.text), currentDivs: null, // to be filled in once the dom loads - apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool), + apool: (new AttribPool()).fromJsonable(tsclient.clientVars.collab_client_vars.apool), alines: Changeset.splitAttributionLines( - clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.text), + tsclient.clientVars.collab_client_vars.initialAttributedText.attribs, tsclient.clientVars.collab_client_vars.initialAttributedText.text), // generates a jquery element containing HTML for a line lineToElement: function(line, aline) @@ -250,7 +249,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro */ function applyChangeset(changeset, revision, preventSliderMovement, timeDelta) - { + { // disable the next 'gotorevision' call handled by a timeslider update if (!preventSliderMovement) { @@ -274,12 +273,12 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro debugLog('Time Delta: ', timeDelta) updateTimer(); - + var authors = _.map(padContents.getActiveAuthors(), function(name) { return authorData[name]; }); - + BroadcastSlider.setAuthors(authors); } @@ -292,7 +291,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro str = '0' + str; return str; } - + var date = new Date(padContents.currentTime); var dateFormat = function() { @@ -307,15 +306,15 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro "month": month, "year": year, "hours": hours, - "minutes": minutes, + "minutes": minutes, "seconds": seconds })); } - - - - - + + + + + $('#timer').html(dateFormat()); var revisionDate = html10n.get("timeslider.saved", { "day": date.getDate(), @@ -338,7 +337,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro $('#revision_date').html(revisionDate) } - + updateTimer(); function goToRevision(newRevision) @@ -401,7 +400,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro changesetLoader.queueUp(start, 1, update); } - + var authors = _.map(padContents.getActiveAuthors(), function(name){ return authorData[name]; }); @@ -453,7 +452,8 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro var start = request.rev; var requestID = Math.floor(Math.random() * 100000); - sendSocketMsg("CHANGESET_REQ", { + //sendSocketMsg("CHANGESET_REQ", { + tsclient.sendMessage("CHANGESET_REQ", { "start": start, "granularity": granularity, "requestID": requestID @@ -461,21 +461,21 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro self.reqCallbacks[requestID] = callback; }, - handleSocketResponse: function(message) + handle_CHANGESET_REQ: function(data) { var self = changesetLoader; - var start = message.data.start; - var granularity = message.data.granularity; - var callback = self.reqCallbacks[message.data.requestID]; - delete self.reqCallbacks[message.data.requestID]; + var start = data.start; + var granularity = data.granularity; + var callback = self.reqCallbacks[data.requestID]; + delete self.reqCallbacks[data.requestID]; - self.handleResponse(message.data, start, granularity, callback); + self.handleResponse(data, start, granularity, callback); setTimeout(self.loadFromQueue, 10); }, handleResponse: function(data, start, granularity, callback) { - debugLog("response: ", data); + debugLog("handleResponse: ", data); var pool = (new AttribPool()).fromJsonable(data.apool); for (var i = 0; i < data.forwardsChangesets.length; i++) { @@ -489,56 +489,44 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro } if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1); }, - handleMessageFromServer: function (obj) + handle_COLLABROOM: function (obj) { - debugLog("handleMessage:", arguments); - - if (obj.type == "COLLABROOM") + debugLog("handle_COLLABROOM:", arguments); + if (obj.type == "NEW_CHANGES") { - obj = obj.data; + debugLog(obj); + var changeset = Changeset.moveOpsToNewPool( + obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); - if (obj.type == "NEW_CHANGES") - { - debugLog(obj); - var changeset = Changeset.moveOpsToNewPool( - obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); + var changesetBack = Changeset.inverse( + obj.changeset, padContents.currentLines, padContents.alines, padContents.apool); - var changesetBack = Changeset.inverse( - obj.changeset, padContents.currentLines, padContents.alines, padContents.apool); + changesetBack = Changeset.moveOpsToNewPool( + changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); - var changesetBack = Changeset.moveOpsToNewPool( - changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); - - loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta); - } - else if (obj.type == "NEW_AUTHORDATA") - { - var authorMap = {}; - authorMap[obj.author] = obj.data; - receiveAuthorData(authorMap); - - var authors = _.map(padContents.getActiveAuthors(), function(name) { - return authorData[name]; - }); - - BroadcastSlider.setAuthors(authors); - } - else if (obj.type == "NEW_SAVEDREV") - { - var savedRev = obj.savedRev; - BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev); - } + loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta); } - else if(obj.type == "CHANGESET_REQ") + else if (obj.type == "NEW_AUTHORDATA") { - changesetLoader.handleSocketResponse(obj); + var authorMap = {}; + authorMap[obj.author] = obj.data; + receiveAuthorData(authorMap); + + var authors = _.map(padContents.getActiveAuthors(), function(name) { + return authorData[name]; + }); + + BroadcastSlider.setAuthors(authors); } - else + else if (obj.type == "NEW_SAVEDREV") { - debugLog("Unknown message type: " + obj.type); + var savedRev = obj.savedRev; + BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev); } } }; + //tsclient.on("CHANGESET_REQ", changesetLoader.handle_CHANGESET_REQ); + //tsclient.on("COLLABROOM", changesetLoader.handle_COLLABROOM); // to start upon window load, just push a function onto this array //window['onloadFuncts'].push(setUpSocket); @@ -547,12 +535,12 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro { // set up the currentDivs and DOM padContents.currentDivs = []; - $("#padcontent").html(""); + //$("#padcontent").html(""); for (var i = 0; i < padContents.currentLines.length; i++) { var div = padContents.lineToElement(padContents.currentLines[i], padContents.alines[i]); padContents.currentDivs.push(div); - $("#padcontent").append(div); + //$("#padcontent").append(div); } debugLog(padContents.currentDivs); }); @@ -570,7 +558,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro goToRevision.apply(goToRevision, arguments); } } - + BroadcastSlider.onSlider(goToRevisionIfEnabled); var dynamicCSS = makeCSSManager('dynamicsyntax'); @@ -581,7 +569,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro for (var author in newAuthorData) { var data = newAuthorData[author]; - var bgcolor = typeof data.colorId == "number" ? clientVars.colorPalette[data.colorId] : data.colorId; + var bgcolor = typeof data.colorId == "number" ? tsclient.clientVars.colorPalette[data.colorId] : data.colorId; if (bgcolor && dynamicCSS) { var selector = dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author)); @@ -592,7 +580,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro } } - receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData); + receiveAuthorData(tsclient.clientVars.collab_client_vars.historicalAuthorData); return changesetLoader; } diff --git a/src/static/js/broadcast_revisions.js b/src/static/js/broadcast_revisions.js deleted file mode 100644 index 1980bdf30..000000000 --- a/src/static/js/broadcast_revisions.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - -/** - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// revision info is a skip list whos entries represent a particular revision -// of the document. These revisions are connected together by various -// changesets, or deltas, between any two revisions. - -function loadBroadcastRevisionsJS() -{ - function Revision(revNum) - { - this.rev = revNum; - this.changesets = []; - } - - Revision.prototype.addChangeset = function(destIndex, changeset, timeDelta) - { - var changesetWrapper = { - deltaRev: destIndex - this.rev, - deltaTime: timeDelta, - getValue: function() - { - return changeset; - } - }; - this.changesets.push(changesetWrapper); - this.changesets.sort(function(a, b) - { - return (b.deltaRev - a.deltaRev) - }); - } - - revisionInfo = {}; - revisionInfo.addChangeset = function(fromIndex, toIndex, changeset, backChangeset, timeDelta) - { - var startRevision = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex); - var endRevision = revisionInfo[toIndex] || revisionInfo.createNew(toIndex); - startRevision.addChangeset(toIndex, changeset, timeDelta); - endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta); - } - - revisionInfo.latest = clientVars.collab_client_vars.rev || -1; - - revisionInfo.createNew = function(index) - { - revisionInfo[index] = new Revision(index); - if (index > revisionInfo.latest) - { - revisionInfo.latest = index; - } - - return revisionInfo[index]; - } - - // assuming that there is a path from fromIndex to toIndex, and that the links - // are laid out in a skip-list format - revisionInfo.getPath = function(fromIndex, toIndex) - { - var changesets = []; - var spans = []; - var times = []; - var elem = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex); - if (elem.changesets.length != 0 && fromIndex != toIndex) - { - var reverse = !(fromIndex < toIndex) - while (((elem.rev < toIndex) && !reverse) || ((elem.rev > toIndex) && reverse)) - { - var couldNotContinue = false; - var oldRev = elem.rev; - - for (var i = reverse ? elem.changesets.length - 1 : 0; - reverse ? i >= 0 : i < elem.changesets.length; - i += reverse ? -1 : 1) - { - if (((elem.changesets[i].deltaRev < 0) && !reverse) || ((elem.changesets[i].deltaRev > 0) && reverse)) - { - couldNotContinue = true; - break; - } - - if (((elem.rev + elem.changesets[i].deltaRev <= toIndex) && !reverse) || ((elem.rev + elem.changesets[i].deltaRev >= toIndex) && reverse)) - { - var topush = elem.changesets[i]; - changesets.push(topush.getValue()); - spans.push(elem.changesets[i].deltaRev); - times.push(topush.deltaTime); - elem = revisionInfo[elem.rev + elem.changesets[i].deltaRev]; - break; - } - } - - if (couldNotContinue || oldRev == elem.rev) break; - } - } - - var status = 'partial'; - if (elem.rev == toIndex) status = 'complete'; - - return { - 'fromRev': fromIndex, - 'rev': elem.rev, - 'status': status, - 'changesets': changesets, - 'spans': spans, - 'times': times - }; - } -} - -exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS; diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js index 8179b7b5f..35da14198 100644 --- a/src/static/js/broadcast_slider.js +++ b/src/static/js/broadcast_slider.js @@ -1,9 +1,3 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - /** * Copyright 2009 Google Inc. * @@ -20,23 +14,19 @@ * limitations under the License. */ - // These parameters were global, now they are injected. A reference to the - // Timeslider controller would probably be more appropriate. var _ = require('./underscore'); var padmodals = require('./pad_modals').padmodals; -function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) +function init(connection, fireWhenAllScriptsAreLoaded) { var BroadcastSlider; (function() { // wrap this code in its own namespace - var sliderLength = 1000; - var sliderPos = 0; - var sliderActive = false; - var slidercallbacks = []; - var savedRevisions = []; - var sliderPlaying = false; + + + + var clientVars = connection.clientVars; function disableSelection(element) { @@ -48,113 +38,6 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) element.style.MozUserSelect = "none"; element.style.cursor = "default"; } - var _callSliderCallbacks = function(newval) - { - sliderPos = newval; - for (var i = 0; i < slidercallbacks.length; i++) - { - slidercallbacks[i](newval); - } - } - - var updateSliderElements = function() - { - for (var i = 0; i < savedRevisions.length; i++) - { - var position = parseInt(savedRevisions[i].attr('pos')); - savedRevisions[i].css('left', (position * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)) - 1); - } - $("#ui-slider-handle").css('left', sliderPos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)); - } - - var addSavedRevision = function(position, info) - { - var newSavedRevision = $('
'); - newSavedRevision.addClass("star"); - - newSavedRevision.attr('pos', position); - newSavedRevision.css('position', 'absolute'); - newSavedRevision.css('left', (position * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)) - 1); - $("#timeslider-slider").append(newSavedRevision); - newSavedRevision.mouseup(function(evt) - { - BroadcastSlider.setSliderPosition(position); - }); - savedRevisions.push(newSavedRevision); - }; - - var removeSavedRevision = function(position) - { - var element = $("div.star [pos=" + position + "]"); - savedRevisions.remove(element); - element.remove(); - return element; - }; - - /* Begin small 'API' */ - - function onSlider(callback) - { - slidercallbacks.push(callback); - } - - function getSliderPosition() - { - return sliderPos; - } - - function setSliderPosition(newpos) - { - newpos = Number(newpos); - if (newpos < 0 || newpos > sliderLength) return; - if(!newpos){ - newpos = 0; // stops it from displaying NaN if newpos isn't set - } - window.location.hash = "#" + newpos; - $("#ui-slider-handle").css('left', newpos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)); - $("a.tlink").map(function() - { - $(this).attr('href', $(this).attr('thref').replace("%revision%", newpos)); - }); - - $("#revision_label").html(html10n.get("timeslider.version", { "version": newpos})); - - if (newpos == 0) - { - $("#leftstar").css('opacity', .5); - $("#leftstep").css('opacity', .5); - } - else - { - $("#leftstar").css('opacity', 1); - $("#leftstep").css('opacity', 1); - } - - if (newpos == sliderLength) - { - $("#rightstar").css('opacity', .5); - $("#rightstep").css('opacity', .5); - } - else - { - $("#rightstar").css('opacity', 1); - $("#rightstep").css('opacity', 1); - } - - sliderPos = newpos; - _callSliderCallbacks(newpos); - } - - function getSliderLength() - { - return sliderLength; - } - - function setSliderLength(newlength) - { - sliderLength = newlength; - updateSliderElements(); - } // just take over the whole slider screen with a reconnect message @@ -163,117 +46,11 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) padmodals.showModal("disconnected"); } + //TODO: figure out what the hell this is for var fixPadHeight = _.throttle(function(){ var height = $('#timeslider-top').height(); $('#editorcontainerbox').css({marginTop: height}); }, 600); - - function setAuthors(authors) - { - var authorsList = $("#authorsList"); - authorsList.empty(); - var numAnonymous = 0; - var numNamed = 0; - var colorsAnonymous = []; - _.each(authors, function(author) - { - var authorColor = clientVars.colorPalette[author.colorId] || author.colorId; - if (author.name) - { - if (numNamed !== 0) authorsList.append(', '); - - $('') - .text(author.name || "unnamed") - .css('background-color', authorColor) - .addClass('author') - .appendTo(authorsList); - - numNamed++; - } - else - { - numAnonymous++; - if(authorColor) colorsAnonymous.push(authorColor); - } - }); - if (numAnonymous > 0) - { - var anonymousAuthorString = html10n.get("timeslider.unnamedauthors", { num: numAnonymous }); - - if (numNamed !== 0){ - authorsList.append(' + ' + anonymousAuthorString); - } else { - authorsList.append(anonymousAuthorString); - } - - if(colorsAnonymous.length > 0){ - authorsList.append(' ('); - _.each(colorsAnonymous, function(color, i){ - if( i > 0 ) authorsList.append(' '); - $(' ') - .css('background-color', color) - .addClass('author author-anonymous') - .appendTo(authorsList); - }); - authorsList.append(')'); - } - - } - if (authors.length == 0) - { - authorsList.append(html10n.get("timeslider.toolbar.authorsList")); - } - - fixPadHeight(); - } - - BroadcastSlider = { - onSlider: onSlider, - getSliderPosition: getSliderPosition, - setSliderPosition: setSliderPosition, - getSliderLength: getSliderLength, - setSliderLength: setSliderLength, - isSliderActive: function() - { - return sliderActive; - }, - playpause: playpause, - addSavedRevision: addSavedRevision, - showReconnectUI: showReconnectUI, - setAuthors: setAuthors - } - - function playButtonUpdater() - { - if (sliderPlaying) - { - if (getSliderPosition() + 1 > sliderLength) - { - $("#playpause_button_icon").toggleClass('pause'); - sliderPlaying = false; - return; - } - setSliderPosition(getSliderPosition() + 1); - - setTimeout(playButtonUpdater, 100); - } - } - - function playpause() - { - $("#playpause_button_icon").toggleClass('pause'); - - if (!sliderPlaying) - { - if (getSliderPosition() == sliderLength) setSliderPosition(0); - sliderPlaying = true; - playButtonUpdater(); - } - else - { - sliderPlaying = false; - } - } // assign event handlers to html UI elements after page load //$(window).load(function () @@ -281,212 +58,15 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) { disableSelection($("#playpause_button")[0]); disableSelection($("#timeslider")[0]); - - $(document).keyup(function(e) - { - var code = -1; - if (!e) var e = window.event; - if (e.keyCode) code = e.keyCode; - else if (e.which) code = e.which; - - if (code == 37) - { // left - if (!e.shiftKey) - { - setSliderPosition(getSliderPosition() - 1); - } - else - { - var nextStar = 0; // default to first revision in document - for (var i = 0; i < savedRevisions.length; i++) - { - var pos = parseInt(savedRevisions[i].attr('pos')); - if (pos < getSliderPosition() && nextStar < pos) nextStar = pos; - } - setSliderPosition(nextStar); - } - } - else if (code == 39) - { - if (!e.shiftKey) - { - setSliderPosition(getSliderPosition() + 1); - } - else - { - var nextStar = sliderLength; // default to last revision in document - for (var i = 0; i < savedRevisions.length; i++) - { - var pos = parseInt(savedRevisions[i].attr('pos')); - if (pos > getSliderPosition() && nextStar > pos) nextStar = pos; - } - setSliderPosition(nextStar); - } - } - else if (code == 32) playpause(); - - }); - - $(window).resize(function() - { - updateSliderElements(); - }); - - $("#ui-slider-bar").mousedown(function(evt) - { - setSliderPosition(Math.floor((evt.clientX - $("#ui-slider-bar").offset().left) * sliderLength / 742)); - $("#ui-slider-handle").css('left', (evt.clientX - $("#ui-slider-bar").offset().left)); - $("#ui-slider-handle").trigger(evt); - }); - - // Slider dragging - $("#ui-slider-handle").mousedown(function(evt) - { - this.startLoc = evt.clientX; - this.currentLoc = parseInt($(this).css('left')); - var self = this; - sliderActive = true; - $(document).mousemove(function(evt2) - { - $(self).css('pointer', 'move') - var newloc = self.currentLoc + (evt2.clientX - self.startLoc); - if (newloc < 0) newloc = 0; - if (newloc > ($("#ui-slider-bar").width() - 2)) newloc = ($("#ui-slider-bar").width() - 2); - $("#revision_label").html(html10n.get("timeslider.version", { "version": Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))})); - $(self).css('left', newloc); - if (getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))) _callSliderCallbacks(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))) - }); - $(document).mouseup(function(evt2) - { - $(document).unbind('mousemove'); - $(document).unbind('mouseup'); - sliderActive = false; - var newloc = self.currentLoc + (evt2.clientX - self.startLoc); - if (newloc < 0) newloc = 0; - if (newloc > ($("#ui-slider-bar").width() - 2)) newloc = ($("#ui-slider-bar").width() - 2); - $(self).css('left', newloc); - // if(getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2))) - setSliderPosition(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))) - if(parseInt($(self).css('left')) < 2){ - $(self).css('left', '2px'); - }else{ - self.currentLoc = parseInt($(self).css('left')); - } - }); - }) - - // play/pause toggling - $("#playpause_button").mousedown(function(evt) - { - var self = this; - - $(self).css('background-image', 'url(/static/img/crushed_button_depressed.png)'); - $(self).mouseup(function(evt2) - { - $(self).css('background-image', 'url(/static/img/crushed_button_undepressed.png)'); - $(self).unbind('mouseup'); - BroadcastSlider.playpause(); - }); - $(document).mouseup(function(evt2) - { - $(self).css('background-image', 'url(/static/img/crushed_button_undepressed.png)'); - $(document).unbind('mouseup'); - }); - }); - - // next/prev saved revision and changeset - $('.stepper').mousedown(function(evt) - { - var self = this; - var origcss = $(self).css('background-position'); - if (!origcss) - { - origcss = $(self).css('background-position-x') + " " + $(self).css('background-position-y'); - } - var origpos = parseInt(origcss.split(" ")[1]); - var newpos = (origpos - 43); - if (newpos < 0) newpos += 87; - - var newcss = (origcss.split(" ")[0] + " " + newpos + "px"); - if ($(self).css('opacity') != 1.0) newcss = origcss; - - $(self).css('background-position', newcss) - - $(self).mouseup(function(evt2) - { - $(self).css('background-position', origcss); - $(self).unbind('mouseup'); - $(document).unbind('mouseup'); - if ($(self).attr("id") == ("leftstep")) - { - setSliderPosition(getSliderPosition() - 1); - } - else if ($(self).attr("id") == ("rightstep")) - { - setSliderPosition(getSliderPosition() + 1); - } - else if ($(self).attr("id") == ("leftstar")) - { - var nextStar = 0; // default to first revision in document - for (var i = 0; i < savedRevisions.length; i++) - { - var pos = parseInt(savedRevisions[i].attr('pos')); - if (pos < getSliderPosition() && nextStar < pos) nextStar = pos; - } - setSliderPosition(nextStar); - } - else if ($(self).attr("id") == ("rightstar")) - { - var nextStar = sliderLength; // default to last revision in document - for (var i = 0; i < savedRevisions.length; i++) - { - var pos = parseInt(savedRevisions[i].attr('pos')); - if (pos > getSliderPosition() && nextStar > pos) nextStar = pos; - } - setSliderPosition(nextStar); - } - }); - $(document).mouseup(function(evt2) - { - $(self).css('background-position', origcss); - $(self).unbind('mouseup'); - $(document).unbind('mouseup'); - }); - }) if (clientVars) { $("#timeslider").show(); - - var startPos = clientVars.collab_client_vars.rev; - if(window.location.hash.length > 1) - { - var hashRev = Number(window.location.hash.substr(1)); - if(!isNaN(hashRev)) - { - // this is necessary because of the socket.io-event which loads the changesets - setTimeout(function() { setSliderPosition(hashRev); }, 1); - } - } - - setSliderLength(clientVars.collab_client_vars.rev); - setSliderPosition(clientVars.collab_client_vars.rev); - - _.each(clientVars.savedRevisions, function(revision) - { - addSavedRevision(revision.revNum, revision); - }) - + } }); })(); - BroadcastSlider.onSlider(function(loc) - { - $("#viewlatest").html(loc == BroadcastSlider.getSliderLength() ? "Viewing latest content" : "View latest content"); - }) - - return BroadcastSlider; } -exports.loadBroadcastSliderJS = loadBroadcastSliderJS; +exports.init = init; diff --git a/src/static/js/jquery.class.js b/src/static/js/jquery.class.js new file mode 100644 index 000000000..4657c6ab4 --- /dev/null +++ b/src/static/js/jquery.class.js @@ -0,0 +1,836 @@ +(function( $ ) { + // Several of the methods in this plugin use code adapated from Prototype + // Prototype JavaScript framework, version 1.6.0.1 + // (c) 2005-2007 Sam Stephenson + var regs = { + undHash: /_|-/, + colons: /::/, + words: /([A-Z]+)([A-Z][a-z])/g, + lowUp: /([a-z\d])([A-Z])/g, + dash: /([a-z\d])([A-Z])/g, + replacer: /\{([^\}]+)\}/g, + dot: /\./ + }, + getNext = function(current, nextPart, add){ + return current[nextPart] || ( add && (current[nextPart] = {}) ); + }, + isContainer = function(current){ + var type = typeof current; + return type && ( type == 'function' || type == 'object' ); + }, + getObject = function( objectName, roots, add ) { + + var parts = objectName ? objectName.split(regs.dot) : [], + length = parts.length, + currents = $.isArray(roots) ? roots : [roots || window], + current, + ret, + i, + c = 0, + type; + + if(length == 0){ + return currents[0]; + } + while(current = currents[c++]){ + for (i =0; i < length - 1 && isContainer(current); i++ ) { + current = getNext(current, parts[i], add); + } + if( isContainer(current) ) { + + ret = getNext(current, parts[i], add); + + if( ret !== undefined ) { + + if ( add === false ) { + delete current[parts[i]]; + } + return ret; + + } + + } + } + }, + + /** + * @class jQuery.String + * + * A collection of useful string helpers. + * + */ + str = $.String = $.extend( $.String || {} , { + /** + * @function + * Gets an object from a string. + * @param {String} name the name of the object to look for + * @param {Array} [roots] an array of root objects to look for the name + * @param {Boolean} [add] true to add missing objects to + * the path. false to remove found properties. undefined to + * not modify the root object + */ + getObject : getObject, + /** + * Capitalizes a string + * @param {String} s the string. + * @return {String} a string with the first character capitalized. + */ + capitalize: function( s, cache ) { + return s.charAt(0).toUpperCase() + s.substr(1); + }, + /** + * Capitalizes a string from something undercored. Examples: + * @codestart + * jQuery.String.camelize("one_two") //-> "oneTwo" + * "three-four".camelize() //-> threeFour + * @codeend + * @param {String} s + * @return {String} a the camelized string + */ + camelize: function( s ) { + s = str.classize(s); + return s.charAt(0).toLowerCase() + s.substr(1); + }, + /** + * Like camelize, but the first part is also capitalized + * @param {String} s + * @return {String} the classized string + */ + classize: function( s , join) { + var parts = s.split(regs.undHash), + i = 0; + for (; i < parts.length; i++ ) { + parts[i] = str.capitalize(parts[i]); + } + + return parts.join(join || ''); + }, + /** + * Like [jQuery.String.classize|classize], but a space separates each 'word' + * @codestart + * jQuery.String.niceName("one_two") //-> "One Two" + * @codeend + * @param {String} s + * @return {String} the niceName + */ + niceName: function( s ) { + str.classize(parts[i],' '); + }, + + /** + * Underscores a string. + * @codestart + * jQuery.String.underscore("OneTwo") //-> "one_two" + * @codeend + * @param {String} s + * @return {String} the underscored string + */ + underscore: function( s ) { + return s.replace(regs.colons, '/').replace(regs.words, '$1_$2').replace(regs.lowUp, '$1_$2').replace(regs.dash, '_').toLowerCase(); + }, + /** + * Returns a string with {param} replaced values from data. + * + * $.String.sub("foo {bar}",{bar: "far"}) + * //-> "foo far" + * + * @param {String} s The string to replace + * @param {Object} data The data to be used to look for properties. If it's an array, multiple + * objects can be used. + * @param {Boolean} [remove] if a match is found, remove the property from the object + */ + sub: function( s, data, remove ) { + var obs = []; + obs.push(s.replace(regs.replacer, function( whole, inside ) { + //convert inside to type + var ob = getObject(inside, data, typeof remove == 'boolean' ? !remove : remove), + type = typeof ob; + if((type === 'object' || type === 'function') && type !== null){ + obs.push(ob); + return ""; + }else{ + return ""+ob; + } + })); + return obs.length <= 1 ? obs[0] : obs; + } + }); + +})(jQuery); +(function( $ ) { + + // if we are initializing a new class + var initializing = false, + makeArray = $.makeArray, + isFunction = $.isFunction, + isArray = $.isArray, + extend = $.extend, + concatArgs = function(arr, args){ + return arr.concat(makeArray(args)); + }, + // tests if we can get super in .toString() + fnTest = /xyz/.test(function() { + xyz; + }) ? /\b_super\b/ : /.*/, + // overwrites an object with methods, sets up _super + // newProps - new properties + // oldProps - where the old properties might be + // addTo - what we are adding to + inheritProps = function( newProps, oldProps, addTo ) { + addTo = addTo || newProps + for ( var name in newProps ) { + // Check if we're overwriting an existing function + addTo[name] = isFunction(newProps[name]) && + isFunction(oldProps[name]) && + fnTest.test(newProps[name]) ? (function( name, fn ) { + return function() { + var tmp = this._super, + ret; + + // Add a new ._super() method that is the same method + // but on the super-class + this._super = oldProps[name]; + + // The method only need to be bound temporarily, so we + // remove it when we're done executing + ret = fn.apply(this, arguments); + this._super = tmp; + return ret; + }; + })(name, newProps[name]) : newProps[name]; + } + }, + + + /** + * @class jQuery.Class + * @plugin jquery/class + * @tag core + * @download dist/jquery/jquery.class.js + * @test jquery/class/qunit.html + * + * Class provides simulated inheritance in JavaScript. Use clss to bridge the gap between + * jQuery's functional programming style and Object Oriented Programming. It + * is based off John Resig's [http://ejohn.org/blog/simple-javascript-inheritance/|Simple Class] + * Inheritance library. Besides prototypal inheritance, it includes a few important features: + * + * - Static inheritance + * - Introspection + * - Namespaces + * - Setup and initialization methods + * - Easy callback function creation + * + * + * ## Static v. Prototype + * + * Before learning about Class, it's important to + * understand the difference between + * a class's __static__ and __prototype__ properties. + * + * //STATIC + * MyClass.staticProperty //shared property + * + * //PROTOTYPE + * myclass = new MyClass() + * myclass.prototypeMethod() //instance method + * + * A static (or class) property is on the Class constructor + * function itself + * and can be thought of being shared by all instances of the + * Class. Prototype propertes are available only on instances of the Class. + * + * ## A Basic Class + * + * The following creates a Monster class with a + * name (for introspection), static, and prototype members. + * Every time a monster instance is created, the static + * count is incremented. + * + * @codestart + * $.Class.extend('Monster', + * /* @static *| + * { + * count: 0 + * }, + * /* @prototype *| + * { + * init: function( name ) { + * + * // saves name on the monster instance + * this.name = name; + * + * // sets the health + * this.health = 10; + * + * // increments count + * this.Class.count++; + * }, + * eat: function( smallChildren ){ + * this.health += smallChildren; + * }, + * fight: function() { + * this.health -= 2; + * } + * }); + * + * hydra = new Monster('hydra'); + * + * dragon = new Monster('dragon'); + * + * hydra.name // -> hydra + * Monster.count // -> 2 + * Monster.shortName // -> 'Monster' + * + * hydra.eat(2); // health = 12 + * + * dragon.fight(); // health = 8 + * + * @codeend + * + * + * Notice that the prototype init function is called when a new instance of Monster is created. + * + * + * ## Inheritance + * + * When a class is extended, all static and prototype properties are available on the new class. + * If you overwrite a function, you can call the base class's function by calling + * this._super. Lets create a SeaMonster class. SeaMonsters are less + * efficient at eating small children, but more powerful fighters. + * + * + * Monster.extend("SeaMonster",{ + * eat: function( smallChildren ) { + * this._super(smallChildren / 2); + * }, + * fight: function() { + * this.health -= 1; + * } + * }); + * + * lockNess = new SeaMonster('Lock Ness'); + * lockNess.eat(4); //health = 12 + * lockNess.fight(); //health = 11 + * + * ### Static property inheritance + * + * You can also inherit static properties in the same way: + * + * $.Class.extend("First", + * { + * staticMethod: function() { return 1;} + * },{}) + * + * First.extend("Second",{ + * staticMethod: function() { return this._super()+1;} + * },{}) + * + * Second.staticMethod() // -> 2 + * + * ## Namespaces + * + * Namespaces are a good idea! We encourage you to namespace all of your code. + * It makes it possible to drop your code into another app without problems. + * Making a namespaced class is easy: + * + * @codestart + * $.Class.extend("MyNamespace.MyClass",{},{}); + * + * new MyNamespace.MyClass() + * @codeend + *

Introspection

+ * Often, it's nice to create classes whose name helps determine functionality. Ruby on + * Rails's [http://api.rubyonrails.org/classes/ActiveRecord/Base.html|ActiveRecord] ORM class + * is a great example of this. Unfortunately, JavaScript doesn't have a way of determining + * an object's name, so the developer must provide a name. Class fixes this by taking a String name for the class. + * @codestart + * $.Class.extend("MyOrg.MyClass",{},{}) + * MyOrg.MyClass.shortName //-> 'MyClass' + * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' + * @codeend + * The fullName (with namespaces) and the shortName (without namespaces) are added to the Class's + * static properties. + * + * + *

Setup and initialization methods

+ *

+ * Class provides static and prototype initialization functions. + * These come in two flavors - setup and init. + * Setup is called before init and + * can be used to 'normalize' init's arguments. + *

+ *
PRO TIP: Typically, you don't need setup methods in your classes. Use Init instead. + * Reserve setup methods for when you need to do complex pre-processing of your class before init is called. + * + *
+ * @codestart + * $.Class.extend("MyClass", + * { + * setup: function() {} //static setup + * init: function() {} //static constructor + * }, + * { + * setup: function() {} //prototype setup + * init: function() {} //prototype constructor + * }) + * @codeend + * + *

Setup

+ *

Setup functions are called before init functions. Static setup functions are passed + * the base class followed by arguments passed to the extend function. + * Prototype static functions are passed the Class constructor function arguments.

+ *

If a setup function returns an array, that array will be used as the arguments + * for the following init method. This provides setup functions the ability to normalize + * arguments passed to the init constructors. They are also excellent places + * to put setup code you want to almost always run.

+ *

+ * The following is similar to how [jQuery.Controller.prototype.setup] + * makes sure init is always called with a jQuery element and merged options + * even if it is passed a raw + * HTMLElement and no second parameter. + *

+ * @codestart + * $.Class.extend("jQuery.Controller",{ + * ... + * },{ + * setup: function( el, options ) { + * ... + * return [$(el), + * $.extend(true, + * this.Class.defaults, + * options || {} ) ] + * } + * }) + * @codeend + * Typically, you won't need to make or overwrite setup functions. + *

Init

+ * + *

Init functions are called after setup functions. + * Typically, they receive the same arguments + * as their preceding setup function. The Foo class's init method + * gets called in the following example: + *

+ * @codestart + * $.Class.Extend("Foo", { + * init: function( arg1, arg2, arg3 ) { + * this.sum = arg1+arg2+arg3; + * } + * }) + * var foo = new Foo(1,2,3); + * foo.sum //-> 6 + * @codeend + *

Callbacks

+ *

Similar to jQuery's proxy method, Class provides a + * [jQuery.Class.static.callback callback] + * function that returns a callback to a method that will always + * have + * this set to the class or instance of the class. + *

+ * The following example uses this.callback to make sure + * this.name is available in show. + * @codestart + * $.Class.extend("Todo",{ + * init: function( name ) { this.name = name } + * get: function() { + * $.get("/stuff",this.callback('show')) + * }, + * show: function( txt ) { + * alert(this.name+txt) + * } + * }) + * new Todo("Trash").get() + * @codeend + *

Callback is available as a static and prototype method.

+ *

Demo

+ * @demo jquery/class/class.html + * + * @constructor Creating a new instance of an object that has extended jQuery.Class + * calls the init prototype function and returns a new instance of the class. + * + */ + + clss = $.Class = function() { + if (arguments.length) { + clss.extend.apply(clss, arguments); + } + }; + + /* @Static*/ + extend(clss, { + /** + * @function callback + * Returns a callback function for a function on this Class. + * The callback function ensures that 'this' is set appropriately. + * @codestart + * $.Class.extend("MyClass",{ + * getData: function() { + * this.showing = null; + * $.get("data.json",this.callback('gotData'),'json') + * }, + * gotData: function( data ) { + * this.showing = data; + * } + * },{}); + * MyClass.showData(); + * @codeend + *

Currying Arguments

+ * Additional arguments to callback will fill in arguments on the returning function. + * @codestart + * $.Class.extend("MyClass",{ + * getData: function( callback ) { + * $.get("data.json",this.callback('process',callback),'json'); + * }, + * process: function( callback, jsonData ) { //callback is added as first argument + * jsonData.processed = true; + * callback(jsonData); + * } + * },{}); + * MyClass.getData(showDataFunc) + * @codeend + *

Nesting Functions

+ * Callback can take an array of functions to call as the first argument. When the returned callback function + * is called each function in the array is passed the return value of the prior function. This is often used + * to eliminate currying initial arguments. + * @codestart + * $.Class.extend("MyClass",{ + * getData: function( callback ) { + * //calls process, then callback with value from process + * $.get("data.json",this.callback(['process2',callback]),'json') + * }, + * process2: function( type,jsonData ) { + * jsonData.processed = true; + * return [jsonData]; + * } + * },{}); + * MyClass.getData(showDataFunc); + * @codeend + * @param {String|Array} fname If a string, it represents the function to be called. + * If it is an array, it will call each function in order and pass the return value of the prior function to the + * next function. + * @return {Function} the callback function. + */ + callback: function( funcs ) { + + //args that should be curried + var args = makeArray(arguments), + self; + + funcs = args.shift(); + + if (!isArray(funcs) ) { + funcs = [funcs]; + } + + self = this; + + return function class_cb() { + var cur = concatArgs(args, arguments), + isString, + length = funcs.length, + f = 0, + func; + + for (; f < length; f++ ) { + func = funcs[f]; + if (!func ) { + continue; + } + + isString = typeof func == "string"; + if ( isString && self._set_called ) { + self.called = func; + } + cur = (isString ? self[func] : func).apply(self, cur || []); + if ( f < length - 1 ) { + cur = !isArray(cur) || cur._use_call ? [cur] : cur + } + } + return cur; + } + }, + /** + * @function getObject + * Gets an object from a String. + * If the object or namespaces the string represent do not + * exist it will create them. + * @codestart + * Foo = {Bar: {Zar: {"Ted"}}} + * $.Class.getobject("Foo.Bar.Zar") //-> "Ted" + * @codeend + * @param {String} objectName the object you want to get + * @param {Object} [current=window] the object you want to look in. + * @return {Object} the object you are looking for. + */ + getObject: $.String.getObject, + /** + * @function newInstance + * Creates a new instance of the class. This method is useful for creating new instances + * with arbitrary parameters. + *

Example

+ * @codestart + * $.Class.extend("MyClass",{},{}) + * var mc = MyClass.newInstance.apply(null, new Array(parseInt(Math.random()*10,10)) + * @codeend + * @return {class} instance of the class + */ + newInstance: function() { + var inst = this.rawInstance(), + args; + if ( inst.setup ) { + args = inst.setup.apply(inst, arguments); + } + if ( inst.init ) { + inst.init.apply(inst, isArray(args) ? args : arguments); + } + return inst; + }, + /** + * Setup gets called on the inherting class with the base class followed by the + * inheriting class's raw properties. + * + * Setup will deeply extend a static defaults property on the base class with + * properties on the base class. For example: + * + * $.Class("MyBase",{ + * defaults : { + * foo: 'bar' + * } + * },{}) + * + * MyBase("Inheriting",{ + * defaults : { + * newProp : 'newVal' + * } + * },{} + * + * Inheriting.defaults -> {foo: 'bar', 'newProp': 'newVal'} + * + * @param {Object} baseClass the base class that is being inherited from + * @param {String} fullName the name of the new class + * @param {Object} staticProps the static properties of the new class + * @param {Object} protoProps the prototype properties of the new class + */ + setup: function( baseClass, fullName ) { + this.defaults = extend(true, {}, baseClass.defaults, this.defaults); + return arguments; + }, + rawInstance: function() { + initializing = true; + var inst = new this(); + initializing = false; + return inst; + }, + /** + * Extends a class with new static and prototype functions. There are a variety of ways + * to use extend: + * @codestart + * //with className, static and prototype functions + * $.Class.extend('Task',{ STATIC },{ PROTOTYPE }) + * //with just classname and prototype functions + * $.Class.extend('Task',{ PROTOTYPE }) + * //With just a className + * $.Class.extend('Task') + * @codeend + * @param {String} [fullName] the classes name (used for classes w/ introspection) + * @param {Object} [klass] the new classes static/class functions + * @param {Object} [proto] the new classes prototype functions + * @return {jQuery.Class} returns the new class + */ + extend: function( fullName, klass, proto ) { + // figure out what was passed + if ( typeof fullName != 'string' ) { + proto = klass; + klass = fullName; + fullName = null; + } + if (!proto ) { + proto = klass; + klass = null; + } + + proto = proto || {}; + var _super_class = this, + _super = this.prototype, + name, shortName, namespace, prototype; + + // Instantiate a base class (but only create the instance, + // don't run the init constructor) + initializing = true; + prototype = new this(); + initializing = false; + // Copy the properties over onto the new prototype + inheritProps(proto, _super, prototype); + + // The dummy class constructor + + function Class() { + // All construction is actually done in the init method + if ( initializing ) return; + + if ( this.constructor !== Class && arguments.length ) { //we are being called w/o new + return arguments.callee.extend.apply(arguments.callee, arguments) + } else { //we are being called w/ new + return this.Class.newInstance.apply(this.Class, arguments) + } + } + // Copy old stuff onto class + for ( name in this ) { + if ( this.hasOwnProperty(name) ) { + Class[name] = this[name]; + } + } + + // copy new props on class + inheritProps(klass, this, Class); + + // do namespace stuff + if ( fullName ) { + + var parts = fullName.split(/\./), + shortName = parts.pop(), + current = clss.getObject(parts.join('.'), window, true), + namespace = current; + + + current[shortName] = Class; + } + + // set things that can't be overwritten + extend(Class, { + prototype: prototype, + namespace: namespace, + shortName: shortName, + constructor: Class, + fullName: fullName + }); + + //make sure our prototype looks nice + Class.prototype.Class = Class.prototype.constructor = Class; + + + /** + * @attribute fullName + * The full name of the class, including namespace, provided for introspection purposes. + * @codestart + * $.Class.extend("MyOrg.MyClass",{},{}) + * MyOrg.MyClass.shortName //-> 'MyClass' + * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' + * @codeend + */ + + var args = Class.setup.apply(Class, concatArgs([_super_class],arguments)); + + if ( Class.init ) { + Class.init.apply(Class, args || []); + } + + /* @Prototype*/ + return Class; + /** + * @function setup + * If a setup method is provided, it is called when a new + * instances is created. It gets passed the same arguments that + * were given to the Class constructor function ( new Class( arguments ... )). + * + * $.Class("MyClass", + * { + * setup: function( val ) { + * this.val = val; + * } + * }) + * var mc = new MyClass("Check Check") + * mc.val //-> 'Check Check' + * + * Setup is called before [jQuery.Class.prototype.init init]. If setup + * return an array, those arguments will be used for init. + * + * $.Class("jQuery.Controller",{ + * setup : function(htmlElement, rawOptions){ + * return [$(htmlElement), + * $.extend({}, this.Class.defaults, rawOptions )] + * } + * }) + * + *
PRO TIP: + * Setup functions are used to normalize constructor arguments and provide a place for + * setup code that extending classes don't have to remember to call _super to + * run. + *
+ * + * Setup is not defined on $.Class itself, so calling super in inherting classes + * will break. Don't do the following: + * + * $.Class("Thing",{ + * setup : function(){ + * this._super(); // breaks! + * } + * }) + * + * @return {Array|undefined} If an array is return, [jQuery.Class.prototype.init] is + * called with those arguments; otherwise, the original arguments are used. + */ + //break up + /** + * @function init + * If an init method is provided, it gets called when a new instance + * is created. Init gets called after [jQuery.Class.prototype.setup setup], typically with the + * same arguments passed to the Class + * constructor: ( new Class( arguments ... )). + * + * $.Class("MyClass", + * { + * init: function( val ) { + * this.val = val; + * } + * }) + * var mc = new MyClass(1) + * mc.val //-> 1 + * + * [jQuery.Class.prototype.setup Setup] is able to modify the arguments passed to init. Read + * about it there. + * + */ + //Breaks up code + /** + * @attribute Class + * References the static properties of the instance's class. + *

Quick Example

+ * @codestart + * // a class with a static classProperty property + * $.Class.extend("MyClass", {classProperty : true}, {}); + * + * // a new instance of myClass + * var mc1 = new MyClass(); + * + * // + * mc1.Class.classProperty = false; + * + * // creates a new MyClass + * var mc2 = new mc.Class(); + * @codeend + * Getting static properties via the Class property, such as it's + * [jQuery.Class.static.fullName fullName] is very common. + */ + } + + }) + + + + + + clss.prototype. + /** + * @function callback + * Returns a callback function. This does the same thing as and is described better in [jQuery.Class.static.callback]. + * The only difference is this callback works + * on a instance instead of a class. + * @param {String|Array} fname If a string, it represents the function to be called. + * If it is an array, it will call each function in order and pass the return value of the prior function to the + * next function. + * @return {Function} the callback function + */ + callback = clss.callback; + + +})(jQuery) \ No newline at end of file diff --git a/src/static/js/revisioncache.js b/src/static/js/revisioncache.js new file mode 100644 index 000000000..bd77bb5a0 --- /dev/null +++ b/src/static/js/revisioncache.js @@ -0,0 +1,867 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +require('./jquery.class'); +var libchangeset = require("./Changeset"); + +function log () { + console.log.apply(console, arguments); +}; + +$.Class("Changeset", + {//statics + }, + {//instance + init: function (from_revision, to_revision, deltatime, value) { + this.from_revision = from_revision; + this.to_revision = to_revision; + this.deltatime = deltatime; + this.value = value; + }, + getValue: function () { + return this.value; + }, + compose: function (other, pad) { + var newvalue = libchangeset.compose(this.value, other.value, pad.apool); + var newchangeset = new Changeset(this.from_revision, other.to_revision, + this.deltatime + other.deltatime, newvalue); + //TODO: insert new changeset into the graph somehow. + return newchangeset; + }, + /** + * Apply this changeset to the passed pad. + * @param {PadClient} pad - The pad to apply the changeset to. + */ + apply: function (pad) { + // must mutate attribution lines before text lines + libchangeset.mutateAttributionLines(this.value, pad.alines, pad.apool); + + // Looks like this function can take a regular array of strings + libchangeset.mutateTextLines(this.value, /* padcontents */ /*this.lines */ pad.divs); + }, + /** + * 'Follow' the Changeset in a given direction, returning the revision at + * the specified end of the edge. + * @param {bool} direction - If true, go to the 'from' revision, otherwise + * go to the 'to' revision. + * @returns {Revision} + */ + follow: function () { + return this.to_revision; + } + } +); + +/** + * Revision class. Represents a specific revision. Each instance has three + * possible edges in each direction. Each edge is essentially a Changeset. + * We store three edges at different granularities, to make skipping fast. + * e.g. to go from r1 to r251, you start at r1, use the big edge to go to + * r100, use the big edge again to go to r200, use the next 5 medium edges to + * go from r200 to r210, etc. until reaching r250, follow the next small edge + * to get to 251. A total of 8 edges are traversed (a.k.a. applied), + * making this significantly cheaper than applying all 250 changesets from r1 + * to r251. + */ +$.Class("Revision", + {//statics + // we rely on the fact that granularities are always traversed biggest to + // smallest. Changing this will break lots of stuff. + granularities: {huge: 1000, big: 100, medium: 10, small: 1} + }, + {//instance + /** + * Create a new revision for the specified revision number. + * @constructor + * @param {number} revnum - The revision number this object represents. + */ + init: function (revnum) { + this.revnum = revnum; + // next/previous edges, granularityed as big, medium and small + this.next = {}; + this.previous = {}; + for (var granularity in this.granularties) { + this.next[granularity] = null; + this.previous[granularity] = null; + } + }, + /** + * Add a changeset from this revision to the target. + * @param {Revision} target - The target revision. + * @param {object} changeset - The raw changeset data. + * @param {time} timedelta - The difference in time between this revision + * and the target. + * @returns {Changeset} - The new changeset object. + */ + addChangeset: function (target, changeset, timedelta) { + if (this.revnum == target.revnum) + // This should really never happen, but if it does, let's short-circuit. + return; + + var delta_revnum = target.revnum - this.revnum; + // select the right edge set: + var direction_edges = delta_revnum < 0 ? this.previous : this.next; + + // find the correct granularity and add an edge (changeset) for that granularity + for (var granularity in Revision.granularities) { + if (Math.abs(delta_revnum) == Revision.granularities[granularity]) { + //TODO: should we check whether the edge exists? + direction_edges[granularity] = new Changeset(this, target, timedelta, changeset); + return direction_edges[granularity]; + } + } + // our delta_revnum isn't one of the granularities. Something is wrong + //TODO: handle this case? + return null; + }, + lt: function (other, is_reverse) { + if (is_reverse) + return this.gt(other); + return this.revnum < other.revnum; + }, + gt: function (other, is_reverse) { + if (is_reverse) + return this.lt(other); + return this.revnum > other.revnum; + } + } +); + +$.Class("RevisionCache", + { + VERBOSE: true, + }, + {//instance + /** + * Create a new RevisionCache. + * @constructor + * @param {TimesliderClient} connection - The connection to be used for loading changesets. + * @param {number} head_revnum - The current head revision number. TODO: we can probably do away with this now. + */ + init: function (connection, head_revnum) { + this.log = RevisionCache.VERBOSE ? log : function () {}; + this.connection = connection; + this.loader = new ChangesetLoader(connection); + this.revisions = {}; + this.head_revision = this.getRevision(head_revnum || 0); + this.loader.start(); + }, + /** + * Get the head revision. + * @returns {Revision} - the head revision. + */ + getHeadRevision: function () { + return this.head_revision; + }, + /** + * Get a Revision instance for the specified revision number. + * If we don't yet have a Revision instance for this revision, create a + * new one. Also make sure that the head_revision attribute always refers + * to the instance for the pad's head revision. + * @param {number} revnum - The revision number for which we want a + * Revision object. + * @returns {Revision} + */ + getRevision: function (revnum) { + if (revnum in this.revisions) + return this.revisions[revnum]; + var revision = new Revision(revnum); + this.revisions[revnum] = revision; + if (this.head_revision && revnum > this.head_revision.revnum) { + this.head_revision = revision; + } + return revision; + }, + /** + * Add a new revision at the head. + * @param {number} revnum - the revision number of the new revision. + * @param {string} forward - the forward changeset to get here from previous head. + * @param {string} reverse - the reverse changeset. + * @param {string} timedelta - the time difference. + */ + appendHeadRevision: function (revnum, forward, reverse, timedelta) { + this.addChangesetPair(this.head_revision.revnum, revnum, forward, reverse, timedelta); + this.head_revision = this.getRevision(revnum); + //TODO: render it if we are currently at the head_revision? + }, + /** + * Links two revisions, specified by from and to with the changeset data + * in value and reverseValue respectively. + * @param {number} from - The revision number from which the forward + * changeset originates. + * @param {number} to - The revision number to which the forward changeset + * bring us. + * @param {changeset} forwardValue - The forward changeset data. + * @param {changeset} reverseValue - The reverse changeset data. + * @param {time} timedelta - The difference in time between the from and + * to revisions. + */ + addChangesetPair: function (from, to, value, reverseValue, timedelta) { + var from_rev = this.getRevision(from); + var to_rev = this.getRevision(to); + from_rev.addChangeset(to_rev, value, timedelta); + to_rev.addChangeset(from_rev, reverseValue, -timedelta); + }, + /** + * Find a (minimal) path from a given revision to another revision. If a + * complete path cannot be found, return a path which comes as close as + * possible to the to revision. + * @param {Revision} from - The revision from which to start. + * @param {Revision} to - The revision which the path should try to reach. + * @returns {object} - A list of Changesets which describe a (partial) path + * from 'from' to 'to', and the last revision reached. + */ + findPath: function (from, to) { + /* + *TODO: currently we only ever move in the direction of sign(to-from). + *It might be worth implementing 'jitter' movements, so that if you, + *for example, you are trying to go from 0 to 99, and you have the + *following edges: + * 0 -> 100 + * 100 -> 99 + *The algorithm would be smart enough to provide you with that as a path + */ + var path = []; + var found_discontinuity = false; + var current = from; + var direction = (to.revnum - from.revnum) < 0; + var granularity = 0; + + //log("[findpath] from: %d, to: %d", from.revnum, to.revnum); + while (current.lt(to, direction) && !found_discontinuity) { + //log("\t[findPath] while current: ", current.revnum); + var delta_revnum = to.revnum - current.revnum; + var direction_edges = direction ? current.previous : current.next; + for (granularity in Revision.granularities) { + if (Math.abs(delta_revnum) >= Revision.granularities[granularity]) { + //log("\t\t[findPath] for delta: %d, granularity: %d", delta_revnum, Revision.granularities[granularity]); + /* + * the delta is larger than the granularity, let's use the granularity + *TODO: what happens if we DON'T have the edge? + * in theory we need to fetch it (and this is certainly the case for playback + * at granularity = 1). However, when skipping, we might try to find the NEXT + * Revision (which is not linked by the graph to current) and request revisions + * from current to that Revision (at the largest possible granularity) + */ + var edge = direction_edges[granularity]; + //log("\t\t[findpath] edge:", edge); + if (edge) { + // add this edge to our path + path.push(edge); + // follow the edge to the next Revision node + current = edge.follow(); + // no need to look for smaller granularities + break; + } else { + // we don't have an edge. Normally we can just continue to the + // next granularity level. BUT, if we are at the lowest + // granularity and don't have an edge, we've reached a DISCONTINUITY + // and can no longer continue. + if (Revision.granularities[granularity] == 1) + found_discontinuity = true; + } + } + } + } + + //log("[findpath] ------------------"); + // return either a full path, or a path ending as close as we can get to + // the target revision. + return {path: path, end_revision: current, granularity: granularity}; + }, + //TODO: horrible name! + transition: function (from_revnum, to_revnum, applyChangeset_callback) { + var path = []; + var current_revision = this.getRevision(from_revnum); + var target_revision = this.getRevision(to_revnum); + this.log("[revisioncache > transition] from %d -> %d", from_revnum, to_revnum); + // For debugging: + function print_path(path) { + var res = "["; + for (var p in path) { + res += path[p].from_revision.revnum + "->" + path[p].to_revision.revnum + ", "; + } + res += "]"; + return res; + } + + // lets just keep a 'final' path, which is a list of changesets. + // The transition should complete when the first element's from revnum is + // from_revnum, and the last element's to revnum is to_revnum. + // (Assuming that there are no discontinuities. + var thePath = []; + + function is_complete() { + console.log(thePath); + if (thePath.length && thePath[0].from_revision.revnum == from_revnum + && thePath.slice(-1)[0].to_revision.revnum == to_revnum) { + return true; + } + return false; + } + + var thePath = []; + + var _this = this; + function partialTransition (cur_start, cur_end) { + _this.log("[partialTransition] from: %d, to: %d, cur_start: %d, cur_end: %d", from_revnum, to_revnum, cur_start, cur_end); + var cur_start_rev = _this.getRevision(cur_start); + var res = _this.findPath(cur_start_rev, _this.getRevision(cur_end)); + log("find: ", print_path(res.path)); + + if (!res.path.length) { + // we got nutting, request changesets for the full path. + _this.requestChangesets(cur_start_rev, target_revision, partialTransition); + return; + } + + //TODO: we should probably check for discontinuities which indicate a real WTF condition. + + // just prepend the found path to thePath; we assume that we get further from + // the original target as we go. this is because we set the new target to be + // the head of our + thePath = res.path.concat(thePath); + + log("THE PATH: ", print_path(thePath)); + + //FIXME: this should test for 'completeness'! + if (is_complete()) { + //log("found: ", print_path(res.path)); + if(applyChangeset_callback) { + applyChangeset_callback(thePath); + } + return; + } + // next iteration, we want to find a path that reaches the beginning of our + // current path, as we assume that the 'tail' of the path is always correct. + target_revision = thePath[0].from_revision; + } + + partialTransition(from_revnum, to_revnum); + + }, + /** + * Request changesets which will allow transitioning from 'from' to 'to' + * from the server. + * @param {Revision} from - The start revision. + * @param {Revision} to - The end revision. + * @param {function} changesetsProcessed_callback - A callback triggered + * when the requested changesets have been + * received and processed (added to the graph) + */ + requestChangesets: function (from, to, changesetsProcessed_callback) { + this.log("[revisioncache] requestChangesets: %d -> %d", from.revnum, to.revnum); + var delta = to.revnum - from.revnum; + var sign = delta > 0 ? 1 : -1; + var start = delta > 0 ? from.revnum : to.revnum; + var end = delta > 0 ? to.revnum : from.revnum; + var adelta = Math.abs(delta); + + var _this = this; + function process_received_changesets (data) { + _this.log("[revisioncache] received changesets {from: %d, to: %d} @ granularity: %d", data.start, data.actualEndNum, data.granularity); + var start = data.start; + for (var i = 0; i < data.timeDeltas.length; i++, start += data.granularity) { + _this.addChangesetPair(start, start + data.granularity, data.forwardsChangesets[i], data.backwardsChangesets[i], data.timeDeltas[i]); + } + if (changesetsProcessed_callback) { + if (sign == 1) + changesetsProcessed_callback(data.start, data.start + (data.granularity*data.timeDeltas.length)); + else + changesetsProcessed_callback(data.start + (data.granularity*data.timeDeltas.length), data.start); + } + } + + var rounddown = function (a, b) { + return Math.floor(a / b) * b; + }; + var roundup = function (a, b) { + return (Math.floor(a / b)+1) * b; + }; + + this.log("[requestChangesets] start: %d, end: %d, delta: %d, adelta: %d", start, end, delta, adelta); + for (var g in Revision.granularities) { + var granularity = Revision.granularities[g]; + var remainder = Math.floor(adelta / granularity); + this.log("\t[requestChangesets] start: %d, granularity: %d, adelta: %d, //: %d", start, granularity, adelta, remainder); + this.log("\ttest: start: %d, end: %d", rounddown(start,granularity), roundup(end,granularity)); + this.log("\trounddown delta: %d, start: %d", rounddown(adelta, granularity), rounddown(start, granularity)); + if (remainder) { + //this.loader.enqueue(start, granularity, process_received_changesets); + this.log("\t[requestChangesets] REQUEST start: %d, end: %d, granularity: %d", rounddown(start, granularity), roundup(adelta, granularity), granularity); + this.loader.enqueue(rounddown(start, granularity), granularity, process_received_changesets); + // for the next granularity, we assume that we have now successfully navigated + // as far as required for this granularity. We should also make sure that only + // the significant part of the adelta is used in the next granularity. + start = rounddown(start, granularity) + rounddown(adelta, granularity); + adelta = adelta - rounddown(adelta, granularity); + this.log("\t\tnew start: %d, delta: %d", start, adelta); + } + } + }, + } +); + +$.Class("Thread", + {//statics + VERBOSE: true + }, + {//instance + init: function (interval) { + this._is_running = false; + this._is_stopping = false; + this._interval_id = null; + this._interval = interval ? interval : 1000; + this.log = Thread.VERBOSE ? log : function () {}; + }, + _run: function () { + this.log("[thread] tick"); + }, + // start the run loop + start: function () { + var _this = this; + this.log("[thread] starting"); + var wrapper = function () { + if (_this._is_running && _this._is_stopping) { + this.log("[thread] shutting down"); + clearInterval(_this._interval_id); + _this._is_running = false; + return; + } + _this._run.apply(_this); + }; + this._is_running = true; + this._is_stopping = false; + this._interval_id = setInterval(wrapper, this._interval); + }, + // stop the run loop + stop: function () { + this._is_stopping = true; + this.log("[thread] request stop"); + // TODO: consider finding a way to make this block + // or alternatively, having a callback which is called + // when the thread stops + } + } +); + +$.Class("ChangesetRequest", + {//statics + }, + {//instance + init: function (start, granularity, callback) { + this.log = ChangesetLoader.VERBOSE ? log : function () {}; + this.start = start; + this.granularity = granularity; + this.request_id = (this.granularity << 16) + this.start; + this.fulfill_callback = callback; + }, + getRequestID: function () { + return this.request_id; + }, + fulfill: function (data) { + var id = this.getRequestID(); + this.log("[changesetrequest] Fulfilling request: %d, start: %d, granularity: %d", id, id & 0xffff, id >> 16); + if (this.fulfill_callback) + this.fulfill_callback(data); + } + + } +); + +Thread("ChangesetLoader", + {//statics + VERBOSE: false, + }, + {//instance + /** + * Create a new ChangesetLoader. + * @constructor + * @param {TimesliderClient} connection - a TimesliderClient object to be used + * for communication with the server. + */ + init: function (connection) { + this._super(200); + this.connection = connection; + this.queues = {}; + for (var granularity in Revision.granularities) { + this.queues[granularity] = []; + } + this.pending = {}; + var _this = this; + this.connection.on("CHANGESET_REQ", function () { + _this.on_response.apply(_this, arguments); + }); + this.log = ChangesetLoader.VERBOSE ? log : function () {}; + }, + /** + * Enqueue a request for changesets. The changesets will be retrieved + * asynchronously. + * @param {number} start - The revision from which to start. + * + * @param {number} granularity - The granularity of the changesets. If this + * is 1, the response will include changesets which + * can be applied to go from revision r to r+1. + * If 10 is specified, the resulting changesets will + * be 'condensed', so that each changeset will go from + * r to r+10. + * If any other number is specified, that granularity will + * apply. + * + * TODO: there is currently no 'END' revision implemented + * in the server. The 'END' calculated at the server is: + * start + (100 * granularity) + * We should probably fix this so that you can specify + * exact ranges. Right now, the minimum number of + * changesets/revisions you can retrieve is 100, which + * feels broken. + * @param {function} callback - A callback which will be triggered when the request has + * been fulfilled. The context of the callback will be the + * ChangesetRequest object, so you can check what you actually + * asked for. + */ + enqueue: function (start, granularity, callback) { + this.log("[changeset_loader] enqueue: %d, %d", start, granularity); + //TODO: check cache to see if we really need to fetch this + // maybe even do splices if we just need a smaller range + // in the middle + var queue = null; + for (var g in Revision.granularities) { + if (granularity == Revision.granularities[g]) { + queue = this.queues[g]; + break; + } + } + + var request = new ChangesetRequest(start, granularity, callback); + if (! (request.getRequestID() in this.pending)) { + queue.push(request); + this.log("[changesetloader] enqueued request:", request.getRequestID()) + } + + }, + _run: function () { + var _this = this; + function addToPending () { + _this.pending[request.getRequestID()] = request; + } + //TODO: pop an item from the queue and perform a request. + for (var q in this.queues) { + var queue = this.queues[q]; + if (queue.length > 0) { + // TODO: pop and handle + var request = queue.pop(); + if (request.getRequestID() in this.pending) { + //this request is already pending! + var id = request.getRequestID(); + this.log("ALREADY PENDING REQUEST: %d, start: %d, granularity: %d", id, id & 0xffff, id >> 16); + continue; + } + //TODO: test AGAIN to make sure that it hasn't been retrieved and cached by + //a previous request. This should handle the case when two requests for the + //same changesets are enqueued (which would be fine, as at enqueue time, we + //only check the cache of AVAILABLE changesets, not the pending requests), + //the first one is fulfilled, and then we pop the second one, and don't + //need to perform a server request. Note: it might be worth changing enqueue + //to check the pending requests queue to avoid this situation entirely. + + this.connection.sendMessage("CHANGESET_REQ", { + start: request.start, + granularity: request.granularity, + requestID: request.getRequestID(), + }, addToPending); + } + } + //TODO: this stop is just for debugging!!!! + //FIXME: remove when done testing + //this.stop(); + }, + on_response: function (data) { + this.log("[changesetloader] on_response: ", data); + if (!(data.requestID in this.pending)) { + this.log("[changesetloader] WTF? changeset not pending: ", data.requestID); + return; + } + + // pop it from the pending list: + var request = this.pending[data.requestID]; + delete this.pending[data.requestID]; + this.log("[changesetloader] still pending: ", this.pending); + //fulfill the request + request.fulfill(data); + }, + } +); + +var libcssmanager = require("./cssmanager"); +var linestylefilter = require("./linestylefilter").linestylefilter; +var libcolorutils = require('./colorutils').colorutils; +$.Class("Author", + {//static + }, + {//instance + init: function (id, data, palette) { + this.id = id; + this.name = data.name; + this.is_anonymous = !this.name; + // if the colorId is an integer, it's an index into the color palette, + // otherwise we assume it is a valid css color string + this.background_color = typeof data.colorId == "number" ? palette[data.colorId] : data.colorId; + // foreground color should be black unless the luminosity of the + // background color is lower than 0.5. This effectively makes sure + // that the text is readable. + this.color = (libcolorutils.luminosity(libcolorutils.css2triple(this.background_color)) < 0.5 ? "#ffffff" : "#000000"); + // generate a css class name for this author. + this.cssclass = linestylefilter.getAuthorClassName(this.id); + }, + /** + * Create and add a rule to the stylesheet setting the foreground and + * background colors for this authors cssclass. This class can then be + * applied to any span authored by this author, and the colors will just work. + * @param {object} cssmanager - A cssmanager wrapper for the stylesheet to + * which the rules should be added. + */ + addStyleRule: function (cssmanager) { + // retrieve a style selector for '.' class, which is applied + // to blobs which were authored by that . + var selector = cssmanager.selectorStyle("." + this.cssclass); + // apply the colors + selector.backgroundColor = this.background_color; + selector.color = this.color; + }, + /** + * Retrieve the name of this user. + */ + getName: function () { + return this.is_anonymous ? "anonymous" : this.name; + }, + /** + * Retrieve the cssclass for this user. + */ + getCSSClass: function () { + return this.cssclass; + }, + } +); + +var AttribPool = require("./AttributePool"); +var domline = require("./domline").domline; +$.Class("PadClient", + {//static + USE_COMPOSE: false, + VERBOSE: true, + }, + {//instance + /** + * Create a PadClient. + * @constructor + * @param {RevisionCache} revisionCache - A RevisionCache object to use. + * @param {dict} options - All the necessary options. TODO: document this. + */ + init: function (revisionCache, options) { + this.revisionCache = revisionCache; + this.revision = this.revisionCache.getRevision(options.revnum); + this.timestamp = options.timestamp; + this.alines = libchangeset.splitAttributionLines(options.atext.attributes, options.atext.text); + this.apool = (new AttribPool()).fromJsonable(options.atext.apool); + this.lines = libchangeset.splitTextLines(options.atext.text); + this.authors = {}; + this.dynamicCSS = libcssmanager.makeCSSManager('dynamicsyntax'); + this.palette = options.palette; + this.log = PadClient.VERBOSE ? log : function () {}; + + this.updateAuthors(options.author_info); + + //TODO: this is a kludge! we should receive the padcontent as an + //injected dependency + this.divs = []; + this.padcontent = $("#padcontent"); + for (var i in this.lines) { + var div = this._getDivForLine(this.lines[i], this.alines[i]); + this.divs.push(div); + this.padcontent.append(div); + } + + //TODO: monkey patch divs.splice to use our custom splice function + this.divs.original_splice = this.divs.splice; + var _this = this; + this.divs.splice = function () { + return _this._spliceDivs.apply(_this, arguments); + }; + // we need to provide a get, as we want to give + // libchangeset the text of a div, not the div itself + this.divs.get = function (index) { + return this[index].data('text'); + }; + }, + goToRevision: function (revnum, atRevision_callback) { + this.log("[padclient > goToRevision] revnum: %d", revnum); + var _this = this; + if (this.revision.revnum == revnum) { + if (atRevision_callback) + atRevision_callback.call(this, this.revision, this.timestamp); + return; + } + + this.revisionCache.transition(this.revision.revnum, revnum, function (path) { + _this.log("[padclient > applyChangeset_callback] path:", path); + var time = _this.timestamp; + var p, changeset = null; //pre-declare, because they're used in both blocks. + if (PadClient.USE_COMPOSE) { + var composed = path[0]; + var _path = path.slice(1); + for (p in _path) { + changeset = _path[p]; + composed = composed.compose(changeset, _this); + } + composed.apply(_this); + time += composed.deltatime * 1000; + } else { // Don't compose, just apply + for (p in path) { + changeset = path[p]; + time += changeset.deltatime * 1000; + //try { + _this.log("[transition] %d -> %d, changeset: %s", changeset.from_revision.revnum, changeset.to_revision.revnum, changeset.value); + changeset.apply(_this); + /*} catch (err) { + log("Error applying changeset: "); + log("\t", changeset.value); + log("\t %d -> %d ", changeset.from_revision.revnum, changeset.to_revision.revnum); + log(err); + log("--------------"); + }*/ + } + } + + // set revision and timestamp + _this.revision = path.slice(-1)[0].to_revision; + _this.timestamp = time; + // fire the callback + if (atRevision_callback) { + _this.log("[padclient] about to call atRevision_callback", _this.revision, _this.timestamp); + atRevision_callback.call(_this, _this.revision, _this.timestamp); + } + }); + }, + /** + * Update the authors of this pad. + * @param {object} author_info - The author info object sent by the server + */ + updateAuthors: function (author_info) { + var authors = author_info; + this.log("[updateAuthors]: ", authors); + for (var authorid in authors) { + if (authorid in this.authors) { + // just dispose of existing ones instead of trying to update existing + // objects. + delete this.authors[authorid]; + } + var author = new Author(authorid, authors[authorid], this.palette); + this.authors[authorid] = author; + author.addStyleRule(this.dynamicCSS); + } + }, + /** + * Merge a foreign (forward) changeset into our data. This involves rebuilding + * the forward changeset in our apool and building a reverse changeset. + * This is used to move new upstream changesets/revisions into our apool context. + * @param {string} changeset - The foreign changeset to merge + * @param {object} apool - The apool for that changeset + * @returns {object} - A values object with forward and reverse changesets. + */ + mergeForeignChangeset: function (changeset, apool) { + var values = {}; + values.forward = libchangeset.moveOpsToNewPool( + changeset, + (new AttribPool()).fromJsonable(apool), + this.apool + ); + var reverseValue = libchangeset.inverse( + changeset, + this.divs, + this.alines, + this.apool + ); + values.reverse = libchangeset.moveOpsToNewPool( + reverseValue, + (new AttribPool()).fromJsonable(apool), + this.apool + ); + return values; + }, + /** + * Get a div jquery element for a given attributed text line. + * @param {string} text - The text content of the line. + * @param {string} atext - The attributes string. + * @return {jquery object} - The div element ready for insertion into the DOM. + */ + _getDivForLine: function (text, atext) { + //this.log("[_getDivsForLine] %s; %s", text, atext); + var dominfo = domline.createDomLine(text != '\n', true); + + // Here begins the magic invocation: + linestylefilter.populateDomLine(text, atext, this.apool, dominfo); + dominfo.prepareForAdd(); + + var div = $("
" + + dominfo.node.innerHTML + "
"); + return div; + }, + /** + * we need a customized splice function for our divs array, because we + * need to be able to: + * - remove elements from the DOM when they are spliced out + * - create a new div for line elements and add them to the array + * instead of the raw line + * - add the new divs to the DOM + * this function is fully compliant with the Array.prototype.splice + * spec, as we're monkey-patching it on to the divs array. + * @param {number} index - Index at which to start changing the array. + * @param {number} howMany - An integer indicating the number of old array elements to remove. + * @param {array} elements - The elements to add to the array. In our case, these are lines. + */ + _spliceDivs: function (index, howMany, elements) { + elements = Array.prototype.slice.call(arguments, 2); + // remove howMany divs starting from index. We need to remove them from + // the DOM. + for (var i = index; i < index + howMany && i < this.divs.length; i++) + this.divs[i].remove(); + + // generate divs for the new elements: + var newdivs = []; + for (i in elements) + newdivs.push(this._getDivForLine(elements[i], this.alines[index + i])); + + // if we are splicing at the beginning of the array, we need to prepend + // to the padcontent DOM element + if (!this.divs[index - 1]) + this.padcontent.prepend(newdivs); + // otherwise just add the new divs after the index-th div + else + this.divs[index - 1].after(newdivs); + // super primitive scrollIntoView + if (newdivs.length) { + newdivs[0][0].scrollIntoView(false); + } + + // perform the splice on our array itself + // TODO: monkey patching divs.splice, so use divs.original_splice or something + args = [index, howMany].concat(newdivs); + return this.divs.original_splice.apply(this.divs, args); + }, + } +); diff --git a/src/static/js/revisionslider.js b/src/static/js/revisionslider.js new file mode 100644 index 000000000..40ecbfbae --- /dev/null +++ b/src/static/js/revisionslider.js @@ -0,0 +1,259 @@ +var sliderui = require('./sliderui'); +require('./jquery.class'); + +$.Class("RevisionSlider", + {//statics + /** + * The number of milliseconds to wait between revisions when playing back. + */ + PLAYBACK_DELAY: 400, + }, + {//instance + /** + * Create a new RevisionSlider, given a connection to the server and a root + * element. + * @constructor + * @param {TimesliderClient} connection - The connection to the server. + * @param {jquery object} root_element - The element to build the slider on. + */ + init: function (connection, root_element) { + this.connection = connection; + this.revision_number = this.connection.getCurrentRevision().revnum; + this.timestamp = 0; + this.is_playing = false; + // if there was a revision specified in the 'location.hash', jump to it. + if (window.location.hash.length > 1) { + var rev = Number(window.location.hash.substr(1)); + if(!isNaN(rev)) + this.revision_number = rev; + } + + console.log("New RevisionSlider, current_revision = %d", this.revision_number); + // parse the various elements we need: + this.elements = {}; + this.loadElements(root_element); + var _this = this; + this.slider = new SliderUI(this.elements.slider_bar, + options = { + value: this.revision_number, + max: this.connection.getHeadRevision(), + change: function () { _this.onChange.apply(_this, arguments); }, + slide: function () { _this.onSlide.apply(_this, arguments); }, + }); + this.loadSavedRevisionHandles(); + this.slider.render(); + + this._mouseInit(); + + this.goToRevision(this.revision_number); + }, + onChange: function (value) { + console.log("in change handler:", value); + if (!this.is_playing) + this.goToRevision(value); + }, + onSlide: function (value) { + console.log("in slide handler:", value); + if (!this.is_playing) + this.goToRevision(value); + }, + /** + * Populate the elements dictionary with the various elements we might want + * to use. + * @param {jquery object} root_element - The root element of this slider. + */ + loadElements: function (root_element) { + this.elements.root = root_element; + this.elements.slider_bar = root_element.find("#ui-slider-bar"); + this.elements.slider = root_element.find("#timeslider-slider"); + this.elements.button_left = root_element.find("#leftstep"); + this.elements.button_right = root_element.find("#rightstep"); + this.elements.button_play = root_element.find("#playpause_button"); + this.elements.timestamp = root_element.find("#timer"); + this.elements.revision_label = root_element.find("#revision_label"); + this.elements.revision_date = root_element.find("#revision_date"); + this.elements.authors = root_element.find("#authorsList"); + }, + /** + * Create 'star' handles on the slider for each saved revision. + */ + loadSavedRevisionHandles: function () { + for (var r in this.connection.savedRevisions) { + var rev = this.connection.savedRevisions[r]; + this.slider.createHandle(rev.revNum, "star"); + } + }, + /** + * Toggle (and execute) the playback mode. + */ + playpause: function () { + if (this.is_playing) { + this.is_playing = false; + return; + } + + var revnum = this.revision_number; + if (revnum == this.connection.getHeadRevision()) + revnum = 0; + + var _this = this; + var keepPlaying = function (current_revnum) { + if (current_revnum == _this.connection.getHeadRevision()) + _this.is_playing = false; + if (!_this.is_playing) { + _this.render(); + return; + } + setTimeout(function () { + _this.goToRevision(current_revnum + 1, keepPlaying); + }, RevisionSlider.PLAYBACK_DELAY); + }; + + this.is_playing = true; + this.goToRevision(revnum, keepPlaying); + }, + /** + * Update the UI elements to the current revision + */ + render: function () { + this.elements.revision_label.html(html10n.get("timeslider.version", { "version": this.revision_number })); + this.slider.setMax(this.connection.getHeadRevision()); + this.slider.setValue(this.revision_number); + window.location.hash = "#" + this.revision_number; + this.setTimestamp(this.timestamp); + if (this.is_playing) + this.elements.button_play.find("div").addClass("pause"); + else + this.elements.button_play.find("div").removeClass("pause"); + if (this.revision_number == this.connection.getHeadRevision()) + this.elements.button_right.addClass("disabled"); + else + this.elements.button_right.removeClass("disabled"); + if (this.revision_number === 0) + this.elements.button_left.addClass("disabled"); + else + this.elements.button_left.removeClass("disabled"); + + this.renderAuthors(); + }, + + + /** + * Render the authors line. + */ + renderAuthors: function () { + //TODO: consider alphabetizing the authors? + var authors = this.connection.getAuthors(); + this.elements.authors.empty(); + if ($.isEmptyObject(authors)) { + this.elements.authors.append("No authors"); + return; + } + for (var authorid in authors) { + var author = authors[authorid]; + var span = $("") + .text(author.getName()) + .addClass('author') + .addClass(author.getCSSClass()); + this.elements.authors.append(span); + } + }, + /** + * Go to a specific revision number. This will perform the actual + * transition to the revision and set the UI elements as required + * once the transition is done. The callback can be used to perform + * actions after the transition is complete and the UI has been + * updated. + * @param {number} revnum - The revision to transition to. + * @param {callback} atRevision_callback - The callback. + */ + goToRevision: function (revnum, atRevision_callback) { + if (revnum > this.connection.getHeadRevision()) + revnum = this.connection.latest_revision; + if (revnum < 0) + revnum = 0; + + var _this = this; + this.connection.goToRevision(revnum, function (revision, timestamp) { + console.log("[revisionslider > goToRevision > callback]", revision, timestamp); + //update UI elements: + _this.revision_number = revision.revnum; + _this.timestamp = timestamp; + _this.render.call(_this); + //TODO: set the enabled/disabled for button-left and button-right + if (atRevision_callback) { + atRevision_callback(revnum); + } + }); + }, + /** + * Set the timestamp and revision date displays + * @param {number} timestamp - The timestamp of the current revision. + */ + setTimestamp: function (timestamp) { + var zeropad = function (str, length) { + str = str + ""; + while (str.length < length) + str = '0' + str; + return str; + }; + var months = [ + html10n.get("timeslider.month.january"), + html10n.get("timeslider.month.february"), + html10n.get("timeslider.month.march"), + html10n.get("timeslider.month.april"), + html10n.get("timeslider.month.may"), + html10n.get("timeslider.month.june"), + html10n.get("timeslider.month.july"), + html10n.get("timeslider.month.august"), + html10n.get("timeslider.month.september"), + html10n.get("timeslider.month.october"), + html10n.get("timeslider.month.november"), + html10n.get("timeslider.month.december") + ]; + var date = new Date(timestamp); + var timestamp_format = html10n.get("timeslider.dateformat", + { + "day": zeropad(date.getDate(), 2), + "month": zeropad(date.getMonth() + 1, 2), + "year": date.getFullYear(), + "hours": zeropad(date.getHours(), 2), + "minutes": zeropad(date.getMinutes(), 2), + "seconds": zeropad(date.getSeconds(), 2), + }); + this.elements.timestamp.html(timestamp_format); + + var revisionDate = html10n.get("timeslider.saved", { + "day": date.getDate(), + "month": months[date.getMonth()], + "year": date.getFullYear() + }); + + this.elements.revision_date.html(revisionDate); + }, + /** + * Initialize mouse events and handlers + */ + _mouseInit: function () { + var _this = this; + this.elements.button_left.on("click", function (event) { + if ($(this).hasClass("disabled")) + return; + _this.is_playing = false; + _this.goToRevision(_this.revision_number - 1); + }); + + this.elements.button_right.on("click", function (event) { + if ($(this).hasClass("disabled")) + return; + _this.is_playing = false; + _this.goToRevision(_this.revision_number + 1); + }); + + this.elements.button_play.on("click", function (event) { + _this.playpause(); + }); + } + + } +); diff --git a/src/static/js/sliderui.js b/src/static/js/sliderui.js new file mode 100644 index 000000000..ec98fbed2 --- /dev/null +++ b/src/static/js/sliderui.js @@ -0,0 +1,168 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +require("./jquery.class"); + +/** + * This is an implementation of a (very) simple Slider UI. + * + * Create a slider by doing: + * slider = new SliderUI(sliderbar_element, options); + * sliderbar_element should be a jquery wraper of the element which will serve as + * the bar on which the handles will be hung. + * optionalions is an optional dictionary which currently supports the following: + * value = the initial value for the default handle (default: 0) + * max = the maximum value for the slider (default: 100) + */ + + +/** + * A class for anything which can be hung off of the slider bar. + * I.e. this is for the handle, or saved-revisions (stars). + */ +$.Class("SliderHandleUI", + {//statics + }, + {//instance + /** + * Construct the SliderHandle. + * @param {SliderUI} slider The slider from which this handle will be hung. + * @param {Number} position The initial position for this handle. + */ + init: function (slider, value, type) { + //console.log("New SliderHandle(%d, %s)", value, type); + this.slider = slider; + this.value = value; + //create the element: + this.element = $("
"); + if (type === "") + type = "handle"; + this.element.addClass("ui-slider-handle-" + type); + this._mouseInit(); + }, + _mouseInit: function () { + this.element.on("mousedown.sliderhandle", null, this, function(event) { + //console.log("sliderhandleui - mousedown"); + }); + }, + } +); + +//TODO: +// - window resizing is currently broken! +// - keyboard events +$.Class("SliderUI", + {//statics + defaults: { + min: 0, + max: 100, + value: 0, + } + }, + {//instance + init: function (element, options) { + this.options = $.extend({}, this.defaults, options); + this.element = element; + this.current_value = this.options.value; + this.handles = []; + this.createHandle(this.current_value, 'handle'); + this._mouseInit(); + + // handle window resize + var _this = this; + $(window).resize(function() { + _this.render(); + }); + }, + _getStep: function () { + return (this.element.width()) / (this.options.max * 1.0); + }, + render: function () { + for(var h in this.handles) { + handle = this.handles[h]; + handle.element.css('left', (handle.value * this._getStep()) ); + } + }, + /** + * Update the value in the UI. This should never be called internally. + * It should only be called by an event handler after a transition + * has completed. + * @param {number} value - The value to set. + */ + setValue: function (value) { + if (value < 0) + value = 0; + if (value > this.options.max) + value = this.options.max; + this.handles[0].value = value; + this.current_value = value; + this.render(); + }, + setMax: function (max) { + this.options.max = max; + this.render(); + }, + createHandle: function (value, type) { + //console.log("createHandle(%d, %s)", value, type); + var handle = new SliderHandleUI(this, value, type); + this.handles.push(handle); + this.element.append(handle.element); + return handle; + }, + _trigger: function (eventname, value) { + //console.log("triggering event: ", eventname); + if (eventname in this.options) { + return this.options[eventname](value); + } + }, + _mouseInit: function () { + // handle all mouse events for the slider and handles right here + var _this = this; + this.element.on("mousedown.slider", function (event) { + if (event.target == _this.element[0] || $(event.target).hasClass("ui-slider-handle")) { + // the click is on the slider bar itself. + var start_value = Math.floor((event.clientX-_this.element.offset().left) / _this._getStep()); + //console.log("sliderbar mousedown, value:", start_value); + if (_this.current_value != start_value) { + //_this.setValue(start_value); + } + var prev_value = start_value; + + $(document).on("mousemove.slider", function (event) { + var current_value = Math.floor((event.clientX-_this.element.offset().left) / _this._getStep()); + //console.log("sliderbar mousemove, value:", current_value); + // don't change the value if it hasn't actually changed! + if (prev_value != current_value) { + _this._trigger("slide", current_value); + prev_value = current_value; + } + }); + + $(document).on("mouseup.slider", function (event) { + // make sure to get rid of the handlers on document, + // we don't need them after this 'slide' session is done. + $(document).off("mouseup.slider mousemove.slider"); + var end_value = Math.floor((event.clientX-_this.element.offset().left) / _this._getStep()); + //console.log("sliderbar mouseup, value:", end_value); + // always change the value at mouseup + _this._trigger("change", end_value); + + }); + } else { + console.log("We shouldn't be here!"); + console.log(event.target); + } + }); + }, + } +); diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index fd22c69a3..481a05c95 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -1,9 +1,3 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - /** * Copyright 2009 Google Inc. * @@ -23,7 +17,8 @@ // These jQuery things should create local references, but for now `require()` // assigns to the global `$` and augments it with plugins. require('./jquery'); -JSON = require('./json2'); +require('./jquery.class'); +//JSON = require('./json2'); var createCookie = require('./pad_utils').createCookie; var readCookie = require('./pad_utils').readCookie; @@ -32,7 +27,290 @@ var hooks = require('./pluginfw/hooks'); var token, padId, export_links; -function init() { +$.Class("SocketClient", + { //statics + VERBOSE: false, + }, + { //instance + init: function (baseurl) { + this.baseurl = baseurl; + this.log = SocketClient.VERBOSE ? console.log : function () {}; + // connect to the server + this.log("[socket_client] connecting to:", this.baseurl); + this.socket = io.connect(this.baseurl, {resource: "socket.io"}); + // setup the socket callbacks: + _this = this; + + this.socket.on("connect", function() { + _this.onConnect.apply(_this, arguments); + }); + this.socket.on("disconnect", function() { + _this.onDisconnect.apply(_this, arguments); + }); + this.socket.on("message", function(message) { + _this.onMessage.apply(_this, arguments); + }); + }, + + onConnect: function() { + this.log("[socket_client] > onConnect"); + }, + + onDisconnect: function() { + this.log("[socket_client] > onDisconnect"); + }, + + /** + * Triggered when a new message arrives from the server. + * @param {object} message - The message. + */ + onMessage: function(message) { + this.log("[socket_client] > onMessage: ", message); + }, + + /** + * Sends a message to the server. + * @param {object} message - The message to send + * @param {function} callback - A callback function which will be called after + * the message has been sent to the socket. + */ + sendMessage: function(message, callback) { + this.log("[socket_client] > sendMessage: ", message); + this.socket.json.send(message); + if (callback) + callback(); + }, + } +); + +SocketClient("AuthenticatedSocketClient", + { //statics + VERBOSE: false, + }, + { //instance + init: function (baseurl, padID) { + this.log = AuthenticatedSocketClient.VERBOSE ? console.log : function () {}; + + //make sure we have a token + this.token = readCookie("token"); + if(this.token === null) + { + this.token = "t." + randomString(); + createCookie("token", this.token, 60); + } + this.padID = padID; + this.sessionID = decodeURIComponent(readCookie("sessionID")); + this.password = readCookie("password"); + this.handlers = {}; + + this._super(baseurl); + }, + + /** + * Sends a pad message to the server, including all the neccessary + * session info and tokens. + * @param {string} type - The message type to send. See the server code for + * valid message types. + * @param {object} data - The data payload to be sent to the server. + * @param {function} callback - A callback function which will be called after + * the message has been sent to the socket. + */ + sendMessage: function (type, data, callback) { + this.sessionID = decodeURIComponent(readCookie("sessionID")); + this.password = readCookie("password"); + var msg = { "component" : "pad", // FIXME: Remove this stupidity! + "type": type, + "data": data, + "padId": this.padID, + "token": this.token, + "sessionID": this.sessionID, + "password": this.password, + "protocolVersion": 2}; + this._super(msg, callback); + }, + + onMessage: function (message) { + this.log("[authorized_client] > onMessage:", message); + if (message.accessStatus) + { //access denied? + //TODO raise some kind of error? + this.log("ACCESS ERROR!"); + } + this.dispatchMessage(message.type, message.data); + }, + + /** + * Dispatch incoming messages to handlers in subclasses or registered + * as event handlers. + * @param {string} type - The type of the message. See the server code + * for possible values. + * @param {object} data - The message payload. + */ + dispatchMessage: function(type, data) { + this.log("[authorized_client] > dispatchMessage('%s', %s)", type, data); + // first call local handlers + if ("handle_" + type in this) + this["handle_" + type](data); + // then call registered handlers + if (type in this.handlers) + for(var h in this.handlers[type]) + { //TODO: maybe chain the handlers into some kind of chain-of-responsibility? + var handler = this.handlers[type][h]; + handler.handler.call(this, data, handler.context); + } + }, + + /** + * Register an event handler for a given message type. + * @param {string} type - The message type. + * @param {function} handler - The handler function. + * @param {object} context - Optionally, some context to be passed to the handler. + */ + on: function(type, handler, context) { + if (!(type in this.handlers)) + this.handlers[type] = []; + this.handlers[type].push({handler: handler, context: context}); + return this; + }, + + /** + * Dispatch COLLABROOM messages. + * @param {object} data - The data received from the server. + */ + handle_COLLABROOM: function(data) { + //this.log("[authsocket_client] handle_COLLABROOM: ", data); + this.dispatchMessage(data.type, data); + }, + + } +); + +require('./revisioncache'); +require('./revisionslider'); +AuthenticatedSocketClient("TimesliderClient", + { //statics + VERBOSE: false, + }, + { //instance + init: function (baseurl, padID) { + this.log = TimesliderClient.VERBOSE ? console.log : function () {}; + this._super(baseurl, padID); + }, + + onConnect: function () { + this.sendMessage("CLIENT_READY", {}); + }, + + initialize: function (clientVars) { + if (this.is_initialized) + return; + this.clientVars = clientVars; + var collabClientVars = this.clientVars.collab_client_vars; + this.savedRevisions = this.clientVars.savedRevisions; + + this.revisionCache = new RevisionCache(this, collabClientVars.rev || 0); + + this.padClient = new PadClient(this.revisionCache, + { + revnum: collabClientVars.rev, + timestamp: collabClientVars.time, + atext: { + text: collabClientVars.initialAttributedText.text, + attributes: collabClientVars.initialAttributedText.attribs, + apool: collabClientVars.apool, + }, + author_info: collabClientVars.historicalAuthorData, + palette: this.clientVars.colorPalette, + }); + + //TODO: not wild about the timeslider-top selector being hard-coded here. + this.ui = new RevisionSlider(this, $("#timeslider-top")); + this.is_initialized = true; + }, + + // ------------------------------------------ + // Handling events + handle_CLIENT_VARS: function(data) { + this.log("[timeslider_client] handle_CLIENT_VARS: ", data); + this.initialize(data); + }, + + /** + * Handle USER_NEWINFO messages, which let us know that a (new) user + * has connected to this pad. + * @param {object} data - the data received from the server. + */ + handle_USER_NEWINFO: function (data) { + this.log("[timeslider_client] handle_USER_NEWINFO: ", data.userInfo); + //TODO: we might not want to add EVERY new user to the users list, + //possibly only active users? + var authors = {}; + authors[data.userInfo.userId] = data.userInfo; + this.padClient.updateAuthors(authors); + this.ui.render(); + }, + + /** + * Handle USER_LEAVE messages, which lets us know that a user has left the + * pad. + * @param {object} data - The data received from the server. + */ + handle_USER_LEAVE: function (data) { + this.log("[timeslider_client] handle_USER_LEAVE ", data.userInfo); + }, + + /** + * Handle NEW_CHANGES messages, which lets us know that a new revision has + * been added to the pad. + * @param {object} data - The data received from the server. + */ + handle_NEW_CHANGES: function (data) { + this.log("[timeslider_client] handle_NEW_CHANGES: ", data); + var changesets = this.padClient.mergeForeignChangeset(data.changeset, data.apool); + //TODO: handle calculation of real timedela based on currenttime + //TODO: deal with author? + this.revisionCache.appendHeadRevision(data.newRev, changesets.forward, changesets.reverse, data.timeDelta); + this.ui.render(); + }, + + + /** + * Go to the specified revision number. This abstracts the implementation + * of the actual goToRevision in the padClient. + * @param {number} revision_number - The revision to go to. + * @param {callback} atRevision_callback - Called when the transition to the + * revision has completed (i.e. + * changesets have been applied). + */ + goToRevision: function (revision_number, atRevision_callback) { + this.padClient.goToRevision(revision_number, atRevision_callback); + }, + /** + * Get the authors for the current pad. + * @return {dict} - A dictionary of author-id to Author objects. + */ + getAuthors: function () { + return this.padClient.authors; + }, + /** + * Get the current revision. + * @return {Revision} - the current revision. + */ + getCurrentRevision: function () { + return this.padClient.revision; + }, + /** + * Get the head revision. + * @return {Revision} - the head revision. + */ + getHeadRevision: function () { + return this.revisionCache.getHeadRevision().revnum; + }, + } +); + +function init(baseURL) { + var timesliderclient; $(document).ready(function () { // start the custom js @@ -47,7 +325,7 @@ function init() { //ensure we have a token token = readCookie("token"); - if(token == null) + if(token === null) { token = "t." + randomString(); createCookie("token", token, 60); @@ -55,45 +333,43 @@ function init() { var loc = document.location; //get the correct port - var port = loc.port == "" ? (loc.protocol == "https:" ? 443 : 80) : loc.port; + var port = loc.port === "" ? (loc.protocol == "https:" ? 443 : 80) : loc.port; //create the url var url = loc.protocol + "//" + loc.hostname + ":" + port + "/"; //find out in which subfolder we are - var resource = exports.baseURL.substring(1) + 'socket.io'; - - //build up the socket io connection - socket = io.connect(url, {resource: resource}); + var resource = baseURL.substring(1) + 'socket.io'; - //send the ready message once we're connected - socket.on('connect', function() - { - sendSocketMsg("CLIENT_READY", {}); - }); + var cl; + console.log(url, baseURL, resource, padId); + timesliderclient = new TimesliderClient(url, padId) + .on("CLIENT_VARS", function(data, context, callback) { + //load all script that doesn't work without the clientVars + BroadcastSlider = require('./broadcast_slider').init(this,fireWhenAllScriptsAreLoaded); - socket.on('disconnect', function() - { - BroadcastSlider.showReconnectUI(); - }); + //initialize export ui + require('./pad_impexp').padimpexp.init(); - //route the incoming messages - socket.on('message', function(message) - { - if(window.console) console.log(message); + //change export urls when the slider moves + //TODO: fix this to use the slider.change event + //BroadcastSlider.onSlider(function(revno) + //{ + //// export_links is a jQuery Array, so .each is allowed. + //export_links.each(function() + //{ + //this.setAttribute('href', this.href.replace( /(.+?)\/\w+\/(\d+\/)?export/ , '$1/' + padId + '/' + revno + '/export')); + //}); + //}); - if(message.type == "CLIENT_VARS") - { - handleClientVars(message); - } - else if(message.accessStatus) - { - $("body").html("

You have no permission to access this pad

") - } else { - changesetLoader.handleMessageFromServer(message); - } - }); + //fire all start functions of these scripts, formerly fired with window.load + for(var i=0;i < fireWhenAllScriptsAreLoaded.length;i++) + { + fireWhenAllScriptsAreLoaded[i](); + } + //$("#ui-slider-handle").css('left', $("#ui-slider-bar").width() - 2); + }); //get all the export links - export_links = $('#export > .exportlink') + export_links = $('#export > .exportlink'); if(document.referrer.length > 0 && document.referrer.substring(document.referrer.lastIndexOf("/")-1,document.referrer.lastIndexOf("/")) === "p") { $("#returnbutton").attr("href", document.referrer); @@ -106,64 +382,14 @@ function init() { window.location.reload(); }); - exports.socket = socket; // make the socket available + //exports.socket = socket; // make the socket available exports.BroadcastSlider = BroadcastSlider; // Make the slider available hooks.aCallAll("postTimesliderInit"); }); -} - -//sends a message over the socket -function sendSocketMsg(type, data) -{ - var sessionID = decodeURIComponent(readCookie("sessionID")); - var password = readCookie("password"); - - var msg = { "component" : "pad", // FIXME: Remove this stupidity! - "type": type, - "data": data, - "padId": padId, - "token": token, - "sessionID": sessionID, - "password": password, - "protocolVersion": 2}; - - socket.json.send(msg); + return timesliderclient; } var fireWhenAllScriptsAreLoaded = []; - -var changesetLoader; -function handleClientVars(message) -{ - //save the client Vars - clientVars = message.data; - - //load all script that doesn't work without the clientVars - BroadcastSlider = require('./broadcast_slider').loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); - require('./broadcast_revisions').loadBroadcastRevisionsJS(); - changesetLoader = require('./broadcast').loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider); - //initialize export ui - require('./pad_impexp').padimpexp.init(); - - //change export urls when the slider moves - BroadcastSlider.onSlider(function(revno) - { - // export_links is a jQuery Array, so .each is allowed. - export_links.each(function() - { - this.setAttribute('href', this.href.replace( /(.+?)\/\w+\/(\d+\/)?export/ , '$1/' + padId + '/' + revno + '/export')); - }); - }); - - //fire all start functions of these scripts, formerly fired with window.load - for(var i=0;i < fireWhenAllScriptsAreLoaded.length;i++) - { - fireWhenAllScriptsAreLoaded[i](); - } - $("#ui-slider-handle").css('left', $("#ui-slider-bar").width() - 2); -} - -exports.baseURL = ''; exports.init = init; diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index 2e00b8c2a..786af23d8 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -52,12 +52,11 @@
<% e.begin_block("timesliderTop"); %>
-