From 44aa476ec07ec35c78a4ad01c8116786102b56a6 Mon Sep 17 00:00:00 2001 From: Peter 'Pita' Martischka Date: Mon, 20 Jun 2011 11:44:04 +0100 Subject: [PATCH] A lot small changes that results in a timeslider that shows the latest text --- node/AuthorManager.js | 10 + node/Models/Pad.js | 76 + ...MessageHandler.js => PadMessageHandler.js} | 12 +- node/SocketIORouter.js | 83 + node/TimesliderMessageHandler.js | 446 ++++ node/server.js | 54 +- package.json | 2 +- static/css/broadcast.css | 251 ++ static/css/pad2_ejs.css | 1139 +++++++++ static/img/backgrad.gif | Bin 0 -> 697 bytes static/img/crushed_button_undepressed.png | Bin 0 -> 4166 bytes static/img/crushed_current_location.png | Bin 0 -> 1009 bytes static/img/editbar_background.gif | Bin 0 -> 181 bytes static/img/editbar_background_left.gif | Bin 0 -> 204 bytes static/img/editbar_background_right.gif | Bin 0 -> 867 bytes static/img/maximize_maximized.png | Bin 0 -> 604 bytes static/img/padtop5.gif | Bin 0 -> 3872 bytes static/img/padtopback2.gif | Bin 0 -> 372 bytes static/img/play.png | Bin 0 -> 3017 bytes static/img/roundcorner_left.gif | Bin 0 -> 123 bytes static/img/roundcorner_right.gif | Bin 0 -> 131 bytes static/img/stepper_buttons.png | Bin 0 -> 4858 bytes static/img/timeslider_background.png | Bin 0 -> 915 bytes static/img/timeslider_left.png | Bin 0 -> 1653 bytes static/img/timeslider_right.png | Bin 0 -> 1581 bytes static/js/broadcast.js | 714 ++++++ static/js/broadcast_revisions.js | 133 + static/js/broadcast_slider.js | 489 ++++ static/js/collab_client.js | 3 +- static/js/cssmanager_client.js | 111 + static/js/domline_client.js | 292 +++ static/js/draggable.js | 189 ++ static/js/easysync2_client.js | 2269 +++++++++++++++++ static/js/linestylefilter_client.js | 342 +++ static/js/pad2.js | 5 +- static/timeslider.html | 376 +++ 36 files changed, 6944 insertions(+), 52 deletions(-) rename node/{MessageHandler.js => PadMessageHandler.js} (98%) create mode 100644 node/SocketIORouter.js create mode 100644 node/TimesliderMessageHandler.js create mode 100644 static/css/broadcast.css create mode 100644 static/css/pad2_ejs.css create mode 100644 static/img/backgrad.gif create mode 100644 static/img/crushed_button_undepressed.png create mode 100644 static/img/crushed_current_location.png create mode 100644 static/img/editbar_background.gif create mode 100644 static/img/editbar_background_left.gif create mode 100644 static/img/editbar_background_right.gif create mode 100644 static/img/maximize_maximized.png create mode 100644 static/img/padtop5.gif create mode 100644 static/img/padtopback2.gif create mode 100644 static/img/play.png create mode 100644 static/img/roundcorner_left.gif create mode 100644 static/img/roundcorner_right.gif create mode 100644 static/img/stepper_buttons.png create mode 100644 static/img/timeslider_background.png create mode 100644 static/img/timeslider_left.png create mode 100644 static/img/timeslider_right.png create mode 100644 static/js/broadcast.js create mode 100644 static/js/broadcast_revisions.js create mode 100644 static/js/broadcast_slider.js create mode 100644 static/js/cssmanager_client.js create mode 100644 static/js/domline_client.js create mode 100644 static/js/draggable.js create mode 100644 static/js/easysync2_client.js create mode 100644 static/js/linestylefilter_client.js create mode 100644 static/timeslider.html diff --git a/node/AuthorManager.js b/node/AuthorManager.js index 5d30b6026..44eb48b67 100644 --- a/node/AuthorManager.js +++ b/node/AuthorManager.js @@ -72,6 +72,16 @@ exports.getAuthor4Token = function (token, callback) }); } +/** + * Returns the Author Obj of the author + * @param {String} author The id of the author + * @param {Function} callback callback(err, authorObj) + */ +exports.getAuthor = function (author, callback) +{ + db.get("globalAuthor:" + author, callback); +} + /** * Returns the color Id of the author * @param {String} author The id of the author diff --git a/node/Models/Pad.js b/node/Models/Pad.js index 64b444450..8b9d3f79e 100644 --- a/node/Models/Pad.js +++ b/node/Models/Pad.js @@ -90,6 +90,11 @@ Class('Pad', { db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback); }, // getRevisionAuthor + getRevisionDate : function(revNum, callback) + { + db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback); + }, // getRevisionAuthor + getAllAuthors : function() { var authors = []; @@ -105,6 +110,77 @@ Class('Pad', { return authors; }, + getInternalRevisionAText : function(targetRev, callback) + { + var _this = this; + + var keyRev = this.getKeyRevisionNumber(targetRev); + var atext; + var changesets = []; + + //find out which changesets are needed + var neededChangesets = []; + var curRev = keyRev; + while (curRev < targetRev) + { + curRev++; + neededChangesets.push(curRev); + } + + async.series([ + //get all needed data out of the database + function(callback) + { + async.parallel([ + //get the atext of the key revision + function (callback) + { + db.getSub("pad:"+_this.id+":revs:"+keyRev, ["meta", "atext"], function(err, _atext) + { + atext = Changeset.cloneAText(_atext); + callback(err); + }); + }, + //get all needed changesets + function (callback) + { + async.forEach(neededChangesets, function(item, callback) + { + _this.getRevisionChangeset(item, function(err, changeset) + { + changesets[item] = changeset; + callback(err); + }); + }, callback); + } + ], callback); + }, + //apply all changesets to the key changeset + function(callback) + { + var apool = _this.apool(); + var curRev = keyRev; + + while (curRev < targetRev) + { + curRev++; + var cs = changesets[curRev]; + atext = Changeset.applyToAText(cs, atext, apool); + } + + callback(null); + } + ], function(err) + { + callback(err, atext); + }); + }, + + getKeyRevisionNumber : function(revNum) + { + return Math.floor(revNum / 100) * 100; + }, + text : function() { return this.atext.text; diff --git a/node/MessageHandler.js b/node/PadMessageHandler.js similarity index 98% rename from node/MessageHandler.js rename to node/PadMessageHandler.js index ecf11c952..bd8983971 100644 --- a/node/MessageHandler.js +++ b/node/PadMessageHandler.js @@ -143,11 +143,6 @@ exports.handleMessage = function(client, message) { throw "Message is null!"; } - //Etherpad sometimes send JSON and sometimes a JSONstring... - if(typeof message == "string") - { - message = JSON.parse(message); - } if(!message.type) { throw "Message have no type attribute!"; @@ -161,19 +156,16 @@ exports.handleMessage = function(client, message) else if(message.type == "COLLABROOM" && message.data.type == "USER_CHANGES") { - console.error(JSON.stringify(message)); handleUserChanges(client, message); } else if(message.type == "COLLABROOM" && message.data.type == "USERINFO_UPDATE") { - console.error(JSON.stringify(message)); handleUserInfoUpdate(client, message); } //if the message type is unkown, throw an exception else { - console.error(message); throw "unkown Message Type: '" + message.type + "'"; } } @@ -469,9 +461,9 @@ function handleClientReady(client, message) { throw "CLIENT_READY Message have no protocolVersion!"; } - if(message.protocolVersion != 1) + if(message.protocolVersion != 2) { - throw "CLIENT_READY Message have a unkown protocolVersion '" + protocolVersion + "'!"; + throw "CLIENT_READY Message have a unkown protocolVersion '" + message.protocolVersion + "'!"; } var author; diff --git a/node/SocketIORouter.js b/node/SocketIORouter.js new file mode 100644 index 000000000..35eee4b5c --- /dev/null +++ b/node/SocketIORouter.js @@ -0,0 +1,83 @@ +/** + * This is the Socket.IO Router. It routes the Messages between the + * components of the Server. The components are at the moment: pad and timeslider + */ + +/* + * 2011 Peter 'Pita' Martischka + * + * 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. + */ + +/** + * Saves all components + * key is the component name + * value is the component module + */ +var components = {}; + +var socket; + +/** + * adds a component + */ +exports.addComponent = function(moduleName, module) +{ + //save the component + components[moduleName] = module; + + //give the module the socket + module.setSocketIO(socket); +} + +/** + * sets the socket.io and adds event functions for routing + */ +exports.setSocketIO = function(_socket) +{ + //save this socket internaly + socket = _socket; + + socket.on('connection', function(client) + { + //tell all components about this connect + for(var i in components) + { + components[i].handleConnect(client); + } + + client.on('message', function(message) + { + //route this message to the correct component, if possible + if(message.component && components[message.component]) + { + console.error(message); + + components[message.component].handleMessage(client, message); + } + else + { + throw "Can't route the message:" + JSON.stringify(message); + } + }); + + client.on('disconnect', function() + { + //tell all components about this disconnect + for(var i in components) + { + components[i].handleDisconnect(client); + } + }); + }); +} diff --git a/node/TimesliderMessageHandler.js b/node/TimesliderMessageHandler.js new file mode 100644 index 000000000..8e589443c --- /dev/null +++ b/node/TimesliderMessageHandler.js @@ -0,0 +1,446 @@ +/** + * The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions + */ + +/* + * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var async = require("async"); +var padManager = require("./PadManager"); +var Changeset = require("./Changeset"); +var AttributePoolFactory = require("./AttributePoolFactory"); +var authorManager = require("./AuthorManager"); + +/** + * Saves the Socket class we need to send and recieve data from the client + */ +var socketio; + +/** + * This Method is called by server.js to tell the message handler on which socket it should send + * @param socket_io The Socket + */ +exports.setSocketIO = function(socket_io) +{ + socketio=socket_io; +} + +/** + * Handles the connection of a new user + * @param client the new client + */ +exports.handleConnect = function(client) +{ + +} + +/** + * Handles the disconnection of a user + * @param client the client that leaves + */ +exports.handleDisconnect = function(client) +{ + +} + +/** + * Handles a message from a user + * @param client the client that send this message + * @param message the message from the client + */ +exports.handleMessage = function(client, message) +{ + //Check what type of message we get and delegate to the other methodes + if(message.type == "CLIENT_READY") + { + handleClientReady(client, message); + } + //if the message type is unkown, throw an exception + else + { + throw "unkown Message Type: '" + message.type + "'"; + } +} + +function handleClientReady(client, message) +{ + createTimesliderClientVars (message.padId, function(err, clientVars) + { + if(err) throw err; + + client.send({type: "CLIENT_VARS", data: clientVars}); + }) +} + +function createTimesliderClientVars (padId, callback) +{ + var clientVars = { + viewId: padId, + colorPalette: ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd"], + sliderEnabled : true, + supportsSlider: true, + savedRevisions: [], + padIdForUrl: padId, + fullWidth: false, + disableRightBar: false, + initialChangesets: [], + hooks: [], + initialStyledContents: {} + }; + var pad; + var initialChangesets = []; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + pad = _pad; + callback(err); + }); + }, + //get all authors and add them to + function(callback) + { + var historicalAuthorData = {}; + //get all authors out of the attribut pool + var authors = pad.getAllAuthors(); + + //get all author data out of the database + async.forEach(authors, function(authorId, callback) + { + authorManager.getAuthor(authorId, function(err, author) + { + historicalAuthorData[authorId] = author; + callback(err); + }); + }, function(err) + { + //add historicalAuthorData to the clientVars and continue + clientVars.historicalAuthorData = historicalAuthorData; + clientVars.initialStyledContents.historicalAuthorData = historicalAuthorData; + callback(err); + }); + }, + function(callback) + { + //currentTime: rev.timestamp, + + //get the head revision Number + var lastRev = pad.getHeadRevisionNumber(); + + //add the revNum to the client Vars + clientVars.revNum = lastRev; + clientVars.totalRevs = lastRev; + + var atext = Changeset.cloneAText(pad.atext); + var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); + var apool = attribsForWire.pool.toJsonable(); + atext.attribs = attribsForWire.translated; + + clientVars.initialStyledContents.apool = apool; + clientVars.initialStyledContents.atext = atext; + + var granularities = [100, 10, 1]; + + //get the latest rough changesets + async.forEach(granularities, function(granularity, callback) + { + var topGranularity = granularity*10; + + getChangesetInfo(padId, Math.floor(lastRev / topGranularity)*topGranularity, + Math.floor(lastRev / topGranularity)*topGranularity+topGranularity, granularity, + function(err, changeset) + { + clientVars.initialChangesets.push(changeset); + callback(err); + }); + }, callback); + } + ], function(err) + { + callback(err, clientVars); + }); +} + +/** + * Tries to rebuild the getChangestInfo function of the original Etherpad + * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 + */ +function getChangesetInfo(padId, startNum, endNum, granularity, callback) +{ + var forwardsChangesets = []; + var backwardsChangesets = []; + var timeDeltas = []; + var apool = AttributePoolFactory.createAttributePool(); + var pad; + var composedChangesets = {}; + var revisionDate = []; + var lines; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + pad = _pad; + callback(err); + }); + }, + function(callback) + { + //calculate the last full endnum + var lastRev = pad.getHeadRevisionNumber(); + if (endNum > lastRev+1) { + endNum = lastRev+1; + } + endNum = Math.floor(endNum / granularity)*granularity; + + var compositesChangesetNeeded = []; + var revTimesNeeded = []; + + //figure out which composite Changeset and revTimes we need, to load them in bulk + var compositeStart = startNum; + while (compositeStart < endNum) + { + var compositeEnd = compositeStart + granularity; + + //add the composite Changeset we needed + compositesChangesetNeeded.push({start: compositeStart, end: compositeEnd}); + + //add the t1 time we need + revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1); + //add the t2 time we need + revTimesNeeded.push(compositeEnd - 1); + + compositeStart += granularity; + } + + //get all needed db values parallel + async.parallel([ + function(callback) + { + //get all needed composite Changesets + async.forEach(compositesChangesetNeeded, function(item, callback) + { + composePadChangesets(padId, item.start, item.end, function(err, changeset) + { + composedChangesets[item.start + "/" + item.end] = changeset; + callback(err); + }); + }, callback); + }, + function(callback) + { + //get all needed revision Dates + async.forEach(revTimesNeeded, function(revNum, callback) + { + pad.getRevisionDate(revNum, function(err, revDate) + { + revisionDate[revNum] = Math.floor(revDate/1000); + callback(err); + }); + }, callback); + }, + //get the lines + function(callback) + { + getPadLines(padId, startNum-1, function(err, _lines) + { + lines = _lines; + callback(err); + }); + } + ], callback); + }, + //doesn't know what happens here excatly :/ + function(callback) + { + var compositeStart = startNum; + + while (compositeStart < endNum) + { + if (compositeStart + granularity > endNum) + { + break; + } + + var compositeEnd = compositeStart + granularity; + + var forwards = composedChangesets[compositeStart + "/" + compositeEnd]; + var backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); + + Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); + Changeset.mutateTextLines(forwards, lines.textlines); + + var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); + var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); + + var t1, t2; + if (compositeStart == 0) + { + t1 = revisionDate[0]; + } + else + { + t1 = revisionDate[compositeStart - 1]; + } + + t2 = revisionDate[compositeEnd - 1]; + + timeDeltas.push(t2 - t1); + forwardsChangesets.push(forwards2); + backwardsChangesets.push(backwards2); + + compositeStart += granularity; + } + + callback(); + } + ], function(err) + { + if(err) + { + callback(err); + } + else + { + callback(null, {forwardsChangesets: forwardsChangesets, + backwardsChangesets: backwardsChangesets, + apool: apool.toJsonable(), + actualEndNum: endNum, + timeDeltas: timeDeltas, + start: startNum, + granularity: granularity }); + } + }); +} + +/** + * Tries to rebuild the getPadLines function of the original Etherpad + * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 + */ +function getPadLines(padId, revNum, callback) +{ + var atext; + var result = {}; + var pad; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + pad = _pad; + callback(err); + }); + }, + //get the atext + function(callback) + { + if(revNum >= 0) + { + pad.getInternalRevisionAText(revNum, function(err, _atext) + { + atext = _atext; + callback(err); + }); + } + else + { + atext = Changeset.makeAText("\n"); + callback(null); + } + }, + function(callback) + { + result.textlines = Changeset.splitTextLines(atext.text); + result.alines = Changeset.splitAttributionLines(atext.attribs, atext.text); + callback(null); + } + ], function(err) + { + callback(err, result); + }); +} + +/** + * Tries to rebuild the composePadChangeset function of the original Etherpad + * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 + */ +function composePadChangesets(padId, startNum, endNum, callback) +{ + var pad; + var changesets = []; + var changeset; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + pad = _pad; + callback(err); + }); + }, + //fetch all changesets we need + function(callback) + { + var changesetsNeeded=[]; + + //create a array for all changesets, we will + //replace the values with the changeset later + for(var r=startNum;r + +.docbarpadtitle { position: absolute; height: auto; left: 9px; + width: 280px; font-size: 1.6em; color: #444; font-weight: normal; + line-height: 22px; margin-left: 2px; height: 22px; top: 2px; + overflow: hidden; text-overflow: ellipsis /*not supported in FF*/; + white-space:nowrap; } +.docbar-public .docbarpadtitle { padding-left: 22px; + background: url(/static/img/public.gif) no-repeat left center; } + +.docbarrenamelink { position: absolute; top: 6px; + font-size: 1.1em; display: none; } +.docbarrenamelink a { color: #999; } +.docbarrenamelink a:hover { color: #48d; } +#padtitlebuttons { position: absolute; width: 74px; zoom: 1; + height: 17px; top: 4px; left: 170px; display: none; + background: url(/static/img/ok_or_cancel.gif) 0px 0px; } +#padtitlesave { position: absolute; display: block; + height: 0; padding-top: 17px; overflow: hidden; + width: 23px; left: 0; top: 0; } +#padtitlecancel { position: absolute; display: block; + height: 0; padding-top: 17px; overflow: hidden; + width: 35px; right: 0; top: 0; } +#padtitleedit { position: absolute; top: 2px; left: 5px; + height: 15px; padding: 2px; font-size: 1.4em; + background: white; border-left: 1px solid #c3c3c3; + border-top: 1px solid #c3c3c3; + border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6; + width: 150px; display: none; +} + +#padmain { + margin-top: 6px; + position: absolute; + top: 56px; + left: 0px; + right: 0px; + bottom: 0px; + zoom: 1; +} + +#padeditor { + bottom:0px; + left:0; + position:absolute; + right:0; + top:0; + zoom: 1; +} +.hidesidebar #padeditor { right: 0; } + +#vdraggie { + background: url(/static/img/vdraggie.gif) no-repeat top center; + cursor: W-resize; + bottom:0; + position:absolute; + right:268px; + top:0; + width:56px; + z-index: 10; +} + +.editbar { height: 36px; + background: #a5bfe2 url(/static/img/editbar_background.gif) repeat-x; + height:36px; + left:0; + position:absolute; + right:0; + top:0; + color: #444444; +} + +.editbarinner { + overflow: hidden; +} + +.editbarleft { float: left; height: 100%; overflow: hidden; + background: url(/static/img/editbar_background_left.gif) no-repeat left top; width: 2px; } +.editbarright { float: right; height: 100%; overflow: hidden; + background: url(/static/img/editbar_background_right.gif) no-repeat right top; width: 2px; } + +.editbartable +{ + position:absolute; + top: 6px; + left: 7px; + width: 250px; + height: 24px; +} + +.editbarsavetable +{ + position:absolute; + top: 6px; + right: 29px; + height: 24px; +} + +.editbarsavetable td, .editbartable td +{ + white-space: nowrap; +} + +.editbarbutton +{ + border-top: 1px solid #8b9eba; + border-bottom: 1px solid #8b9eba; + border-left: 1px solid #758aa9; + background-color: white; + padding: 0px 2px; +} + +.editbarbutton img +{ + border: 0px; + width: 16px; + height: 16px; +} + +.editbarbutton a { + text-decoration: none; +} +.editbarbutton a:active +{ + position: relative; + top: 1px; + left: 1px; +} + +.editbargroupsfirst +{ + border-left-width: 0px !important; +} + +.editbar #syncstatussyncing { position: absolute; height: 26px; width: 26px; + background: url(/static/img/syncing2.gif) no-repeat center center; + right: 38px; top: 5px; display: none; } +.editbar #syncstatusdone { position: absolute; height: 26px; width: 26px; + background: url(/static/img/syncdone.gif) no-repeat center center; + right: 38px; top: 5px; display: none; } + +#editorcontainerbox { + border-left: 1px solid #c4c4c4; border-right: 1px solid #c4c4c4; + border-bottom: 1px solid #c4c4c4; + background: #fff; + bottom:0; + left:0; + overflow: hidden; + position: absolute; + right: 0; + top: 36px; + zoom: 1; + z-index: 11; +} + +#editorcontainer { height: 100%; } + +#editorcontainer iframe { width: 100%; height: 100%; padding: 0; margin: 0; } + +#editorloadingbox { padding-top: 100px; padding-bottom: 100px; font-size: 2.5em; color: #aaa; + text-align: center; position: absolute; width: 100%; height: 30px; z-index: 100; } + +#padsidebar { + bottom: 0px; + position: absolute; + right: 0; + top: 0; + width: 290px; + overflow: hidden; + z-index: 11; +} +.hidesidebar #padsidebar { width: 0; overflow: hidden; } + +#padsidebarfull { + border: 1px solid #c4c4c4; + background: #fafafa; + zoom: 1; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding: 8px; +} + +#padusers { + border: 1px solid #c4c4c4; + background: #fafafa; + zoom: 1; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 300px; +} +#myuser { + background: #d9e7f9; + padding: 5px; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 31px; +} +#otherusers { + overflow: auto; + position: absolute; + top: 41px; + left: 0; + right: 0; + bottom: 28px; +} +#userlistbuttonarea { + background: url(/static/img/inviteshare2.gif) repeat-x 0 0; + position: absolute; + height: 28px; + left: 0; + right: 0; + bottom: 0; +} + +#hdraggie { + background: url(/static/img/hdraggie.gif) repeat-x center top; + cursor: S-resize; + position: absolute; + top: 300px; + left: 0; + right: 0; + height: 10px; +} + +#padchat { + border: 1px solid #c4c4c4; + position: absolute; + top: 310px; + left: 0; + right: 0; + bottom: 0px; +} +#chatlines { + overflow: auto; + background: #fafafa; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 30px; +} +#chatbottom { + background: #ecf2fa; + padding: 4px; + position: absolute; + height: 22px; + left: 0; + right: 0; + bottom: 0px; +} + +#myswatchbox { position: absolute; left: 5px; top: 5px; width: 22px; height: 22px; + /*border-top: 1px solid #c3cfe0; border-left: 1px solid #c3cfe0; + border-right: 1px solid #ecf3fc; border-bottom: 1px solid #ecf3fc;*/ + border: 1px solid #bbb; + padding: 1px; background: transparent; cursor: pointer; } +#myuser .myswatchboxhoverable, #myuser .myswatchboxunhoverable { + background: white; +} +#myuser .myswatchboxhoverable:hover { + background: #bbb; +} +#myswatch { width: 100%; height: 100%; background: transparent;/*...initially*/ } +#mycolorpicker { + background: url(/static/img/colorpicker.gif) no-repeat left top; + width: 232px; height: 140px; + position: absolute; + left: 13px; top: 13px; z-index: 101; + display: none;/*...initially*/ +} +#mycolorpicker .n1 { left: 13px; } +#mycolorpicker .n2 { left: 40px; } +#mycolorpicker .n3 { left: 67px; } +#mycolorpicker .n4 { left: 94px; } +#mycolorpicker .n5 { left: 121px; } +#mycolorpicker .n6 { left: 148px; } +#mycolorpicker .n7 { left: 175px; } +#mycolorpicker .n8 { left: 202px; } + +#mycolorpicker .n9 { left: 13px; top: 34px ! important;} +#mycolorpicker .n10 { left: 40px; top: 34px ! important;} +#mycolorpicker .n11 { left: 67px; top: 34px ! important;} +#mycolorpicker .n12 { left: 94px; top: 34px ! important;} +#mycolorpicker .n13 { left: 121px; top: 34px ! important;} +#mycolorpicker .n14 { left: 148px; top: 34px ! important;} +#mycolorpicker .n15 { left: 175px; top: 34px ! important;} +#mycolorpicker .n16 { left: 202px; top: 34px ! important;} + +#mycolorpicker .n17 { left: 13px; top: 56px ! important;} +#mycolorpicker .n18 { left: 40px; top: 56px ! important;} +#mycolorpicker .n19 { left: 67px; top: 56px ! important;} +#mycolorpicker .n20 { left: 94px; top: 56px ! important;} +#mycolorpicker .n21 { left: 121px; top: 56px ! important;} +#mycolorpicker .n22 { left: 148px; top: 56px ! important;} +#mycolorpicker .n23 { left: 175px; top: 56px ! important;} +#mycolorpicker .n24 { left: 202px; top: 56px ! important;} + +#mycolorpicker .n25 { left: 13px; top: 78px ! important;} +#mycolorpicker .n26 { left: 40px; top: 78px ! important;} +#mycolorpicker .n27 { left: 67px; top: 78px ! important;} +#mycolorpicker .n28 { left: 94px; top: 78px ! important;} +#mycolorpicker .n29 { left: 121px; top: 78px ! important;} +#mycolorpicker .n30 { left: 148px; top: 78px ! important;} +#mycolorpicker .n31 { left: 175px; top: 78px ! important;} +#mycolorpicker .n32 { left: 202px; top: 78px ! important;} + +#mycolorpicker .pickerswatchouter { + border: 1px solid white; + width: 15px; height: 15px; position: absolute; + top: 12px; +} +#mycolorpicker .pickerswatch { + border: 1px solid #999; + width: 13px; + height: 13px; + position: absolute; + left: 0; top: 0; +} +#mycolorpicker .picked { border: 1px solid #666 !important; } +#mycolorpicker .picked .pickerswatch { border: 1px solid #666; } +#mycolorpickersave { position: absolute; left: 14px; top: 102px; + width: 47px; height: 0; padding-top: 20px; overflow: hidden; + cursor: pointer; } +#mycolorpickercancel { position: absolute; left: 87px; top: 102px; + width: 44px; height: 0; padding-top: 20px; overflow: hidden; + cursor: pointer; } +#myusernameform { margin-left: 35px; } +#myusernameedit { font-size: 1.6em; color: #444; + padding: 3px; height: 18px; margin: 0; border: 0; + width: 197px; background: transparent; } +#myusernameform input.editable { border: 1px solid #bbb; } +#myuser .myusernameedithoverable:hover { background: white; } +#mystatusform { margin-left: 35px; margin-top: 5px; } +#mystatusedit { font-size: 1.2em; color: #777; + font-style: italic; display: none; + padding: 2px; height: 14px; margin: 0; border: 1px solid #bbb; + width: 199px; background: transparent; } +#myusernameform .editactive, #myusernameform .editempty { + background: white; border-left: 1px solid #c3c3c3; + border-top: 1px solid #c3c3c3; + border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6; +} +#myusernameform .editempty { color: #ef641e; } + +table#otheruserstable { display: none; } +#nootherusers { padding: 10px; font-size: 1.2em; color: #999; font-weight: bold;} +#nootherusers a { color: #48d; } + +#otheruserstable td { + border-bottom: 1px solid #e1e1e1; + height: 26px; + vertical-align: middle; + padding: 0 2px; +} + +#otheruserstable .swatch { + border: 1px solid #999; width: 13px; height: 13px; overflow: hidden; + margin: 0 4px; +} + +.usertdswatch { width: 1%; } +.usertdname { font-size: 1.3em; color: #444; } +.usertdstatus { font-size: 1.1em; font-style: italic; color: #999; } +.usertdactivity { font-size: 1.1em; color: #777; } + +.usertdname input { border: 1px solid #bbb; width: 80px; padding: 2px; } +.usertdname input.editactive, .usertdname input.editempty { + background: white; border-left: 1px solid #c3c3c3; + border-top: 1px solid #c3c3c3; + border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6; +} +.usertdname input.editempty { color: #888; font-style: italic;} + +#sharebutton { + background: url(/static/img/inviteshare2.gif) no-repeat 0 -31px; + position: absolute; display: block; top: 3px; padding-top: 23px; + height: 0; overflow: hidden; width: 96px; + left:50%; + margin-left: -48px; +} + + /*#guestslabel { font-size: 1.2em; position: absolute; width: auto; + height: 22px; line-height: 22px; top: 4px; left: 8px; } +#guestsmenu { font-size: 1.2em; position: absolute; left: 100px; + top: 5px; width: 95px; } +.guestpolicystuff { display: none; }*/ + +.guestprompt { border: 1px solid #ccc; font-size: 1.2em; + padding: 5px; color: #222; background: #ffc; } +.guestprompt .choices { float: right; } +.guestprompt a { margin: 0 0.5em; } + +#chattop { background: #ecf2fa; padding: 5px; font-size: 1.2em; border-bottom: 1px solid #ddd; } +#chattop a { color: #36b; } +#chatlines .chatline { color: #444; padding-left: 5px; padding-top: 2px; padding-bottom: 2px; + background: #ddd; overflow: hidden; } +#chatlines .chatlinetime { display: block; font-size: 1em; color: #666; float: right; width: auto; + padding-right: 5px; } +#chatlines .chatlinename, #chatlines .chatlinetext { font-size: 1.2em; } +#chatlines h2 { margin: 0; padding-left: 5px; padding-top: 2px; padding-bottom: 2px; color: #999; font-style: italic; font-weight: bold; font-size: 1.2em; } +#chatprompt { font-size: 1.2em; color: #444; float: left; line-height: 22px; width: 35px; text-align: right; } +#chatentryform { margin-left: 40px; } +#chatentrybox { font-size: 1.2em; color: #444; + padding: 2px; height: 16px; margin: 0; border-left: 1px solid #c3c3c3; + border-top: 1px solid #c3c3c3; + border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6; + width: 230px; } +#padchat a#chatloadmore { display: none; font-size: 1.2em; padding: 2px 5px; font-style: italic; } +#padchat #chatloadingmore { display: none; font-size: 1.2em; padding: 2px 5px; font-style: italic; + color: #999; } +#padchat a#chatloadmore:focus { outline: 0; } + +#djs { font-family: monospace; font-size: 10pt; + height: 200px; overflow: auto; border: 1px solid #ccc; + background: #fee; margin: 0; padding: 6px; +} +#djs p { margin: 0; padding: 0; display: block; } + +.modaldialog.cboxreconnecting .modaldialog-inner, +.modaldialog.cboxconnecting .modaldialog-inner { + background: url(/static/img/connectingbar.gif) no-repeat center 60px; + height: 100px; +} +.modaldialog.cboxreconnecting { + background: #fed; +} +.modaldialog.cboxconnecting, +.modaldialog.cboxdisconnected { + background: #fdd; +} +.cboxdisconnected #connectionboxinner div { display: none; } +.cboxdisconnected_userdup #connectionboxinner #disconnected_userdup { display: block; } +.cboxdisconnected_initsocketfail #connectionboxinner #disconnected_initsocketfail { display: block; } +.cboxdisconnected_looping #connectionboxinner #disconnected_looping { display: block; } +.cboxdisconnected_slowcommit #connectionboxinner #disconnected_slowcommit { display: block; } +.cboxdisconnected_unauth #connectionboxinner #disconnected_unauth { display: block; } +.cboxdisconnected_unknown #connectionboxinner #disconnected_unknown { display: block; } +.cboxdisconnected_initsocketfail #connectionboxinner #reconnect_advise, +.cboxdisconnected_looping #connectionboxinner #reconnect_advise, +.cboxdisconnected_slowcommit #connectionboxinner #reconnect_advise, +.cboxdisconnected_unknown #connectionboxinner #reconnect_advise { display: block; } +.cboxdisconnected_unknown #connectionboxinner #offline_notification { display: block; } +.cboxdisconnected div#reconnect_form { display: block; } +.cboxdisconnected div#hide_dialog{ display: block; } +.cboxdisconnected .disconnected h2 { display: none; } +.cboxdisconnected .disconnected .h2_disconnect { display: block; } +.cboxdisconnected_userdup .disconnected h2.h2_disconnect { display: none; } +.cboxdisconnected_userdup .disconnected h2.h2_userdup { display: block; } +.cboxdisconnected_unauth .disconnected h2.h2_disconnect { display: none; } +.cboxdisconnected_unauth .disconnected h2.h2_unauth { display: block; } + +#connectionstatus { + position: absolute; width: 37px; height: 41px; overflow: hidden; + right: 0; + z-index: 11; +} +#connectionboxinner .connecting { + margin-top: 20px; + font-size: 2.0em; color: #555; + text-align: center; display: none; +} +.cboxconnecting #connectionboxinner .connecting { display: block; } + +#connectionboxinner .disconnected h2 { + font-size: 1.8em; color: #333; + text-align: left; + margin-top: 10px; margin-left: 10px; margin-right: 10px; + margin-bottom: 10px; +} +#connectionboxinner .disconnected p { + margin: 10px 10px; + font-size: 1.2em; + line-height: 1.1; + color: #333; +} +#connectionboxinner .disconnected { display: none; } +.cboxdisconnected #connectionboxinner .disconnected { display: block; } + +#connectionboxinner .reconnecting { + margin-top: 20px; + font-size: 1.6em; color: #555; + text-align: center; display: none; +} +.cboxreconnecting #connectionboxinner .reconnecting { display: block; } + +#reconnect_form button { + font-size: 12pt; +} + +#hide_dialog button { + font-size: 12pt; +} + +/* We give docbar a higher z-index than its descendant impexp-wrapper in + order to allow the Import/Export panel to be on top of stuff lower + down on the page in IE. Strange but it works! */ +.docbar { z-index: 52; } + +#impexp-wrapper { width: 650px; right: 10px; } +#impexp-panel { height: 160px; } +.docbarimpexp-closing #impexp-wrapper { z-index: 50; } + +#savedrevs-wrapper { width: 100%; left: 0; } +#savedrevs-panel { height: 79px; } +.docbarsavedrevs-closing #savedrevs-wrapper { z-index: 50; } +#savedrevs-wrapper .dbpanel-rightedge { background-position: 0 -10px; } + +#options-wrapper { width: 340px; right: 200px; } +#options-panel { height: 114px; } +.docbaroptions-closing #options-wrapper { z-index: 50; } + +#security-wrapper { width: 320px; right: 300px; } +#security-panel { height: 130px; } +.docbarsecurity-closing #security-wrapper { z-index: 50; } + +#revision-notifier { position: absolute; right: 8px; top: 25px; + width: auto; height: auto; font-size: 1.2em; background: #ffc; + border: 1px solid #aaa; color: #444; padding: 3px 5px; + display: none; z-index: 55; } +#revision-notifier .label { color: #777; font-weight: bold; } + +/* We don't ever actually hide the wrapper, even when the panel is + cloased, so that its contents can always be manipulated accurately. */ +.dbpanel-wrapper { position: absolute; + overflow: hidden; /* animated: */ height: 0; top: 25px; /* /animated */ + z-index: 51; zoom: 1; } +.dbpanel-panel { position: absolute; bottom: 0; width: 100%; } + +.dbpanel-middle { margin-left: 7px; margin-right: 7px; + position: relative; height: 100%; overflow: hidden; zoom: 1; } +.dbpanel-inner { background: #f7f7f7 /* covered up by images */; + width: 100%; height: 100%; position: absolute; overflow: hidden; top: -10px; } + +.dbpanel-top { position: absolute; top: 0; width: 100%; + height: 400px; background-image: url(/static/img/docpanelmiddle2.png); + background-position: left top; } + +.dbpanel-bottom { position: absolute; height: 400px; + bottom: -390px; width: 100%; + background-image: url(/static/img/docpanelmiddle2.png); + background-position: left top; +} + +* html .dbpanel-top, * html .dbpanel-bottom { /* for IE 6+ */ + background-color: transparent; + background-image: url(/static/img/apr09/blank.gif); + /* scale the image instead of repeating, but it amounts to the same */ + filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src="/static/img/docpanelmiddle2.png", sizingMethod="scale"); +} + +.dbpanel-leftedge, .dbpanel-rightedge, .dbpanel-botleftcorner, .dbpanel-botrightcorner { + position: absolute; + background-repeat: no-repeat; + background-color: transparent; + background-image: url(/static/img/docpaneledge2.png); +} + +.dbpanel-leftedge, .dbpanel-rightedge { height: 100%; width: 7px; bottom: 11px; } +.dbpanel-botleftcorner, .dbpanel-botrightcorner { height: 11px; width: 7px; bottom: 0; } + +.dbpanel-leftedge, .dbpanel-botleftcorner { left: 0; background-position: -7px 0; } +.dbpanel-rightedge, .dbpanel-botrightcorner { right: 0; background-position: 0 0; } + +#importexport { position: absolute; top: 5px; left: 0; font-size: 1.2em; color: #444; + height: 100%; width: 100%; } + +* html .dbpanel-leftedge, * html .dbpanel-rightedge, * html .dbpanel-botleftcorner, * html .dbpanel-botrightcorner { + background-color: transparent; + background-image: url(/static/img/apr09/blank.gif); + /* crop the image */ + filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src="/static/img/docpaneledge2.png", sizingMethod="crop"); +} +* html .dbpanel-leftedge, * html .dbpanel-botleftcorner { left: -7px; width: 14px; } + +#impexp-importlabel { position: absolute; top: 5px; left: 10px; width: 300px; } + +#importform { position: absolute; top: 24px; left: 5px; width: 300px; height: 60px; } +#importformsubmitdiv, #importformfilediv { padding: 5px 5px; } +#importexport .importformenabled { + background: #cfc; + border: 1px solid #292; +} +#importexport span.nowrap { white-space: nowrap; } +#importexport #importstatusball { margin-left: 3px; padding-top: 1px; display: none; } +#importexport #importarrow { margin-left: 5px; padding-top: 1px; display: none; } +#importexport .importmessage { border: 1px solid #992; + background: #ffc; padding: 5px; font-size: 85%; display: none; } +#importexport #importmessagefail { margin-top: 5px; } +#importexport #importmessagesuccess { margin: 0 20px; } +#importexport a.disabledexport { + color: #333; text-decoration: none; + opacity: 0.5; filter: alpha(opacity = 50) /*IE*/; +} +#importexport #importfileinput { padding: 2px 0; } +#importexport #importsubmitinput { padding: 2px; } + +#impexp-divider { position: absolute; left: 320px; top: 5px; height: 135px; width: 2px; + background: #ddd; } +#impexp-close { display: block; position: absolute; right: 2px; bottom: 15px; + width: auto; height: auto; font-size: 85%; color: #444; + z-index: 61 /* > clickcatcher */} +#impexp-disabled-clickcatcher { + display: none; + position: absolute; width: 100%; height: 100%; + z-index: 60; +} + +#impexp-exportlabel { position: absolute; top: 5px; left: 350px; + width: 300px; } +#exportlinks .exportlink { + display: block; position: absolute; height: 22px; width: auto; + background-repeat: no-repeat; + background-image: url(/static/img/fileicons.gif); + line-height: 22px; padding-left: 22px; padding-right: 2px; +} +#exportlinks .n1 { left: 350px; top: 30px; } +#exportlinks .n2 { left: 350px; top: 57px; } +#exportlinks .n3 { left: 350px; top: 84px; } +#exportlinks .n4 { left: 485px; top: 30px; } +#exportlinks .n5 { left: 485px; top: 57px; } +#exportlinks .n6 { left: 485px; top: 84px; } +#exportlinks .exporthrefdoc { background-position: 2px -1px; } +#exportlinks .exporthrefhtml { background-position: 2px -25px; } +#exportlinks .exporthreflink { background-position: 2px -49px; } +#exportlinks .exporthrefodt { background-position: 2px -73px; } +#exportlinks .exporthrefpdf { background-position: 2px -97px; } +#exportlinks .exporthreftxt { background-position: 2px -121px; } + +#savedrevisions { position: absolute; top: 0; left: 0; font-size: 1.2em; + color: #444; height: 100%; width: 100%; } +#savedrevs-scrolly { height: 75px; width: auto; margin-right: 136px; + overflow: hidden; position: relative; top: 1px; +} +#savedrevs-scrollleft { height: 100%; width: 14px; position: absolute; + left: 0; top: 0; cursor: pointer; + background: url(/static/img/savedrevarrows.gif) no-repeat right top; +} +#savedrevs-scrollright { height: 100%; width: 14px; position: absolute; + right: 0; top: 0; cursor: pointer; + background: url(/static/img/savedrevarrows.gif) no-repeat left top; +} +#savedrevs-scrolly .disabledscrollleft { background-position: right bottom; } +#savedrevs-scrolly .disabledscrollright { background-position: left bottom; } +#savedrevs-scrollouter { margin-left: 14px; margin-right: 14px; + width: auto; height: 100%; overflow: hidden; position: relative; +} +#savedrevs-scrollinner { position: absolute; width: 1px; height: 100%; + overflow: visible; right: 0/*...initially*/; top: 0; } +#savedrevisions .srouterbox { width: 120px; height: 100%; + position: absolute; top: 0; +} +#savedrevisions .srinnerbox { position: relative; top: 8px; + height: 59px; width: auto; border-left: 1px solid #ddd; + padding: 0 8px 0 8px; } +#savedrevisions a.srname { display: block; white-space: nowrap; + text-overflow: ellipsis /*no FF support*/; overflow: hidden; + text-decoration: none; color: #444; cursor: text; + padding: 1px; height: 14px; position: relative; left: -1px; + width: 100px /*specify for proper overflow in IE*/; +} +#savedrevisions a.srname:hover { text-decoration: none; color: #444; + border: 1px solid #ccc; padding: 0; } +#savedrevisions .sractions { font-size: 85%; color: #ccc; + margin-top: 1px; height: 12px; } +#savedrevisions .sractions a { text-decoration: none; + color: #06c; } +#savedrevisions .sractions a:hover { text-decoration: underline; } +#savedrevisions .srtime { color: #666; font-size: 90%; + white-space: nowrap; margin-top: 3px; } +#savedrevisions .srauthor { color: #666; font-size: 90%; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis /*no FF*/; +} +#savedrevisions .srtwirly { position: absolute; display: block; + bottom: 0; right: 10px; display: none; } +#savedrevisions .srnameedit { + position: absolute; +} +#savedrevs-savenow { display: block; position: absolute; + overflow: hidden; height: 0; padding-top: 24px; width: 81px; + top: 22px; right: 27px; + background: url(/static/img/savedrevsgfx2.gif) no-repeat 0 0; +} +#savedrevs-savenow:active { background-position: 0 -24px; } +#savedrevs-close { display: block; position: absolute; right: 7px; bottom: 8px; + width: auto; height: auto; font-size: 85%; color: #444; } +form#reconnectform { display: none; } + +#padoptions { position: absolute; top: 0; left: 0; font-size: 1.2em; + color: #444; height: 100%; width: 100%; line-height: 15px; } +#options-viewhead { font-weight: bold; position: absolute; top: 10px; left: 15px; + width: auto; height: auto; } +#padoptions label { display: block; } +#padoptions input { padding: 0; margin: 0; } +#options-colorscheck { position: absolute; left: 15px; top: 34px; width: 15px; height: 15px; } +#options-colorslabel { position: absolute; left: 35px; top: 34px; } +#options-linenoscheck { position: absolute; left: 15px; top: 57px; width: 15px; height: 15px; } +#options-linenoslabel { position: absolute; left: 35px; top: 57px; } +#options-fontlabel { position: absolute; left: 15px; top: 82px; } +#viewfontmenu { position: absolute; top: 80px; left: 90px; width: 110px; } +#options-viewexplain { position: absolute; left: 215px; top: 15px; width: 100px; height: 70px; font-size: .7em; + padding-left: 10px; padding-top: 10px; border-left: 1px solid #ccc; + line-height: 20px; font-weight: bold; color: #999; } +#options-close { display: block; position: absolute; right: 7px; bottom: 8px; + width: auto; height: auto; font-size: 85%; color: #444; } + +#padsecurity { position: absolute; top: 0; left: 0; font-size: 1.1em; + color: #444; height: 100%; width: 100%; line-height: 15px; } +#security-close { display: block; position: absolute; right: 7px; bottom: 8px; + width: auto; height: auto; font-size: 85%; color: #444; } +#security-passhead { font-weight: bold; position: absolute; top: 90px; left: 15px; + width: auto; height: auto; } +#security-passbody { position: absolute; left: 75px; top: 90px; } +#security-passwordedit { height: 15px; border: 1px solid #bbb; + position: absolute; top: 0; left: 15px; width: 120px; } +#security-password a { text-decoration: none; display: block; + width: auto; height: auto; } +#password-savelink, #password-cancellink {position: absolute; top: 0; } +#security-password a:hover { text-decoration: underline; } +#password-savelink { left: 144px; color: #06c; } +#password-cancellink { left: 180px; color: #666; } +#password-nonedit { left: 15px; position: absolute; + width: 220px; top: 0; } +#password-setlink { color: #06c; } +#password-clearlink { color: #06c; } +#password-display { height: 15px; width: auto; } +#password-inedit { display: none; } +#password-display, #password-setlink, #password-clearlink { + float: left; margin-right: 10px; +} +#password-display { font-size: 18px; } +#security-password .nopassword #password-display { font-size: 100%; } +#security-password .nopassword #password-clearlink { display: none; } +#security-password .nopassword #password-setlink { left: 60px; } + +#security-access { position: absolute; left: 15px; width: 200px; } +#security-accesshead { font-weight: bold; position: absolute; top: 10px; + left: 0; width: auto; height: auto; } +#security-access input, #security-access label { position: absolute; } +#security-access input { left: 10px; } +#security-access label { left: 30px; width: 250px; } +#access-private, #access-private-label { top: 35px; } +#access-public, #access-public-label { top: 60px; } +#security-access label { color: #999; } +#security-access label strong { font-weight: normal; padding-right: 10px; + color: #444; } + +#mainmodals { z-index: 600; /* higher than the modals themselves + so that modals are on top in IE */ } + +.modalfield { font-size: 1.2em; padding: 1px; border: 1px solid #bbb;} +#mainmodals .editempty { color: #aaa; } + +<% feedbackbox = {width:400, height:270}; %> +#feedbackbox { + position: absolute; display: none; + width: <%=feedbackbox.width%>px; height: <%=feedbackbox.height%>px; + left: 100px/*set in code*/; bottom: 50px; + z-index: 501; zoom: 1; +} +#feedbackbox-tl, #feedbackbox-tr, #feedbackbox-bl, #feedbackbox-br, +#feedbackbox-hide, #feedbackbox-send, #feedbackbox-back { + position: absolute; display: block; + background-repeat: no-repeat; + background-image: url(/static/img/feedbackbox2.gif); +} +#feedbackbox-tl { width: <%=feedbackbox.width-8%>px; + height: <%=feedbackbox.height-8%>px; left: 0; top: 0; + background-position: left top; } +#feedbackbox-tr { width: 8px; height: <%=feedbackbox.height-8%>px; + right: 0; top: 0; background-position: right top; } +#feedbackbox-bl { width: <%=feedbackbox.width-8%>px; + height: 8px; left: 0; bottom: 0; + background-position: left bottom; } +#feedbackbox-br { width: 8px; height: 8px; bottom: 0; right: 0; + background-position: right bottom; } +#feedbackbox-hide { width: 22px; height: 22px; right: 9px; top: 7px; + background-position: -569px -6px; +} +#feedbackbox-back { width: <%=feedbackbox.width-16%>px; + height: <%=feedbackbox.height-16%>px; left: 8px; top: 8px; + background-position: -8px -8px; + background-color: white; } +#feedbackbox-contents { width: <%=feedbackbox.width-16%>px; + height: <%=feedbackbox.height-16%>px; left: 8px; top: 8px; + position: absolute; font-size: 1.4em; color: #444; } +#feedbackbox-contentsinner { padding: 10px; } +#feedbackbox-send { width: 50px; height: 22px; right: 15px; bottom: 15px; + background-position: -535px -363px; +} +#feedbackbox-email { left: 90px; top: 48px; width: 356px; height: auto; } +#feedbackbox-message { left: 90px; top: 84px; width: 358px; height: 100px; } +#feedbackbox-response { position: absolute; bottom: 15px; left: 15px; + width: 390px; height: auto; font-size: 1.2em; display: none; } +#feedbackbox .goodresponse { font-weight: bold; color: green; } +#feedbackbox .badresponse { font-weight: bold; color: red; } +#feedbackbox p { margin-bottom: 1em; } +#feedbackbox ul { margin: 1em 0 1em 2em } +#feedbackbox li { padding: 0.3em 0; } +#feedbackbox li a { display: block; font-weight: bold; } +#feedbackbox li a:hover { background: #ffe; } +#feedbackbox a, #feedbackbox li a:visited { color: #47b; } +#feedbackbox tt { font-size: 110%; } + +.expand-collapse { + height: 22px; + background-image: url(/static/img/sharedistri.gif); + background-repeat: no-repeat; + background-position: 0 3px; + padding-left: 17px; + text-decoration: none; +} +.expand-collapse.expanded { + background-position: 0 -31px; +} + + +.modaldialog { + position: absolute; + top: 100px; + left:50%; + margin-left:-243px; + width: 485px; + display: none; + z-index: 501; + zoom: 1; + overflow: hidden; + background: white; + border: 1px solid #999; +} +.modaldialog .modaldialog-inner { padding: 10pt; } +.modaldialog .modaldialog-inner * { margin: 2pt; } +.modaldialog .modaldialog-hide { + float: right; + background-repeat: no-repeat; + background-image: url(/static/img/sharebox4.gif); + display: block; + width: 22px; height: 22px; + background-position: -454px -6px; + margin-right:-5px; + margin-top:-5px; +} + +.modaldialog label, +.modaldialog h1 { + color:#222222; + font-size:125%; + font-weight:bold; +} + +.modaldialog th { + vertical-align: top; + text-align: left; +} + +.nonprouser #sharebox-stripe { display: none; } + +.sharebox-url { + width: 440px; height: 18px; + text-align: left; + font-size: 1.3em; + line-height: 18px; + padding: 2px; +} + +#sharebox-send { + float: right; + background-repeat: no-repeat; + background-image: url(/static/img/sharebox4.gif); + display: block; + width: 87px; height: 22px; + background-position: -383px -289px; +} + + +#viewbarcontents { display: none; } +#viewzoomtitle { + position: absolute; left: 10px; top: 4px; height: 20px; line-height: 20px; + width: auto; +} +#viewzoommenu { + width: 65px; +} +#bottomarea { + height: 28px; + overflow: hidden; + position: absolute; + height: 28px; + bottom: 0px; + left: 0px; + right: 0px; + font-size: 1.2em; + color: #444; +} +#widthprefcheck { position: absolute; + background-image: url(/static/img/layoutbuttons.gif); + background-repeat: no-repeat; cursor: pointer; + width: 86px; height: 20px; top: 4px; right: 2px; } +.widthprefunchecked { background-position: -1px -1px; } +.widthprefchecked { background-position: -1px -23px; } +#sidebarcheck { position: absolute; + background-image: url(/static/img/layoutbuttons.gif); + background-repeat: no-repeat; cursor: pointer; + width: 86px; height: 20px; top: 4px; right: 90px; } +.sidebarunchecked { background-position: -1px -45px; } +.sidebarchecked { background-position: -1px -67px; } +#feedbackbutton { display: block; position: absolute; width: 68px; + height: 0; padding-top: 17px; overflow: hidden; + background: url(/static/img/bottomareagfx.gif); + top: 5px; right: 220px; +} + +#modaloverlay { + z-index: 500; display: none; + background-image: url(/static/img/overlay2.png); + background-repeat: repeat-both; + width: 100%; position: absolute; + height: 100%; left: 0; top: 0; +} + +* html #modaloverlay { /* for IE 6+ */ + opacity: 1; /* in case this is looked at */ + background-image: none; + background-repeat: no-repeat; + /* scale the image */ + filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src="/static/img/overlay2.png", sizingMethod="scale"); +} + +a.topbarmaximize { + float: right; + width: 16px; + height: 16px; + margin-right:-143px; + margin-top:4px; + background: url(/static/img/maximize_normal.png); +} + +body a.topbarmaximize { + background: url(/static/img/maximize_maximized.png); +} + +#editbarinner h1 { + line-height: 29px; + font-size: 16px; + padding-left: 6pt; + margin-top: 0; +} + +#editbarinner h1 a { + font-size: 12px; +} + +.bigbutton { + display: block; + background-color: #a3bde0; + color: #555555; + border-style: solid; + border-width: 2px; + border-left-color: #d6e2f1; + border-right-color: #86aee1; + border-top-color: #d6e2f1; + border-bottom-color: #86aee1; + margin: 10pt; + text-align: center; + text-decoration: none; + padding: 50pt; + font-size: 20pt; + -moz-border-radius-topleft: 3pt; + -moz-border-radius-topright: 3pt; + -moz-border-radius-bottomleft: 3pt; + -moz-border-radius-bottomright: 3pt; + -webkit-border-top-left-radius: 3pt; + -webkit-border-top-right-radius: 3pt; + -webkit-border-bottom-left-radius: 3pt; + -webkit-border-bottom-right-radius: 3pt; +} + +.modaldialog .bigbutton { + padding-left: 0; + padding-right: 0; + width: 100%; +} diff --git a/static/img/backgrad.gif b/static/img/backgrad.gif new file mode 100644 index 0000000000000000000000000000000000000000..8fee1a5b721cb0c0b0814d75dcc6798d373556dc GIT binary patch literal 697 zcmcJKTTfB}0Dw-FmOdbiu{ za=9E1hsWaqLC|0@Xti1dL2Nc#AQ15T{SXAn)K7z%}yN+s|gIJOb{ubQ##?I&RO z7OtrxHzTP9?0+ENUqe<8J;bwavdzPfq2$U*C#SODR1Z=Xz6;-%po^mCg=}khz3n9u zj&4;|*N|$T0`;$6H#9aiziDZ0t510QuCuHA{fEv&ePjytBaPnwX<)E#2ZK4x8ewy| zUwC{*oKPellSs$KNmJ8bWi#Kt&w_Hr)GkmI{K;}Ig z$feJ=9L;W+0jd4+jx++J=t(;X>N-=Z=*{B|z&%;T$IWuyAY!E8NUMAte@H{m?MN?H z@IMo1@icrNUcfLg346=z3IWr^zAbA=&c9w>$1Bq@ilI@KRak+MAK`=}c1dLt<8 literal 0 HcmV?d00001 diff --git a/static/img/crushed_button_undepressed.png b/static/img/crushed_button_undepressed.png new file mode 100644 index 0000000000000000000000000000000000000000..d86e3f39743bc7b5334c58442a000bd39f772b09 GIT binary patch literal 4166 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 QNB{r;07*qoM6N<$f)X$A_5c6? literal 0 HcmV?d00001 diff --git a/static/img/crushed_current_location.png b/static/img/crushed_current_location.png new file mode 100644 index 0000000000000000000000000000000000000000..76e08359e3cdef6b81bf1799de7d9d1333ced9e6 GIT binary patch literal 1009 zcmV4Tx07%D;lubw!VHn5%vyBL$L1eZG#n8bdR3?p50&~}|Y-L^6T}=^UoqczA za-5xJcGnb%5TaXI$LL~7bg+mH;h~^Y9U=tH>L5XMvIruGP=a!lflfp+G*2MVV=={I+u3*z&b%+znSNh? z@6wl(2Or&d>ig(^5$5psQ?64%fTIpj5x4n-$s;zOHF-RxrHF@3mJ*RDu|{00hk|Xy zcZv7J9X#UTC^ICWq?I@+X*%h+Du|mhQry&>Cl=MHtP-!9_NXC^Q4G!N`f2XMZZqWr zS`Gob-V|*^1~C5sI8a@*)wqHE1HeLK(Y7A6_QO>!8nMO(%fyv=si#lb?0Pj&dK=ih zmCgRTm(6b8ryA79OGQfSncR*z?k!;VYd@2IFi$(NJ(G2sfLYq(z;3{I4Y>0eIQkga z_Y4S>1EFTPt|EV>*k!9uTT+|Q<#-|`G>GCU!9%Z>3EiqxUn@iuMX+3kpfiK%85XU_ zn39%q_4d+L#?){h-Dy786=rngn46A9+G&@lC;l2`?H%TOBz_o^JG;#V(zP1XJHs|} z$K+IZ$Yxi+s`z_y>{dtoy*W)@OL>ENeIwJ?k+)xroC^?F5eG+;J~O70a$6@8p#hsY zWirv`&+$7w9W?76p_n;MvHtVao9h(@p@2b#O1~-u1knXA>YzbK5;5|~6d{w9Sur}X zfzdxkDhN1OApaWWtXVmikfv-is~=uJrfUldg7`uFF0P2P;+*(xrYc=i^i1m)WNGo{ z57IR&=XG=5Tu-*XM0GvnRS+lZSYal^(W#@mm^xEcR5EL@>;4VLl+0174kMP*s8uOr z`3(B?II8WmJ&Juj_1s;owZbn9FPFWZ+On(Uwy9WQs;$q(IA=cN5A(6$#NkC3M6+2gxWKJ~}`KYMUz8EMX|Kk@(Bqq`;~X>zMQMY^9n>Q4V> f7zLwX6p#V{MAqJ+{L9Q100000NkvXXu0mjf5AW6a literal 0 HcmV?d00001 diff --git a/static/img/editbar_background.gif b/static/img/editbar_background.gif new file mode 100644 index 0000000000000000000000000000000000000000..54ef6e48405e7643dcc57a58ce4d45bb2e1eb14e GIT binary patch literal 181 zcmZ?wbhEHbWMoibXpv%QTmN|0j{CEB+@AwPJMYikd4Jxn2lIhw_k#tyA1>VUaM9j} zi}yZUvhU&2{g0OIf3*C-;}r)VuRQp8)uG3$4?kXW`0?5!Pu3oNvhL`U>yJN`cCAE1 zivI+ii&7IyQd1PlGfOfQ+&z5*7!-f9Fmf@-Gw3h?0mvZ?tg;Fd5}%|-in{s+i8%H@ WHaW9MgR@A&&5?)MkwJio!5RQQ#91r= literal 0 HcmV?d00001 diff --git a/static/img/editbar_background_left.gif b/static/img/editbar_background_left.gif new file mode 100644 index 0000000000000000000000000000000000000000..fe8d06ec43c8573340c4237dfb89a65dd76bd47b GIT binary patch literal 204 zcmZ?wbhEHbWMWWdXpv@UTmN|0j{CEB+@AwPJMYikd4Jxn2lIhw_k#tyA1>VUaM9j} zi}yZUvhU&2{g0OIf3*C-;}r)VuRQp8)uG3$4?kXW`0?5!Pu3oNvhL`U>yJOZe*L<% zYb7#L{3qyKl$uzQnxasiS(2gP?&%xAp!k!8k&8i|L5BedKn`JGHCe#MAlWeU1#5(M w0HX+xPXmjKIRhjAgC7lS4U7>CBJK`M4Gc02Y#Iv~6d2gT8yEy6dYu@o0mOG*?f?J) literal 0 HcmV?d00001 diff --git a/static/img/editbar_background_right.gif b/static/img/editbar_background_right.gif new file mode 100644 index 0000000000000000000000000000000000000000..55ab00a6b22049936120abae0f65fdc4f7852647 GIT binary patch literal 867 zcmWmDNl#)20Kjq5Q*ZqMP1Do#)U?+&O=fO0?Wt*8lAcW6ml!2$SPJ%*pq>-F;lg4JrBoSYmVA7`;x91ds4k=+2(N_$pl zg2m>v%$D6Wr}_ISsWr1@Nh_c%Z$Bjj;a?XO41sBIa1g_=cDvnbwSvK5AP~5_y9*}E z$H&L9SghG>*6Vc?MIRm>qS5Ht*;zOoetLQeg+jGj&Fl4^pPyGMm5Ymu$H&Lh)6+(y zaesdwi9}9LPB2W|+1c6O-=9b%R#sM)mX_8s^=h@+)zuY`$1$IvX!3f+Z>&5+O-6D3Q)G|bbA9cZ0(*e&yhpHlT(q}wkQB=Xd@a+RBQJ- zYv^yWo+WoJZ-CIDt5UsFnf%dsQuJToZN6ym=FobBBeRoPTH(Jw$e&;cKnvt3mfoo? zkqw<+p!L#dG`7OMDuv$2pmMnkf*__)2$4p)RI=G@VzJm@Fw6r0(|>7aFfhMAU9P24 zDT+jk!EipGSMhk6Oa?&^E|;55r|0J8ip3(I&;N}=qiN0SZ1(Kz?83sr+S(dGAi!?- z&HDf%5uHxQWG&nyny@odtB=?Wxm`4&DEN>9fcpMR?;V}pDHe>=Q*SORU-`JdtUwi oY4hjG0wjFSZhz6J!5Zod^4G0_N(}0G!EKiw#CgroF$MSC|BV$>kN^Mx literal 0 HcmV?d00001 diff --git a/static/img/maximize_maximized.png b/static/img/maximize_maximized.png new file mode 100644 index 0000000000000000000000000000000000000000..d1ae8f87ea37063c4c903bfaf822d63b67927170 GIT binary patch literal 604 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 SGiuz(v7^V2_(F;d0RTHFq|GM) literal 0 HcmV?d00001 diff --git a/static/img/play.png b/static/img/play.png new file mode 100644 index 0000000000000000000000000000000000000000..19afe034f1dc8ba99d449a970d70e88c9e596c95 GIT binary patch literal 3017 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 U&g~@{0(_l^4=OOYFfdpH0MXt-F#rGn literal 0 HcmV?d00001 diff --git a/static/img/roundcorner_right.gif b/static/img/roundcorner_right.gif new file mode 100644 index 0000000000000000000000000000000000000000..97acfbf2e6b0d7a6876999603877bbdb61736e7e GIT binary patch literal 131 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 literal 0 HcmV?d00001 diff --git a/static/img/timeslider_background.png b/static/img/timeslider_background.png new file mode 100644 index 0000000000000000000000000000000000000000..faa45c68d92ca3c7580ac58e88b028cfd97b7b26 GIT binary patch literal 915 zcmV;E18n?>P)4Tx0C)j~RL^S@K@|QrZmG~B2wH0nvUrdpNm;9CMbtL^5n^i$+aIn^?(HA4aZWV5ov6ELTdbo0FI&wK{O>*+w4vx20?>!`FrQsdJlnHR>OPy zcd~b_n$otK2Za4V;76L-DzNVtaSB-y0*E}{p()372;bw_^6ZZ}PI-92wGS&j#91PI zKs7DSe@(bk%_Y-7gGe}(^>I=@oY#w#*Bu9GZf3^F5WP>3rn}7Ut74&?PWBFvy`A)a zPP5)V!Xd&78LdA?xQ(9mjMYElVd13a#D+Z_7&Y|xU=_C-srWU*6kiZcC!$nw*)9$7 zn6CX+@=AhmkT}X@VSsa5NKe;HZuq)~1$`#h6R+ZTR#D-3j}vF!)ZOnz+5)dI4jl{{ z44Mr{P!L4~VVJN`K!!XTF*LGrKO?IK8z<8w`3e3jI8lUGNUta*C8 zn(P`s>{pjD=7Kek#B;Fw@hxAK%$F&Q6vg9J^Xf~4by_hu-=A!MJ3Znq&n~srbFGPs zH&&aMXZ>nO`|hf|ljc?VPhR!${AbO?W8x_>CU%PFA&Hm8F7cAsOREdwU~R_;ot1_u z(ruCYB-LPGn!NQdT|ZlRy+(fw^-+`=%+gee_kY4FWHg<*4sZI8+sFJD270UUORdLHO0nA4V) z%{fwsET5CQ>B?eK%uw4yQc~9?*JVo2}ze(;aRcp*ceL#HUJSllrgm5wQKR zQu+C;QrUh^8rFfA`ftFz{YAidi-`aL010qNS#tmY3ljhU3ljkVnw%H_002u#L_t(2 z6=OVe=FEQvAYl9t1Rw^>j~_okGy@Zu9A!X)VPHdpQO~;s5=}AFUVs@Sxrg!JzkiU( p{rU4J*gqs`hbv~edi5$;I{<&781Gd6ss{i7002ovPDHLkV1kGIt9<|f literal 0 HcmV?d00001 diff --git a/static/img/timeslider_left.png b/static/img/timeslider_left.png new file mode 100644 index 0000000000000000000000000000000000000000..594d86b745bcfb5744a7f6a7dc16d30701556d78 GIT binary patch literal 1653 zcmb`Gdpy$%0L6cH7n6BK(JqQhX61Q3%QL2p%v#K=GK=KVq}4EHMv+%@V;+;ckEGI? zNg=O$N2ElGvS~H1;_7P1)x5jzzxVUG|J^^%Ip6cg`SWCBF-MfQ!?ptepo~U&;I=Yz zOD+)kEjMbbI{*M6X_UJ=7VYl77keS@OjK+H03^y?@f(QIaMRqhCDpt#fCR^=)F&1g zpo6$)k#B4_)QK@r@t#IxLHN(;VUq2i(tQopS3gd?Mo5rfDiyw`=~3sy&AqM!BjFM* zoLH%4y3kKL+F1Qr@G^B#VmC*Nql^N|7Y41D(!d{;dvG_^qheDIN3UN6uH960vo`V< z&teSa-7?54$Nr|y(sL0q zIw|@(%XGjbC3ZBZ?sw`6GDvZ?F?~t*d3?OzXvj4KWnZ)uk8{2u%qY#_$w9jP~zOF->I~l=i>P||JS9ww=NT5}-P7EZcNyhN}4(-}H+gF;iYBt=+(X^-4cqVBZ@amPQUn`3jGcmRqv6(e?>7_!9~E zW8+CojY!Fx4|B+OAijl)E))i3E2FS#NN`wb9o99|5oT#!f5GJ^uKbUM$yS#PqgyW$ zJs!9v+rD>dI(F-W<0jl7zR0}+a(&%x6TyG(`zdMV#fdr*URJ~_XSaMEJJkKDtQn>! z9C|iM_BoZpdP6A;I8uB+iQ2}Vg}KZIlvqxs+c3Mxb!!Wzp{%|VlRVAtN&m&q-j^>0 zTiT6sV(EvRCd>xR8ivWd>|8s1c}e6(G-^$HudQ&(f5rFI(!zRVf7j^=6U%*+oN4{+ zl5Vqw>mqNi(gfjL52S*XX)<{3msP{J1OmHl$^4{CNMgGu9lCU^Xm{VBU$%_ii3qo@ zXjX|^Wr};&wLZ#nra5qsdF0Gxndv3s`mkbm98PE?jPTy|u*{!0LlKn?>5_CR z9S+O7?s!EMuJRYu>o)3eD=YVLBdHkJFr#EOPSCc26eQ+mZyiAKJSvC`07@GFNDg4% z&<21XOwk^0eu;8lDtvO^VKp;-FRlwzTcS<WZ%-=;ymMXZ)Y$1!@xJed4_W%7nz3vue2OL!c9ngNAyCv+~ z0e2{Et!TmNk@iz_WZUwMV7MlnGjQBd&v@Vvr5U~L-t$RQB!ad2`mFUtwnuuS*i=^~ zYf>rYz0vu?=iyQWD`y1N71e&K)f?1H0~60kFPW7bBAKK(mBzCHg~{D>2Q3%^8b}BF zZcZi>&;Ja0m7gz{b$fJf=)C={Zem!7&W5@H!Skjc4_`A9d)?va_}U9zhc;Bc2Wwb} zwhHyL%v&1Il|t#XF?Be7ENLvvv)b8mvvj#-^JSjrPgV-QUCpYuZ;aoD^(Ys4e=BN_ zU=A3^SlIhdmzOQF2+l2FQOLLDlzRCLrMhbSp4*X|`f={t+*Re_irzp6#Jl&wCpBmf zRCT}Be-BD7((6V%Dl2e@Vt3#EwQ9kD2a?Ppyf z{F|GudHvQD3TLSVxqNm9^D|$b)Ozrn3?d+z5RQbX@-((dJmU|E@HG&0X!lJo z8t<$^nKMKen%AxLmV%HwF}@%-@o3C|Dj&*$~|>GRV&#m?rUq!>&L002o#oH=2?8T*)u z9N5pz#l}zo5L))dVC*b07`Rx=1zHWA@(UPzxYZg*9RM(Jtv#^PT(aFhp73ApoF8^HvVg5lJxVTFUfWbZ$_AkDV4 zr>}aJnI0ttHh)XenhU?j*wl`iuurnZk5!slynIl!k@nGa(tV}} zu;*KP?87%8_|v71!CXzrwY;ib=k-hRLA3L4iSAU&Wt{D>HF>KsS@@31FtmE|wzK_2 zL^{-Gbnwdx_LESxwv>VdOoA?nvx8v4?s+wKs3aszN0$|1dWw1AcxKeOTL`~bp&sVH zqoedc8`s;HeL)ITm4dS{FGL>k(E|1AZrkaI?RQseMw~aZDjD>~^>L$)>HG$m;_yJ% z6a{}hI(slI)8%4LMMPv1eGz85=#r~56R(%vPO15^rg0;?Cs!@)aL1I>w|T3?TbFeV z#)?U?M#htxeVQ+ZD3vLx2F{FJpItxPkLB{F%o(R`+v^)^d>>A`=cJmBO4$9`lPS3! znrn|nteE1HZULPlh1p4J{Q(&}h>vb=^rj81Yo=Gin=N7`Hte&G_4GTYY{s(HiMoXi zQh__^yiUH{*UkI0#i~MDr17r&#sG)l6-bHK6EdYrNUr6>oz*(B|KphReDE(wrfela zuNdA0k`GIcRQyXVZy+a!V@OCoTb$WA_tt~ZW5>e~2*b+5URJWT`A!~lVI%ni3S{{b zL$l5H-!6G&?o_YI)a=#}wzn$?+!!0!P+aa#U~AJZwlzF8WnTc%Ae<`&0K}pH0s`m- zNB4D@wlqgOhJ%(0jZ=9j2+Aerg6F!enRe34bI<$BI4wAaW|>W{5>{YczEH@%Yn7{3 zz~1`lY^>3zQ=M_4_2)k0U`r-(6BQphy-g9g)9zyyQ)^p(BXGFwc;9r$b~A73b-Kl4 zqvIxx-V8p{Tq2+9G#_ok;COq^d~0y6bS_S6o9I);I}pXWX<~lqrClQ~j}SBANP0Ih zd4G zO2tzd0IOkJTO9{*!S09N;sNgG=%YsvR8qXQ=(GxzL^eqiJA~3l=Ou#AUS|rpe;_Or1u`i8 zy~y%9C87flws#2gKa5huUiVcKj5Z0eI%8;+r)r?uIsZUKx@_ z;w=qnV#96ogv%C=aC&9aNX{&By+8Td&QpQ&mZ|3iGx~KlNx9ZX2jTy^*1j$%+Wo;+ z+r;~5Z>_Fzw6ft;$hJKMWpD1tz>1Pkyi`?Rq7HFg6Nz-?sp;#M+ownYqRP0}9gm>i_@% literal 0 HcmV?d00001 diff --git a/static/js/broadcast.js b/static/js/broadcast.js new file mode 100644 index 000000000..c150ebb66 --- /dev/null +++ b/static/js/broadcast.js @@ -0,0 +1,714 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var global = this; + +function loadBroadcastJS() +{ + // just in case... (todo: this must be somewhere else in the client code.) + // Below Array#map code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_map.htm + if (!Array.prototype.map) + { + Array.prototype.map = function (fun /*, thisp*/ ) + { + var len = this.length >>> 0; + if (typeof fun != "function") throw new TypeError(); + + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + { + if (i in this) res[i] = fun.call(thisp, this[i], i, this); + } + + return res; + }; + } + + // Below Array#forEach code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_foreach.htm + if (!Array.prototype.forEach) + { + Array.prototype.forEach = function (fun /*, thisp*/ ) + { + var len = this.length >>> 0; + if (typeof fun != "function") throw new TypeError(); + + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + { + if (i in this) fun.call(thisp, this[i], i, this); + } + }; + } + + // Below Array#indexOf code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_indexof.htm + if (!Array.prototype.indexOf) + { + Array.prototype.indexOf = function (elt /*, from*/ ) + { + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) from += len; + + for (; from < len; from++) + { + if (from in this && this[from] === elt) return from; + } + return -1; + }; + } + + function debugLog() + { + try + { + if(window.console) console.log.apply(console, arguments); + } + catch (e) + { + if(window.console) console.log("error printing: ", e); + } + } + + function randomString() + { + return "_" + Math.floor(Math.random() * 1000000); + } + + // for IE + if ($.browser.msie) + { + try + { + document.execCommand("BackgroundImageCache", false, true); + } + catch (e) + {} + } + + var userId = "hiddenUser" + randomString(); + var socketId; + var socket; + + var channelState = "DISCONNECTED"; + + var appLevelDisconnectReason = null; + + var padContents = { + currentRevision: clientVars.revNum, + currentTime: clientVars.currentTime, + currentLines: Changeset.splitTextLines(clientVars.initialStyledContents.atext.text), + currentDivs: null, + // to be filled in once the dom loads + apool: (new AttribPool()).fromJsonable(clientVars.initialStyledContents.apool), + alines: Changeset.splitAttributionLines( + clientVars.initialStyledContents.atext.attribs, clientVars.initialStyledContents.atext.text), + + // generates a jquery element containing HTML for a line + lineToElement: function (line, aline) + { + var element = document.createElement("div"); + var emptyLine = (line == '\n'); + var domInfo = domline.createDomLine(!emptyLine, true); + linestylefilter.populateDomLine(line, aline, this.apool, domInfo); + domInfo.prepareForAdd(); + element.className = domInfo.node.className; + element.innerHTML = domInfo.node.innerHTML; + element.id = Math.random(); + return $(element); + }, + + applySpliceToDivs: function (start, numRemoved, newLines) + { + // remove spliced-out lines from DOM + for (var i = start; i < start + numRemoved && i < this.currentDivs.length; i++) + { + debugLog("removing", this.currentDivs[i].attr('id')); + this.currentDivs[i].remove(); + } + + // remove spliced-out line divs from currentDivs array + this.currentDivs.splice(start, numRemoved); + + var newDivs = []; + for (var i = 0; i < newLines.length; i++) + { + newDivs.push(this.lineToElement(newLines[i], this.alines[start + i])); + } + + // grab the div just before the first one + var startDiv = this.currentDivs[start - 1] || null; + + // insert the div elements into the correct place, in the correct order + for (var i = 0; i < newDivs.length; i++) + { + if (startDiv) + { + startDiv.after(newDivs[i]); + } + else + { + $("#padcontent").prepend(newDivs[i]); + } + startDiv = newDivs[i]; + } + + // insert new divs into currentDivs array + newDivs.unshift(0); // remove 0 elements + newDivs.unshift(start); + this.currentDivs.splice.apply(this.currentDivs, newDivs); + return this; + }, + + // splice the lines + splice: function (start, numRemoved, newLinesVA) + { + var newLines = Array.prototype.slice.call(arguments, 2).map( + + function (s) + { + return s; + }); + + // apply this splice to the divs + this.applySpliceToDivs(start, numRemoved, newLines); + + // call currentLines.splice, to keep the currentLines array up to date + newLines.unshift(numRemoved); + newLines.unshift(start); + this.currentLines.splice.apply(this.currentLines, arguments); + }, + // returns the contents of the specified line I + get: function (i) + { + return this.currentLines[i]; + }, + // returns the number of lines in the document + length: function () + { + return this.currentLines.length; + }, + + getActiveAuthors: function () + { + var self = this; + var authors = []; + var seenNums = {}; + var alines = self.alines; + for (var i = 0; i < alines.length; i++) + { + Changeset.eachAttribNumber(alines[i], function (n) + { + if (!seenNums[n]) + { + seenNums[n] = true; + if (self.apool.getAttribKey(n) == 'author') + { + var a = self.apool.getAttribValue(n); + if (a) + { + authors.push(a); + } + } + } + }); + } + authors.sort(); + return authors; + } + }; + + function callCatchingErrors(catcher, func) + { + try + { + wrapRecordingErrors(catcher, func)(); + } + catch (e) + { /*absorb*/ + } + } + + function wrapRecordingErrors(catcher, func) + { + return function () + { + try + { + return func.apply(this, Array.prototype.slice.call(arguments)); + } + catch (e) + { + // caughtErrors.push(e); + // caughtErrorCatchers.push(catcher); + // caughtErrorTimes.push(+new Date()); + // console.dir({catcher: catcher, e: e}); + debugLog(e); // TODO(kroo): added temporary, to catch errors + throw e; + } + }; + } + + function loadedNewChangeset(changesetForward, changesetBackward, revision, timeDelta) + { + var broadcasting = (BroadcastSlider.getSliderPosition() == revisionInfo.latest); + debugLog("broadcasting:", broadcasting, BroadcastSlider.getSliderPosition(), revisionInfo.latest, revision); + revisionInfo.addChangeset(revision, revision + 1, changesetForward, changesetBackward, timeDelta); + BroadcastSlider.setSliderLength(revisionInfo.latest); + if (broadcasting) applyChangeset(changesetForward, revision + 1, false, timeDelta); + } + + /* + At this point, we must be certain that the changeset really does map from + the current revision to the specified revision. Any mistakes here will + cause the whole slider to get out of sync. + */ + + function applyChangeset(changeset, revision, preventSliderMovement, timeDelta) + { + // disable the next 'gotorevision' call handled by a timeslider update + if (!preventSliderMovement) + { + goToRevisionIfEnabledCount++; + BroadcastSlider.setSliderPosition(revision); + } + + try + { + // must mutate attribution lines before text lines + Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool); + } + catch (e) + { + debugLog(e); + } + + Changeset.mutateTextLines(changeset, padContents); + padContents.currentRevision = revision; + padContents.currentTime += timeDelta * 1000; + debugLog('Time Delta: ', timeDelta) + updateTimer(); + BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function (name) + { + return authorData[name]; + })); + } + + function updateTimer() + { + var zpad = function (str, length) + { + str = str + ""; + while (str.length < length) + str = '0' + str; + return str; + } + + var date = new Date(padContents.currentTime); + var dateFormat = function () + { + var month = zpad(date.getMonth() + 1, 2); + var day = zpad(date.getDate(), 2); + var year = (date.getFullYear()); + var hours = zpad(date.getHours(), 2); + var minutes = zpad(date.getMinutes(), 2); + var seconds = zpad(date.getSeconds(), 2); + return ([month, '/', day, '/', year, ' ', hours, ':', minutes, ':', seconds].join("")); + } + + + + $('#timer').html(dateFormat()); + + var revisionDate = ["Saved", ["Jan", "Feb", "March", "April", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"][date.getMonth()], date.getDate() + ",", date.getFullYear()].join(" ") + $('#revision_date').html(revisionDate) + + } + + function goToRevision(newRevision) + { + padContents.targetRevision = newRevision; + var self = this; + var path = revisionInfo.getPath(padContents.currentRevision, newRevision); + debugLog('newRev: ', padContents.currentRevision, path); + if (path.status == 'complete') + { + var cs = path.changesets; + debugLog("status: complete, changesets: ", cs, "path:", path); + var changeset = cs[0]; + var timeDelta = path.times[0]; + for (var i = 1; i < cs.length; i++) + { + changeset = Changeset.compose(changeset, cs[i], padContents.apool); + timeDelta += path.times[i]; + } + if (changeset) applyChangeset(changeset, path.rev, true, timeDelta); + } + else if (path.status == "partial") + { + debugLog('partial'); + var sliderLocation = padContents.currentRevision; + // callback is called after changeset information is pulled from server + // this may never get called, if the changeset has already been loaded + var update = function (start, end) + { + // if we've called goToRevision in the time since, don't goToRevision + goToRevision(padContents.targetRevision); + }; + + // do our best with what we have... + var cs = path.changesets; + + var changeset = cs[0]; + var timeDelta = path.times[0]; + for (var i = 1; i < cs.length; i++) + { + changeset = Changeset.compose(changeset, cs[i], padContents.apool); + timeDelta += path.times[i]; + } + if (changeset) applyChangeset(changeset, path.rev, true, timeDelta); + + + if (BroadcastSlider.getSliderLength() > 10000) + { + var start = (Math.floor((newRevision) / 10000) * 10000); // revision 0 to 10 + changesetLoader.queueUp(start, 100); + } + + if (BroadcastSlider.getSliderLength() > 1000) + { + var start = (Math.floor((newRevision) / 1000) * 1000); // (start from -1, go to 19) + 1 + changesetLoader.queueUp(start, 10); + } + + start = (Math.floor((newRevision) / 100) * 100); + + changesetLoader.queueUp(start, 1, update); + } + BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function (name) + { + return authorData[name]; + })); + } + + var changesetLoader = { + running: false, + resolved: [], + requestQueue1: [], + requestQueue2: [], + requestQueue3: [], + queueUp: function (revision, width, callback) + { + if (revision < 0) revision = 0; + // if(changesetLoader.requestQueue.indexOf(revision) != -1) + // return; // already in the queue. + if (changesetLoader.resolved.indexOf(revision + "_" + width) != -1) return; // already loaded from the server + changesetLoader.resolved.push(revision + "_" + width); + + var requestQueue = width == 1 ? changesetLoader.requestQueue3 : width == 10 ? changesetLoader.requestQueue2 : changesetLoader.requestQueue1; + requestQueue.push( + { + 'rev': revision, + 'res': width, + 'callback': callback + }); + if (!changesetLoader.running) + { + changesetLoader.running = true; + setTimeout(changesetLoader.loadFromQueue, 10); + } + }, + loadFromQueue: function () + { + var self = changesetLoader; + var requestQueue = self.requestQueue1.length > 0 ? self.requestQueue1 : self.requestQueue2.length > 0 ? self.requestQueue2 : self.requestQueue3.length > 0 ? self.requestQueue3 : null; + + if (!requestQueue) + { + self.running = false; + return; + } + + var request = requestQueue.pop(); + var granularity = request.res; + var callback = request.callback; + var start = request.rev; + debugLog("loadinging revision", start, "through ajax"); + $.getJSON("/ep/pad/changes/" + clientVars.padIdForUrl + "?s=" + start + "&g=" + granularity, function (data, textStatus) + { + if (textStatus !== "success") + { + console.log(textStatus); + BroadcastSlider.showReconnectUI(); + } + self.handleResponse(data, start, granularity, callback); + + setTimeout(self.loadFromQueue, 10); // load the next ajax function + }); + }, + handleResponse: function (data, start, granularity, callback) + { + debugLog("response: ", data); + var pool = (new AttribPool()).fromJsonable(data.apool); + for (var i = 0; i < data.forwardsChangesets.length; i++) + { + var astart = start + i * granularity - 1; // rev -1 is a blank single line + var aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision + if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1; + debugLog("adding changeset:", astart, aend); + var forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool); + var backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool); + revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]); + } + if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1); + } + }; + + function handleMessageFromServer() + { + debugLog("handleMessage:", arguments); + var obj = arguments[0]['data']; + var expectedType = "COLLABROOM"; + + obj = JSON.parse(obj); + if (obj['type'] == expectedType) + { + obj = obj['data']; + + if (obj['type'] == "NEW_CHANGES") + { + debugLog(obj); + var changeset = Changeset.moveOpsToNewPool( + obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); + + var changesetBack = Changeset.moveOpsToNewPool( + obj.changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); + + loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta); + } + else if (obj['type'] == "NEW_AUTHORDATA") + { + var authorMap = {}; + authorMap[obj.author] = obj.data; + receiveAuthorData(authorMap); + BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function (name) + { + return authorData[name]; + })); + } + else if (obj['type'] == "NEW_SAVEDREV") + { + var savedRev = obj.savedRev; + BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev); + } + } + else + { + debugLog("incorrect message type: " + obj['type'] + ", expected " + expectedType); + } + } + + function handleSocketClosed(params) + { + debugLog("socket closed!", params); + socket = null; + + BroadcastSlider.showReconnectUI(); + // var reason = appLevelDisconnectReason || params.reason; + // var shouldReconnect = params.reconnect; + // if (shouldReconnect) { + // // determine if this is a tight reconnect loop due to weird connectivity problems + // // reconnectTimes.push(+new Date()); + // var TOO_MANY_RECONNECTS = 8; + // var TOO_SHORT_A_TIME_MS = 10000; + // if (reconnectTimes.length >= TOO_MANY_RECONNECTS && + // ((+new Date()) - reconnectTimes[reconnectTimes.length-TOO_MANY_RECONNECTS]) < + // TOO_SHORT_A_TIME_MS) { + // setChannelState("DISCONNECTED", "looping"); + // } + // else { + // setChannelState("RECONNECTING", reason); + // setUpSocket(); + // } + // } + // else { + // BroadcastSlider.showReconnectUI(); + // setChannelState("DISCONNECTED", reason); + // } + } + + function sendMessage(msg) + { + socket.postMessage(JSON.stringify( + { + type: "COLLABROOM", + data: msg + })); + } + + function setUpSocket() + { + // required for Comet + if ((!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) + { + document.domain = document.domain; // for comet + } + + var success = false; + callCatchingErrors("setUpSocket", function () + { + appLevelDisconnectReason = null; + + socketId = String(Math.floor(Math.random() * 1e12)); + socket = new WebSocket(socketId); + socket.onmessage = wrapRecordingErrors("socket.onmessage", handleMessageFromServer); + socket.onclosed = wrapRecordingErrors("socket.onclosed", handleSocketClosed); + socket.onopen = wrapRecordingErrors("socket.onopen", function () + { + setChannelState("CONNECTED"); + var msg = { + type: "CLIENT_READY", + roomType: 'padview', + roomName: 'padview/' + clientVars.viewId, + data: { + lastRev: clientVars.revNum, + userInfo: { + userId: userId + } + } + }; + sendMessage(msg); + }); + // socket.onhiccup = wrapRecordingErrors("socket.onhiccup", handleCometHiccup); + // socket.onlogmessage = function(x) {debugLog(x); }; + socket.connect(); + success = true; + }); + if (success) + { + //initialStartConnectTime = +new Date(); + } + else + { + abandonConnection("initsocketfail"); + } + } + + function setChannelState(newChannelState, moreInfo) + { + if (newChannelState != channelState) + { + channelState = newChannelState; + // callbacks.onChannelStateChange(channelState, moreInfo); + } + } + + function abandonConnection(reason) + { + if (socket) + { + socket.onclosed = function () + {}; + socket.onhiccup = function () + {}; + socket.disconnect(); + } + socket = null; + setChannelState("DISCONNECTED", reason); + } + + /*window['onloadFuncts'] = []; + window.onload = function () + { + window['isloaded'] = true; + window['onloadFuncts'].forEach(function (funct) + { + funct(); + }); + };*/ + + // to start upon window load, just push a function onto this array + //window['onloadFuncts'].push(setUpSocket); + //window['onloadFuncts'].push(function () + fireWhenAllScriptsAreLoaded.push(function () + { + // set up the currentDivs and DOM + padContents.currentDivs = []; + $("#padcontent").html(""); + for (var i = 0; i < padContents.currentLines.length; i++) + { + var div = padContents.lineToElement(padContents.currentLines[i], padContents.alines[i]); + padContents.currentDivs.push(div); + $("#padcontent").append(div); + } + debugLog(padContents.currentDivs); + }); + + // this is necessary to keep infinite loops of events firing, + // since goToRevision changes the slider position + var goToRevisionIfEnabledCount = 0; + var goToRevisionIfEnabled = function () + { + if (goToRevisionIfEnabledCount > 0) + { + goToRevisionIfEnabledCount--; + } + else + { + goToRevision.apply(goToRevision, arguments); + } + } + + + + BroadcastSlider.onSlider(goToRevisionIfEnabled); + + (function () + { + for (var i = 0; i < clientVars.initialChangesets.length; i++) + { + var csgroup = clientVars.initialChangesets[i]; + var start = clientVars.initialChangesets[i].start; + var granularity = clientVars.initialChangesets[i].granularity; + debugLog("loading changest on startup: ", start, granularity, csgroup); + changesetLoader.handleResponse(csgroup, start, granularity, null); + } + })(); + + var dynamicCSS = makeCSSManager('dynamicsyntax'); + var authorData = {}; + + function receiveAuthorData(newAuthorData) + { + for (var author in newAuthorData) + { + var data = newAuthorData[author]; + if ((typeof data.colorId) == 'number') + { + var bgcolor = clientVars.colorPalette[data.colorId]; + if (bgcolor && dynamicCSS) + { + dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author)).backgroundColor = bgcolor; + } + } + authorData[author] = data; + } + } + + receiveAuthorData(clientVars.historicalAuthorData); +} diff --git a/static/js/broadcast_revisions.js b/static/js/broadcast_revisions.js new file mode 100644 index 000000000..31a14b385 --- /dev/null +++ b/static/js/broadcast_revisions.js @@ -0,0 +1,133 @@ +/** + * 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. + +var global = this; + +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.totalRevs || -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 + }; + } +} + + // revisionInfo.addChangeset(0, 5, "abcde") + // revisionInfo.addChangeset(5, 10, "fghij") + // revisionInfo.addChangeset(10, 11, "k") + // revisionInfo.addChangeset(11, 12, "l") + // revisionInfo.addChangeset(12, 13, "m") + // revisionInfo.addChangeset(13, 14, "n") + // revisionInfo.addChangeset(14, 15, "o") + // revisionInfo.addChangeset(15, 20, "pqrst") + // + // print (revisionInfo.getPath(15, 0)) diff --git a/static/js/broadcast_slider.js b/static/js/broadcast_slider.js new file mode 100644 index 000000000..209667703 --- /dev/null +++ b/static/js/broadcast_slider.js @@ -0,0 +1,489 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var global = this; + +function loadBroadcastSliderJS() +{ + + (function () + { // wrap this code in its own namespace + var sliderLength = 1000; + var sliderPos = 0; + var sliderActive = false; + var slidercallbacks = []; + var savedRevisions = []; + var sliderPlaying = false; + + function disableSelection(element) + { + element.onselectstart = function () + { + return false; + }; + element.unselectable = "on"; + 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; + $("#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("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 + + + function showReconnectUI() + { + if (!clientVars.sliderEnabled || !clientVars.supportsSlider) + { + $("#padmain, #rightbars").css('top', "130px"); + $("#timeslider").show(); + } + $('#error').show(); + } + + function setAuthors(authors) + { + $("#authorstable").empty(); + var numAnonymous = 0; + var numNamed = 0; + authors.forEach(function (author) + { + if (author.name) + { + numNamed++; + var tr = $(''); + var swatchtd = $(''); + var swatch = $('
'); + swatch.css('background-color', clientVars.colorPalette[author.colorId]); + swatchtd.append(swatch); + tr.append(swatchtd); + var nametd = $(''); + nametd.text(author.name || "unnamed"); + tr.append(nametd); + $("#authorstable").append(tr); + } + else + { + numAnonymous++; + } + }); + if (numAnonymous > 0) + { + var html = "" + (numNamed > 0 ? "...and " : "") + numAnonymous + " unnamed author" + (numAnonymous > 1 ? "s" : "") + ""; + $("#authorstable").append($(html)); + } + if (authors.length == 0) + { + $("#authorstable").append($("No Authors")) + } + } + + global.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 () + fireWhenAllScriptsAreLoaded.push(function () + { + disableSelection($("#playpause_button")[0]); + disableSelection($("#timeslider")[0]); + + if (clientVars.sliderEnabled && clientVars.supportsSlider) + { + $(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("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))) + self.currentLoc = parseInt($(self).css('left')); + }); + }) + + // play/pause toggling + $("#playpause_button").mousedown(function (evt) + { + var self = this; + + $(self).css('background-image', 'url(/static/img/pad/timeslider/crushed_button_depressed.png)'); + $(self).mouseup(function (evt2) + { + $(self).css('background-image', 'url(/static/img/pad/timeslider/crushed_button_undepressed.png)'); + $(self).unbind('mouseup'); + BroadcastSlider.playpause(); + }); + $(document).mouseup(function (evt2) + { + $(self).css('background-image', 'url(/static/img/pad/timeslider/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) + { + if (clientVars.fullWidth) + { + $("#padpage").css('width', '100%'); + $("#revision").css('position', "absolute") + $("#revision").css('right', "20px") + $("#revision").css('top', "20px") + $("#padmain").css('left', '0px'); + $("#padmain").css('right', '197px'); + $("#padmain").css('width', 'auto'); + $("#rightbars").css('right', '7px'); + $("#rightbars").css('margin-right', '0px'); + $("#timeslider").css('width', 'auto'); + } + + if (clientVars.disableRightBar) + { + $("#rightbars").css('display', 'none'); + $('#padmain').css('width', 'auto'); + if (clientVars.fullWidth) $("#padmain").css('right', '7px'); + else $("#padmain").css('width', '860px'); + $("#revision").css('position', "absolute"); + $("#revision").css('right', "20px"); + $("#revision").css('top', "20px"); + } + + + if (clientVars.sliderEnabled) + { + if (clientVars.supportsSlider) + { + $("#padmain, #rightbars").css('top', "130px"); + $("#timeslider").show(); + setSliderLength(clientVars.totalRevs); + setSliderPosition(clientVars.revNum); + clientVars.savedRevisions.forEach(function (revision) + { + addSavedRevision(revision.revNum, revision); + }) + } + else + { + // slider is not supported + $("#padmain, #rightbars").css('top', "130px"); + $("#timeslider").show(); + $("#error").html("The timeslider feature is not supported on this pad. Why not?"); + $("#error").show(); + } + } + else + { + if (clientVars.supportsSlider) + { + setSliderLength(clientVars.totalRevs); + setSliderPosition(clientVars.revNum); + } + } + } + }); + })(); + + BroadcastSlider.onSlider(function (loc) + { + $("#viewlatest").html(loc == BroadcastSlider.getSliderLength() ? "Viewing latest content" : "View latest content"); + }) +} diff --git a/static/js/collab_client.js b/static/js/collab_client.js index 5606d656c..af3275943 100644 --- a/static/js/collab_client.js +++ b/static/js/collab_client.js @@ -270,8 +270,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options) { } function sendMessage(msg) { - //socket.postMessage(JSON.stringify({type: "COLLABROOM", data: msg})); - socket.send(JSON.stringify({type: "COLLABROOM", data: msg})); + socket.send({type: "COLLABROOM", component: "pad", data: msg}); } function wrapRecordingErrors(catcher, func) { diff --git a/static/js/cssmanager_client.js b/static/js/cssmanager_client.js new file mode 100644 index 000000000..893790f11 --- /dev/null +++ b/static/js/cssmanager_client.js @@ -0,0 +1,111 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/cssmanager.js +/** + * 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. + */ + +function makeCSSManager(emptyStylesheetTitle) +{ + + function getSheetByTitle(title) + { + var allSheets = document.styleSheets; + for (var i = 0; i < allSheets.length; i++) + { + var s = allSheets[i]; + if (s.title == title) + { + return s; + } + } + return null; + } + +/*function getSheetTagByTitle(title) { + var allStyleTags = document.getElementsByTagName("style"); + for(var i=0;i= 0) + { + browserDeleteRule(i); + selectorList.splice(i, 1); + } + } + + return { + selectorStyle: selectorStyle, + removeSelectorStyle: removeSelectorStyle, + info: function () + { + return selectorList.length + ":" + browserRules().length; + } + }; +} diff --git a/static/js/domline_client.js b/static/js/domline_client.js new file mode 100644 index 000000000..24eb42c08 --- /dev/null +++ b/static/js/domline_client.js @@ -0,0 +1,292 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/domline.js +// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline +/** + * 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. + */ +// requires: top +// requires: plugins +// requires: undefined +var domline = {}; +domline.noop = function () +{}; +domline.identity = function (x) +{ + return x; +}; + +domline.addToLineClass = function (lineClass, cls) +{ + // an "empty span" at any point can be used to add classes to + // the line, using line:className. otherwise, we ignore + // the span. + cls.replace(/\S+/g, function (c) + { + if (c.indexOf("line:") == 0) + { + // add class to line + lineClass = (lineClass ? lineClass + ' ' : '') + c.substring(5); + } + }); + return lineClass; +} + +// if "document" is falsy we don't create a DOM node, just +// an object with innerHTML and className +domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) +{ + var result = { + node: null, + appendSpan: domline.noop, + prepareForAdd: domline.noop, + notifyAdded: domline.noop, + clearSpans: domline.noop, + finishUpdate: domline.noop, + lineMarker: 0 + }; + + var browser = (optBrowser || {}); + var document = optDocument; + + if (document) + { + result.node = document.createElement("div"); + } + else + { + result.node = { + innerHTML: '', + className: '' + }; + } + + var html = []; + var preHtml, postHtml; + var curHTML = null; + + function processSpaces(s) + { + return domline.processSpaces(s, doesWrap); + } + var identity = domline.identity; + var perTextNodeProcess = (doesWrap ? identity : processSpaces); + var perHtmlLineProcess = (doesWrap ? processSpaces : identity); + var lineClass = 'ace-line'; + result.appendSpan = function (txt, cls) + { + if (cls.indexOf('list') >= 0) + { + var listType = /(?:^| )list:(\S+)/.exec(cls); + if (listType) + { + listType = listType[1]; + if (listType) + { + preHtml = '
  • '; + postHtml = '
'; + } + result.lineMarker += txt.length; + return; // don't append any text + } + } + var href = null; + var simpleTags = null; + if (cls.indexOf('url') >= 0) + { + cls = cls.replace(/(^| )url:(\S+)/g, function (x0, space, url) + { + href = url; + return space + "url"; + }); + } + if (cls.indexOf('tag') >= 0) + { + cls = cls.replace(/(^| )tag:(\S+)/g, function (x0, space, tag) + { + if (!simpleTags) simpleTags = []; + simpleTags.push(tag.toLowerCase()); + return space + tag; + }); + } + + var extraOpenTags = ""; + var extraCloseTags = ""; + + var plugins_; + if (typeof (plugins) != 'undefined') + { + plugins_ = plugins; + } + else + { + plugins_ = parent.parent.plugins; + } + + plugins_.callHook("aceCreateDomLine", { + domline: domline, + cls: cls, + document: document + }).map(function (modifier) + { + cls = modifier.cls; + extraOpenTags = extraOpenTags + modifier.extraOpenTags; + extraCloseTags = modifier.extraCloseTags + extraCloseTags; + }); + + if ((!txt) && cls) + { + lineClass = domline.addToLineClass(lineClass, cls); + } + else if (txt) + { + if (href) + { + extraOpenTags = extraOpenTags + ''; + extraCloseTags = '' + extraCloseTags; + } + if (simpleTags) + { + simpleTags.sort(); + extraOpenTags = extraOpenTags + '<' + simpleTags.join('><') + '>'; + simpleTags.reverse(); + extraCloseTags = '' + extraCloseTags; + } + html.push('', extraOpenTags, perTextNodeProcess(domline.escapeHTML(txt)), extraCloseTags, ''); + } + }; + result.clearSpans = function () + { + html = []; + lineClass = ''; // non-null to cause update + result.lineMarker = 0; + }; + + function writeHTML() + { + var newHTML = perHtmlLineProcess(html.join('')); + if (!newHTML) + { + if ((!document) || (!optBrowser)) + { + newHTML += ' '; + } + else if (!browser.msie) + { + newHTML += '
'; + } + } + if (nonEmpty) + { + newHTML = (preHtml || '') + newHTML + (postHtml || ''); + } + html = preHtml = postHtml = null; // free memory + if (newHTML !== curHTML) + { + curHTML = newHTML; + result.node.innerHTML = curHTML; + } + if (lineClass !== null) result.node.className = lineClass; + } + result.prepareForAdd = writeHTML; + result.finishUpdate = writeHTML; + result.getInnerHTML = function () + { + return curHTML || ''; + }; + + return result; +}; + +domline.escapeHTML = function (s) +{ + var re = /[&<>'"]/g; + /']/; // stupid indentation thing + if (!re.MAP) + { + // persisted across function calls! + re.MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + } + return s.replace(re, function (c) + { + return re.MAP[c]; + }); +}; + +domline.processSpaces = function (s, doesWrap) +{ + if (s.indexOf("<") < 0 && !doesWrap) + { + // short-cut + return s.replace(/ /g, ' '); + } + var parts = []; + s.replace(/<[^>]*>?| |[^ <]+/g, function (m) + { + parts.push(m); + }); + if (doesWrap) + { + var endOfLine = true; + var beforeSpace = false; + // last space in a run is normal, others are nbsp, + // end of line is nbsp + for (var i = parts.length - 1; i >= 0; i--) + { + var p = parts[i]; + if (p == " ") + { + if (endOfLine || beforeSpace) parts[i] = ' '; + endOfLine = false; + beforeSpace = true; + } + else if (p.charAt(0) != "<") + { + endOfLine = false; + beforeSpace = false; + } + } + // beginning of line is nbsp + for (var i = 0; i < parts.length; i++) + { + var p = parts[i]; + if (p == " ") + { + parts[i] = ' '; + break; + } + else if (p.charAt(0) != "<") + { + break; + } + } + } + else + { + for (var i = 0; i < parts.length; i++) + { + var p = parts[i]; + if (p == " ") + { + parts[i] = ' '; + } + } + } + return parts.join(''); +}; diff --git a/static/js/draggable.js b/static/js/draggable.js new file mode 100644 index 000000000..7f7d9bb1e --- /dev/null +++ b/static/js/draggable.js @@ -0,0 +1,189 @@ +/** + * 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. + */ + +function makeDraggable(jqueryNodes, eventHandler) +{ + jqueryNodes.each(function () + { + var node = $(this); + var state = {}; + var inDrag = false; + + function dragStart(evt) + { + if (inDrag) + { + return; + } + inDrag = true; + if (eventHandler('dragstart', evt, state) !== false) + { + $(document).bind('mousemove', dragUpdate); + $(document).bind('mouseup', dragEnd); + } + evt.preventDefault(); + return false; + } + + function dragUpdate(evt) + { + if (!inDrag) + { + return; + } + eventHandler('dragupdate', evt, state); + evt.preventDefault(); + return false; + } + + function dragEnd(evt) + { + if (!inDrag) + { + return; + } + inDrag = false; + try + { + eventHandler('dragend', evt, state); + } + finally + { + $(document).unbind('mousemove', dragUpdate); + $(document).unbind('mouseup', dragEnd); + evt.preventDefault(); + } + return false; + } + node.bind('mousedown', dragStart); + }); +} + +function makeResizableVPane(top, sep, bottom, minTop, minBottom, callback) +{ + if (minTop === undefined) minTop = 0; + if (minBottom === undefined) minBottom = 0; + + makeDraggable($(sep), function (eType, evt, state) + { + if (eType == 'dragstart') + { + state.startY = evt.pageY; + state.topHeight = $(top).height(); + state.bottomHeight = $(bottom).height(); + state.minTop = minTop; + state.maxTop = (state.topHeight + state.bottomHeight) - minBottom; + } + else if (eType == 'dragupdate') + { + var change = evt.pageY - state.startY; + + var topHeight = state.topHeight + change; + if (topHeight < state.minTop) + { + topHeight = state.minTop; + } + if (topHeight > state.maxTop) + { + topHeight = state.maxTop; + } + change = topHeight - state.topHeight; + + var bottomHeight = state.bottomHeight - change; + var sepHeight = $(sep).height(); + + var totalHeight = topHeight + sepHeight + bottomHeight; + topHeight = 100.0 * topHeight / totalHeight; + sepHeight = 100.0 * sepHeight / totalHeight; + bottomHeight = 100.0 * bottomHeight / totalHeight; + + $(top).css('bottom', 'auto'); + $(top).css('height', topHeight + "%"); + $(sep).css('top', topHeight + "%"); + $(bottom).css('top', (topHeight + sepHeight) + '%'); + $(bottom).css('height', 'auto'); + if (callback) callback(); + } + }); +} + +function makeResizableHPane(left, sep, right, minLeft, minRight, sepWidth, sepOffset, callback) +{ + if (minLeft === undefined) minLeft = 0; + if (minRight === undefined) minRight = 0; + + makeDraggable($(sep), function (eType, evt, state) + { + if (eType == 'dragstart') + { + state.startX = evt.pageX; + state.leftWidth = $(left).width(); + state.rightWidth = $(right).width(); + state.minLeft = minLeft; + state.maxLeft = (state.leftWidth + state.rightWidth) - minRight; + } + else if (eType == 'dragend' || eType == 'dragupdate') + { + var change = evt.pageX - state.startX; + + var leftWidth = state.leftWidth + change; + if (leftWidth < state.minLeft) + { + leftWidth = state.minLeft; + } + if (leftWidth > state.maxLeft) + { + leftWidth = state.maxLeft; + } + change = leftWidth - state.leftWidth; + + var rightWidth = state.rightWidth - change; + newSepWidth = sepWidth; + if (newSepWidth == undefined) newSepWidth = $(sep).width(); + newSepOffset = sepOffset; + if (newSepOffset == undefined) newSepOffset = 0; + + if (change == 0) + { + if (rightWidth != minRight || state.lastRightWidth == undefined) + { + state.lastRightWidth = rightWidth; + rightWidth = minRight; + } + else + { + rightWidth = state.lastRightWidth; + state.lastRightWidth = minRight; + } + change = state.rightWidth - rightWidth; + leftWidth = change + state.leftWidth; + } + + var totalWidth = leftWidth + newSepWidth + rightWidth; + leftWidth = 100.0 * leftWidth / totalWidth; + newSepWidth = 100.0 * newSepWidth / totalWidth; + newSepOffset = 100.0 * newSepOffset / totalWidth; + rightWidth = 100.0 * rightWidth / totalWidth; + + $(left).css('right', 'auto'); + $(left).css('width', leftWidth + "%"); + $(sep).css('left', (leftWidth + newSepOffset) + "%"); + $(right).css('left', (leftWidth + newSepWidth) + '%'); + $(right).css('width', 'auto'); + if (callback) callback(); + } + }); +} diff --git a/static/js/easysync2_client.js b/static/js/easysync2_client.js new file mode 100644 index 000000000..ca5ee3daf --- /dev/null +++ b/static/js/easysync2_client.js @@ -0,0 +1,2269 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/easysync2.js +// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.easysync2 +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +//var _opt = (this.Easysync2Support || null); +var _opt = null; // disable optimization for now + +function AttribPool() +{ + var p = {}; + p.numToAttrib = {}; // e.g. {0: ['foo','bar']} + p.attribToNum = {}; // e.g. {'foo,bar': 0} + p.nextNum = 0; + + p.putAttrib = function (attrib, dontAddIfAbsent) + { + var str = String(attrib); + if (str in p.attribToNum) + { + return p.attribToNum[str]; + } + if (dontAddIfAbsent) + { + return -1; + } + var num = p.nextNum++; + p.attribToNum[str] = num; + p.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; + return num; + }; + + p.getAttrib = function (num) + { + var pair = p.numToAttrib[num]; + if (!pair) return pair; + return [pair[0], pair[1]]; // return a mutable copy + }; + + p.getAttribKey = function (num) + { + var pair = p.numToAttrib[num]; + if (!pair) return ''; + return pair[0]; + }; + + p.getAttribValue = function (num) + { + var pair = p.numToAttrib[num]; + if (!pair) return ''; + return pair[1]; + }; + + p.eachAttrib = function (func) + { + for (var n in p.numToAttrib) + { + var pair = p.numToAttrib[n]; + func(pair[0], pair[1]); + } + }; + + p.toJsonable = function () + { + return { + numToAttrib: p.numToAttrib, + nextNum: p.nextNum + }; + }; + + p.fromJsonable = function (obj) + { + p.numToAttrib = obj.numToAttrib; + p.nextNum = obj.nextNum; + p.attribToNum = {}; + for (var n in p.numToAttrib) + { + p.attribToNum[String(p.numToAttrib[n])] = Number(n); + } + return p; + }; + + return p; +} + +var Changeset = {}; + +Changeset.error = function error(msg) +{ + var e = new Error(msg); + e.easysync = true; + throw e; +}; +Changeset.assert = function assert(b, msgParts) +{ + if (!b) + { + var msg = Array.prototype.slice.call(arguments, 1).join(''); + Changeset.error("Changeset: " + msg); + } +}; + +Changeset.parseNum = function (str) +{ + return parseInt(str, 36); +}; +Changeset.numToString = function (num) +{ + return num.toString(36).toLowerCase(); +}; +Changeset.toBaseTen = function (cs) +{ + var dollarIndex = cs.indexOf('$'); + var beforeDollar = cs.substring(0, dollarIndex); + var fromDollar = cs.substring(dollarIndex); + return beforeDollar.replace(/[0-9a-z]+/g, function (s) + { + return String(Changeset.parseNum(s)); + }) + fromDollar; +}; + +Changeset.oldLen = function (cs) +{ + return Changeset.unpack(cs).oldLen; +}; +Changeset.newLen = function (cs) +{ + return Changeset.unpack(cs).newLen; +}; + +Changeset.opIterator = function (opsStr, optStartIndex) +{ + //print(opsStr); + var regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; + var startIndex = (optStartIndex || 0); + var curIndex = startIndex; + var prevIndex = curIndex; + + function nextRegexMatch() + { + prevIndex = curIndex; + var result; + if (_opt) + { + result = _opt.nextOpInString(opsStr, curIndex); + if (result) + { + if (result.opcode() == '?') + { + Changeset.error("Hit error opcode in op stream"); + } + curIndex = result.lastIndex(); + } + } + else + { + regex.lastIndex = curIndex; + result = regex.exec(opsStr); + curIndex = regex.lastIndex; + if (result[0] == '?') + { + Changeset.error("Hit error opcode in op stream"); + } + } + return result; + } + var regexResult = nextRegexMatch(); + var obj = Changeset.newOp(); + + function next(optObj) + { + var op = (optObj || obj); + if (_opt && regexResult) + { + op.attribs = regexResult.attribs(); + op.lines = regexResult.lines(); + op.chars = regexResult.chars(); + op.opcode = regexResult.opcode(); + regexResult = nextRegexMatch(); + } + else if ((!_opt) && regexResult[0]) + { + op.attribs = regexResult[1]; + op.lines = Changeset.parseNum(regexResult[2] || 0); + op.opcode = regexResult[3]; + op.chars = Changeset.parseNum(regexResult[4]); + regexResult = nextRegexMatch(); + } + else + { + Changeset.clearOp(op); + } + return op; + } + + function hasNext() + { + return !!(_opt ? regexResult : regexResult[0]); + } + + function lastIndex() + { + return prevIndex; + } + return { + next: next, + hasNext: hasNext, + lastIndex: lastIndex + }; +}; + +Changeset.clearOp = function (op) +{ + op.opcode = ''; + op.chars = 0; + op.lines = 0; + op.attribs = ''; +}; +Changeset.newOp = function (optOpcode) +{ + return { + opcode: (optOpcode || ''), + chars: 0, + lines: 0, + attribs: '' + }; +}; +Changeset.cloneOp = function (op) +{ + return { + opcode: op.opcode, + chars: op.chars, + lines: op.lines, + attribs: op.attribs + }; +}; +Changeset.copyOp = function (op1, op2) +{ + op2.opcode = op1.opcode; + op2.chars = op1.chars; + op2.lines = op1.lines; + op2.attribs = op1.attribs; +}; +Changeset.opString = function (op) +{ + // just for debugging + if (!op.opcode) return 'null'; + var assem = Changeset.opAssembler(); + assem.append(op); + return assem.toString(); +}; +Changeset.stringOp = function (str) +{ + // just for debugging + return Changeset.opIterator(str).next(); +}; + +Changeset.checkRep = function (cs) +{ + // doesn't check things that require access to attrib pool (e.g. attribute order) + // or original string (e.g. newline positions) + var unpacked = Changeset.unpack(cs); + var oldLen = unpacked.oldLen; + var newLen = unpacked.newLen; + var ops = unpacked.ops; + var charBank = unpacked.charBank; + + var assem = Changeset.smartOpAssembler(); + var oldPos = 0; + var calcNewLen = 0; + var numInserted = 0; + var iter = Changeset.opIterator(ops); + while (iter.hasNext()) + { + var o = iter.next(); + switch (o.opcode) + { + case '=': + oldPos += o.chars; + calcNewLen += o.chars; + break; + case '-': + oldPos += o.chars; + Changeset.assert(oldPos < oldLen, oldPos, " >= ", oldLen, " in ", cs); + break; + case '+': + { + calcNewLen += o.chars; + numInserted += o.chars; + Changeset.assert(calcNewLen < newLen, calcNewLen, " >= ", newLen, " in ", cs); + break; + } + } + assem.append(o); + } + + calcNewLen += oldLen - oldPos; + charBank = charBank.substring(0, numInserted); + while (charBank.length < numInserted) + { + charBank += "?"; + } + + assem.endDocument(); + var normalized = Changeset.pack(oldLen, calcNewLen, assem.toString(), charBank); + Changeset.assert(normalized == cs, normalized, ' != ', cs); + + return cs; +} + +Changeset.smartOpAssembler = function () +{ + // Like opAssembler but able to produce conforming changesets + // from slightly looser input, at the cost of speed. + // Specifically: + // - merges consecutive operations that can be merged + // - strips final "=" + // - ignores 0-length changes + // - reorders consecutive + and - (which margingOpAssembler doesn't do) + var minusAssem = Changeset.mergingOpAssembler(); + var plusAssem = Changeset.mergingOpAssembler(); + var keepAssem = Changeset.mergingOpAssembler(); + var assem = Changeset.stringAssembler(); + var lastOpcode = ''; + var lengthChange = 0; + + function flushKeeps() + { + assem.append(keepAssem.toString()); + keepAssem.clear(); + } + + function flushPlusMinus() + { + assem.append(minusAssem.toString()); + minusAssem.clear(); + assem.append(plusAssem.toString()); + plusAssem.clear(); + } + + function append(op) + { + if (!op.opcode) return; + if (!op.chars) return; + + if (op.opcode == '-') + { + if (lastOpcode == '=') + { + flushKeeps(); + } + minusAssem.append(op); + lengthChange -= op.chars; + } + else if (op.opcode == '+') + { + if (lastOpcode == '=') + { + flushKeeps(); + } + plusAssem.append(op); + lengthChange += op.chars; + } + else if (op.opcode == '=') + { + if (lastOpcode != '=') + { + flushPlusMinus(); + } + keepAssem.append(op); + } + lastOpcode = op.opcode; + } + + function appendOpWithText(opcode, text, attribs, pool) + { + var op = Changeset.newOp(opcode); + op.attribs = Changeset.makeAttribsString(opcode, attribs, pool); + var lastNewlinePos = text.lastIndexOf('\n'); + if (lastNewlinePos < 0) + { + op.chars = text.length; + op.lines = 0; + append(op); + } + else + { + op.chars = lastNewlinePos + 1; + op.lines = text.match(/\n/g).length; + append(op); + op.chars = text.length - (lastNewlinePos + 1); + op.lines = 0; + append(op); + } + } + + function toString() + { + flushPlusMinus(); + flushKeeps(); + return assem.toString(); + } + + function clear() + { + minusAssem.clear(); + plusAssem.clear(); + keepAssem.clear(); + assem.clear(); + lengthChange = 0; + } + + function endDocument() + { + keepAssem.endDocument(); + } + + function getLengthChange() + { + return lengthChange; + } + + return { + append: append, + toString: toString, + clear: clear, + endDocument: endDocument, + appendOpWithText: appendOpWithText, + getLengthChange: getLengthChange + }; +}; + +if (_opt) +{ + Changeset.mergingOpAssembler = function () + { + var assem = _opt.mergingOpAssembler(); + + function append(op) + { + assem.append(op.opcode, op.chars, op.lines, op.attribs); + } + + function toString() + { + return assem.toString(); + } + + function clear() + { + assem.clear(); + } + + function endDocument() + { + assem.endDocument(); + } + + return { + append: append, + toString: toString, + clear: clear, + endDocument: endDocument + }; + }; +} +else +{ + Changeset.mergingOpAssembler = function () + { + // This assembler can be used in production; it efficiently + // merges consecutive operations that are mergeable, ignores + // no-ops, and drops final pure "keeps". It does not re-order + // operations. + var assem = Changeset.opAssembler(); + var bufOp = Changeset.newOp(); + + // If we get, for example, insertions [xxx\n,yyy], those don't merge, + // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. + // This variable stores the length of yyy and any other newline-less + // ops immediately after it. + var bufOpAdditionalCharsAfterNewline = 0; + + function flush(isEndDocument) + { + if (bufOp.opcode) + { + if (isEndDocument && bufOp.opcode == '=' && !bufOp.attribs) + { + // final merged keep, leave it implicit + } + else + { + assem.append(bufOp); + if (bufOpAdditionalCharsAfterNewline) + { + bufOp.chars = bufOpAdditionalCharsAfterNewline; + bufOp.lines = 0; + assem.append(bufOp); + bufOpAdditionalCharsAfterNewline = 0; + } + } + bufOp.opcode = ''; + } + } + + function append(op) + { + if (op.chars > 0) + { + if (bufOp.opcode == op.opcode && bufOp.attribs == op.attribs) + { + if (op.lines > 0) + { + // bufOp and additional chars are all mergeable into a multi-line op + bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; + bufOp.lines += op.lines; + bufOpAdditionalCharsAfterNewline = 0; + } + else if (bufOp.lines == 0) + { + // both bufOp and op are in-line + bufOp.chars += op.chars; + } + else + { + // append in-line text to multi-line bufOp + bufOpAdditionalCharsAfterNewline += op.chars; + } + } + else + { + flush(); + Changeset.copyOp(op, bufOp); + } + } + } + + function endDocument() + { + flush(true); + } + + function toString() + { + flush(); + return assem.toString(); + } + + function clear() + { + assem.clear(); + Changeset.clearOp(bufOp); + } + return { + append: append, + toString: toString, + clear: clear, + endDocument: endDocument + }; + }; +} + +if (_opt) +{ + Changeset.opAssembler = function () + { + var assem = _opt.opAssembler(); + // this function allows op to be mutated later (doesn't keep a ref) + + + function append(op) + { + assem.append(op.opcode, op.chars, op.lines, op.attribs); + } + + function toString() + { + return assem.toString(); + } + + function clear() + { + assem.clear(); + } + return { + append: append, + toString: toString, + clear: clear + }; + }; +} +else +{ + Changeset.opAssembler = function () + { + var pieces = []; + // this function allows op to be mutated later (doesn't keep a ref) + + + function append(op) + { + pieces.push(op.attribs); + if (op.lines) + { + pieces.push('|', Changeset.numToString(op.lines)); + } + pieces.push(op.opcode); + pieces.push(Changeset.numToString(op.chars)); + } + + function toString() + { + return pieces.join(''); + } + + function clear() + { + pieces.length = 0; + } + return { + append: append, + toString: toString, + clear: clear + }; + }; +} + +Changeset.stringIterator = function (str) +{ + var curIndex = 0; + + function assertRemaining(n) + { + Changeset.assert(n <= remaining(), "!(", n, " <= ", remaining(), ")"); + } + + function take(n) + { + assertRemaining(n); + var s = str.substr(curIndex, n); + curIndex += n; + return s; + } + + function peek(n) + { + assertRemaining(n); + var s = str.substr(curIndex, n); + return s; + } + + function skip(n) + { + assertRemaining(n); + curIndex += n; + } + + function remaining() + { + return str.length - curIndex; + } + return { + take: take, + skip: skip, + remaining: remaining, + peek: peek + }; +}; + +Changeset.stringAssembler = function () +{ + var pieces = []; + + function append(x) + { + pieces.push(String(x)); + } + + function toString() + { + return pieces.join(''); + } + return { + append: append, + toString: toString + }; +}; + +// "lines" need not be an array as long as it supports certain calls (lines_foo inside). +Changeset.textLinesMutator = function (lines) +{ + // Mutates lines, an array of strings, in place. + // Mutation operations have the same constraints as changeset operations + // with respect to newlines, but not the other additional constraints + // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline). + // Can be used to mutate lists of strings where the last char of each string + // is not actually a newline, but for the purposes of N and L values, + // the caller should pretend it is, and for things to work right in that case, the input + // to insert() should be a single line with no newlines. + var curSplice = [0, 0]; + var inSplice = false; + // position in document after curSplice is applied: + var curLine = 0, + curCol = 0; + // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && + // curLine >= curSplice[0] + // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then + // curCol == 0 + + function lines_applySplice(s) + { + lines.splice.apply(lines, s); + } + + function lines_toSource() + { + return lines.toSource(); + } + + function lines_get(idx) + { + if (lines.get) + { + return lines.get(idx); + } + else + { + return lines[idx]; + } + } + // can be unimplemented if removeLines's return value not needed + + + function lines_slice(start, end) + { + if (lines.slice) + { + return lines.slice(start, end); + } + else + { + return []; + } + } + + function lines_length() + { + if ((typeof lines.length) == "number") + { + return lines.length; + } + else + { + return lines.length(); + } + } + + function enterSplice() + { + curSplice[0] = curLine; + curSplice[1] = 0; + if (curCol > 0) + { + putCurLineInSplice(); + } + inSplice = true; + } + + function leaveSplice() + { + lines_applySplice(curSplice); + curSplice.length = 2; + curSplice[0] = curSplice[1] = 0; + inSplice = false; + } + + function isCurLineInSplice() + { + return (curLine - curSplice[0] < (curSplice.length - 2)); + } + + function debugPrint(typ) + { + print(typ + ": " + curSplice.toSource() + " / " + curLine + "," + curCol + " / " + lines_toSource()); + } + + function putCurLineInSplice() + { + if (!isCurLineInSplice()) + { + curSplice.push(lines_get(curSplice[0] + curSplice[1])); + curSplice[1]++; + } + return 2 + curLine - curSplice[0]; + } + + function skipLines(L, includeInSplice) + { + if (L) + { + if (includeInSplice) + { + if (!inSplice) + { + enterSplice(); + } + for (var i = 0; i < L; i++) + { + curCol = 0; + putCurLineInSplice(); + curLine++; + } + } + else + { + if (inSplice) + { + if (L > 1) + { + leaveSplice(); + } + else + { + putCurLineInSplice(); + } + } + curLine += L; + curCol = 0; + } + //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); +/*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { + print("BLAH"); + putCurLineInSplice(); + }*/ + // tests case foo in remove(), which isn't otherwise covered in current impl + } + //debugPrint("skip"); + } + + function skip(N, L, includeInSplice) + { + if (N) + { + if (L) + { + skipLines(L, includeInSplice); + } + else + { + if (includeInSplice && !inSplice) + { + enterSplice(); + } + if (inSplice) + { + putCurLineInSplice(); + } + curCol += N; + //debugPrint("skip"); + } + } + } + + function removeLines(L) + { + var removed = ''; + if (L) + { + if (!inSplice) + { + enterSplice(); + } + + function nextKLinesText(k) + { + var m = curSplice[0] + curSplice[1]; + return lines_slice(m, m + k).join(''); + } + if (isCurLineInSplice()) + { + //print(curCol); + if (curCol == 0) + { + removed = curSplice[curSplice.length - 1]; + // print("FOO"); // case foo + curSplice.length--; + removed += nextKLinesText(L - 1); + curSplice[1] += L - 1; + } + else + { + removed = nextKLinesText(L - 1); + curSplice[1] += L - 1; + var sline = curSplice.length - 1; + removed = curSplice[sline].substring(curCol) + removed; + curSplice[sline] = curSplice[sline].substring(0, curCol) + lines_get(curSplice[0] + curSplice[1]); + curSplice[1] += 1; + } + } + else + { + removed = nextKLinesText(L); + curSplice[1] += L; + } + //debugPrint("remove"); + } + return removed; + } + + function remove(N, L) + { + var removed = ''; + if (N) + { + if (L) + { + return removeLines(L); + } + else + { + if (!inSplice) + { + enterSplice(); + } + var sline = putCurLineInSplice(); + removed = curSplice[sline].substring(curCol, curCol + N); + curSplice[sline] = curSplice[sline].substring(0, curCol) + curSplice[sline].substring(curCol + N); + //debugPrint("remove"); + } + } + return removed; + } + + function insert(text, L) + { + if (text) + { + if (!inSplice) + { + enterSplice(); + } + if (L) + { + var newLines = Changeset.splitTextLines(text); + if (isCurLineInSplice()) + { + //if (curCol == 0) { + //curSplice.length--; + //curSplice[1]--; + //Array.prototype.push.apply(curSplice, newLines); + //curLine += newLines.length; + //} + //else { + var sline = curSplice.length - 1; + var theLine = curSplice[sline]; + var lineCol = curCol; + curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; + curLine++; + newLines.splice(0, 1); + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + curSplice.push(theLine.substring(lineCol)); + curCol = 0; + //} + } + else + { + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + } + } + else + { + var sline = putCurLineInSplice(); + curSplice[sline] = curSplice[sline].substring(0, curCol) + text + curSplice[sline].substring(curCol); + curCol += text.length; + } + //debugPrint("insert"); + } + } + + function hasMore() + { + //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); + var docLines = lines_length(); + if (inSplice) + { + docLines += curSplice.length - 2 - curSplice[1]; + } + return curLine < docLines; + } + + function close() + { + if (inSplice) + { + leaveSplice(); + } + //debugPrint("close"); + } + + var self = { + skip: skip, + remove: remove, + insert: insert, + close: close, + hasMore: hasMore, + removeLines: removeLines, + skipLines: skipLines + }; + return self; +}; + +Changeset.applyZip = function (in1, idx1, in2, idx2, func) +{ + var iter1 = Changeset.opIterator(in1, idx1); + var iter2 = Changeset.opIterator(in2, idx2); + var assem = Changeset.smartOpAssembler(); + var op1 = Changeset.newOp(); + var op2 = Changeset.newOp(); + var opOut = Changeset.newOp(); + while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) + { + if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1); + if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2); + func(op1, op2, opOut); + if (opOut.opcode) + { + //print(opOut.toSource()); + assem.append(opOut); + opOut.opcode = ''; + } + } + assem.endDocument(); + return assem.toString(); +}; + +Changeset.unpack = function (cs) +{ + var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; + var headerMatch = headerRegex.exec(cs); + if ((!headerMatch) || (!headerMatch[0])) + { + Changeset.error("Not a changeset: " + cs); + } + var oldLen = Changeset.parseNum(headerMatch[1]); + var changeSign = (headerMatch[2] == '>') ? 1 : -1; + var changeMag = Changeset.parseNum(headerMatch[3]); + var newLen = oldLen + changeSign * changeMag; + var opsStart = headerMatch[0].length; + var opsEnd = cs.indexOf("$"); + if (opsEnd < 0) opsEnd = cs.length; + return { + oldLen: oldLen, + newLen: newLen, + ops: cs.substring(opsStart, opsEnd), + charBank: cs.substring(opsEnd + 1) + }; +}; + +Changeset.pack = function (oldLen, newLen, opsStr, bank) +{ + var lenDiff = newLen - oldLen; + var lenDiffStr = (lenDiff >= 0 ? '>' + Changeset.numToString(lenDiff) : '<' + Changeset.numToString(-lenDiff)); + var a = []; + a.push('Z:', Changeset.numToString(oldLen), lenDiffStr, opsStr, '$', bank); + return a.join(''); +}; + +Changeset.applyToText = function (cs, str) +{ + var unpacked = Changeset.unpack(cs); + Changeset.assert(str.length == unpacked.oldLen, "mismatched apply: ", str.length, " / ", unpacked.oldLen); + var csIter = Changeset.opIterator(unpacked.ops); + var bankIter = Changeset.stringIterator(unpacked.charBank); + var strIter = Changeset.stringIterator(str); + var assem = Changeset.stringAssembler(); + while (csIter.hasNext()) + { + var op = csIter.next(); + switch (op.opcode) + { + case '+': + assem.append(bankIter.take(op.chars)); + break; + case '-': + strIter.skip(op.chars); + break; + case '=': + assem.append(strIter.take(op.chars)); + break; + } + } + assem.append(strIter.take(strIter.remaining())); + return assem.toString(); +}; + +Changeset.mutateTextLines = function (cs, lines) +{ + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var bankIter = Changeset.stringIterator(unpacked.charBank); + var mut = Changeset.textLinesMutator(lines); + while (csIter.hasNext()) + { + var op = csIter.next(); + switch (op.opcode) + { + case '+': + mut.insert(bankIter.take(op.chars), op.lines); + break; + case '-': + mut.remove(op.chars, op.lines); + break; + case '=': + mut.skip(op.chars, op.lines, ( !! op.attribs)); + break; + } + } + mut.close(); +}; + +Changeset.composeAttributes = function (att1, att2, resultIsMutation, pool) +{ + // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. + // Sometimes attribute (key,value) pairs are treated as attribute presence + // information, while other times they are treated as operations that + // mutate a set of attributes, and this affects whether an empty value + // is a deletion or a change. + // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result + // ([], [(bold, )], true) -> [(bold, )] + // ([], [(bold, )], false) -> [] + // ([], [(bold, true)], true) -> [(bold, true)] + // ([], [(bold, true)], false) -> [(bold, true)] + // ([(bold, true)], [(bold, )], true) -> [(bold, )] + // ([(bold, true)], [(bold, )], false) -> [] + // pool can be null if att2 has no attributes. + if ((!att1) && resultIsMutation) + { + // In the case of a mutation (i.e. composing two changesets), + // an att2 composed with an empy att1 is just att2. If att1 + // is part of an attribution string, then att2 may remove + // attributes that are already gone, so don't do this optimization. + return att2; + } + if (!att2) return att1; + var atts = []; + att1.replace(/\*([0-9a-z]+)/g, function (_, a) + { + atts.push(pool.getAttrib(Changeset.parseNum(a))); + return ''; + }); + att2.replace(/\*([0-9a-z]+)/g, function (_, a) + { + var pair = pool.getAttrib(Changeset.parseNum(a)); + var found = false; + for (var i = 0; i < atts.length; i++) + { + var oldPair = atts[i]; + if (oldPair[0] == pair[0]) + { + if (pair[1] || resultIsMutation) + { + oldPair[1] = pair[1]; + } + else + { + atts.splice(i, 1); + } + found = true; + break; + } + } + if ((!found) && (pair[1] || resultIsMutation)) + { + atts.push(pair); + } + return ''; + }); + atts.sort(); + var buf = Changeset.stringAssembler(); + for (var i = 0; i < atts.length; i++) + { + buf.append('*'); + buf.append(Changeset.numToString(pool.putAttrib(atts[i]))); + } + //print(att1+" / "+att2+" / "+buf.toString()); + return buf.toString(); +}; + +Changeset._slicerZipperFunc = function (attOp, csOp, opOut, pool) +{ + // attOp is the op from the sequence that is being operated on, either an + // attribution string or the earlier of two changesets being composed. + // pool can be null if definitely not needed. + //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + if (attOp.opcode == '-') + { + Changeset.copyOp(attOp, opOut); + attOp.opcode = ''; + } + else if (!attOp.opcode) + { + Changeset.copyOp(csOp, opOut); + csOp.opcode = ''; + } + else + { + switch (csOp.opcode) + { + case '-': + { + if (csOp.chars <= attOp.chars) + { + // delete or delete part + if (attOp.opcode == '=') + { + opOut.opcode = '-'; + opOut.chars = csOp.chars; + opOut.lines = csOp.lines; + opOut.attribs = ''; + } + attOp.chars -= csOp.chars; + attOp.lines -= csOp.lines; + csOp.opcode = ''; + if (!attOp.chars) + { + attOp.opcode = ''; + } + } + else + { + // delete and keep going + if (attOp.opcode == '=') + { + opOut.opcode = '-'; + opOut.chars = attOp.chars; + opOut.lines = attOp.lines; + opOut.attribs = ''; + } + csOp.chars -= attOp.chars; + csOp.lines -= attOp.lines; + attOp.opcode = ''; + } + break; + } + case '+': + { + // insert + Changeset.copyOp(csOp, opOut); + csOp.opcode = ''; + break; + } + case '=': + { + if (csOp.chars <= attOp.chars) + { + // keep or keep part + opOut.opcode = attOp.opcode; + opOut.chars = csOp.chars; + opOut.lines = csOp.lines; + opOut.attribs = Changeset.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); + csOp.opcode = ''; + attOp.chars -= csOp.chars; + attOp.lines -= csOp.lines; + if (!attOp.chars) + { + attOp.opcode = ''; + } + } + else + { + // keep and keep going + opOut.opcode = attOp.opcode; + opOut.chars = attOp.chars; + opOut.lines = attOp.lines; + opOut.attribs = Changeset.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); + attOp.opcode = ''; + csOp.chars -= attOp.chars; + csOp.lines -= attOp.lines; + } + break; + } + case '': + { + Changeset.copyOp(attOp, opOut); + attOp.opcode = ''; + break; + } + } + } +}; + +Changeset.applyToAttribution = function (cs, astr, pool) +{ + var unpacked = Changeset.unpack(cs); + + return Changeset.applyZip(astr, 0, unpacked.ops, 0, function (op1, op2, opOut) + { + return Changeset._slicerZipperFunc(op1, op2, opOut, pool); + }); +}; + +/*Changeset.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { + var iter = Changeset.opIterator(opsStr, optStartIndex); + var bankIndex = 0; + +};*/ + +Changeset.mutateAttributionLines = function (cs, lines, pool) +{ + //dmesg(cs); + //dmesg(lines.toSource()+" ->"); + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var csBank = unpacked.charBank; + var csBankIndex = 0; + // treat the attribution lines as text lines, mutating a line at a time + var mut = Changeset.textLinesMutator(lines); + + var lineIter = null; + + function isNextMutOp() + { + return (lineIter && lineIter.hasNext()) || mut.hasMore(); + } + + function nextMutOp(destOp) + { + if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) + { + var line = mut.removeLines(1); + lineIter = Changeset.opIterator(line); + } + if (lineIter && lineIter.hasNext()) + { + lineIter.next(destOp); + } + else + { + destOp.opcode = ''; + } + } + var lineAssem = null; + + function outputMutOp(op) + { + //print("outputMutOp: "+op.toSource()); + if (!lineAssem) + { + lineAssem = Changeset.mergingOpAssembler(); + } + lineAssem.append(op); + if (op.lines > 0) + { + Changeset.assert(op.lines == 1, "Can't have op.lines of ", op.lines, " in attribution lines"); + // ship it to the mut + mut.insert(lineAssem.toString(), 1); + lineAssem = null; + } + } + + var csOp = Changeset.newOp(); + var attOp = Changeset.newOp(); + var opOut = Changeset.newOp(); + while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) + { + if ((!csOp.opcode) && csIter.hasNext()) + { + csIter.next(csOp); + } + //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); + //print("csOp: "+csOp.toSource()); + if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) + { + break; // done + } + else if (csOp.opcode == '=' && csOp.lines > 0 && (!csOp.attribs) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) + { + // skip multiple lines; this is what makes small changes not order of the document size + mut.skipLines(csOp.lines); + //print("skipped: "+csOp.lines); + csOp.opcode = ''; + } + else if (csOp.opcode == '+') + { + if (csOp.lines > 1) + { + var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; + Changeset.copyOp(csOp, opOut); + csOp.chars -= firstLineLen; + csOp.lines--; + opOut.lines = 1; + opOut.chars = firstLineLen; + } + else + { + Changeset.copyOp(csOp, opOut); + csOp.opcode = ''; + } + outputMutOp(opOut); + csBankIndex += opOut.chars; + opOut.opcode = ''; + } + else + { + if ((!attOp.opcode) && isNextMutOp()) + { + nextMutOp(attOp); + } + //print("attOp: "+attOp.toSource()); + Changeset._slicerZipperFunc(attOp, csOp, opOut, pool); + if (opOut.opcode) + { + outputMutOp(opOut); + opOut.opcode = ''; + } + } + } + + Changeset.assert(!lineAssem, "line assembler not finished"); + mut.close(); + + //dmesg("-> "+lines.toSource()); +}; + +Changeset.joinAttributionLines = function (theAlines) +{ + var assem = Changeset.mergingOpAssembler(); + for (var i = 0; i < theAlines.length; i++) + { + var aline = theAlines[i]; + var iter = Changeset.opIterator(aline); + while (iter.hasNext()) + { + assem.append(iter.next()); + } + } + return assem.toString(); +}; + +Changeset.splitAttributionLines = function (attrOps, text) +{ + var iter = Changeset.opIterator(attrOps); + var assem = Changeset.mergingOpAssembler(); + var lines = []; + var pos = 0; + + function appendOp(op) + { + assem.append(op); + if (op.lines > 0) + { + lines.push(assem.toString()); + assem.clear(); + } + pos += op.chars; + } + + while (iter.hasNext()) + { + var op = iter.next(); + var numChars = op.chars; + var numLines = op.lines; + while (numLines > 1) + { + var newlineEnd = text.indexOf('\n', pos) + 1; + Changeset.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines"); + op.chars = newlineEnd - pos; + op.lines = 1; + appendOp(op); + numChars -= op.chars; + numLines -= op.lines; + } + if (numLines == 1) + { + op.chars = numChars; + op.lines = 1; + } + appendOp(op); + } + + return lines; +}; + +Changeset.splitTextLines = function (text) +{ + return text.match(/[^\n]*(?:\n|[^\n]$)/g); +}; + +Changeset.compose = function (cs1, cs2, pool) +{ + var unpacked1 = Changeset.unpack(cs1); + var unpacked2 = Changeset.unpack(cs2); + var len1 = unpacked1.oldLen; + var len2 = unpacked1.newLen; + Changeset.assert(len2 == unpacked2.oldLen, "mismatched composition"); + var len3 = unpacked2.newLen; + var bankIter1 = Changeset.stringIterator(unpacked1.charBank); + var bankIter2 = Changeset.stringIterator(unpacked2.charBank); + var bankAssem = Changeset.stringAssembler(); + + var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) + { + //var debugBuilder = Changeset.stringAssembler(); + //debugBuilder.append(Changeset.opString(op1)); + //debugBuilder.append(','); + //debugBuilder.append(Changeset.opString(op2)); + //debugBuilder.append(' / '); + var op1code = op1.opcode; + var op2code = op2.opcode; + if (op1code == '+' && op2code == '-') + { + bankIter1.skip(Math.min(op1.chars, op2.chars)); + } + Changeset._slicerZipperFunc(op1, op2, opOut, pool); + if (opOut.opcode == '+') + { + if (op2code == '+') + { + bankAssem.append(bankIter2.take(opOut.chars)); + } + else + { + bankAssem.append(bankIter1.take(opOut.chars)); + } + } + + //debugBuilder.append(Changeset.opString(op1)); + //debugBuilder.append(','); + //debugBuilder.append(Changeset.opString(op2)); + //debugBuilder.append(' -> '); + //debugBuilder.append(Changeset.opString(opOut)); + //print(debugBuilder.toString()); + }); + + return Changeset.pack(len1, len3, newOps, bankAssem.toString()); +}; + +Changeset.attributeTester = function (attribPair, pool) +{ + // returns a function that tests if a string of attributes + // (e.g. *3*4) contains a given attribute key,value that + // is already present in the pool. + if (!pool) + { + return never; + } + var attribNum = pool.putAttrib(attribPair, true); + if (attribNum < 0) + { + return never; + } + else + { + var re = new RegExp('\\*' + Changeset.numToString(attribNum) + '(?!\\w)'); + return function (attribs) + { + return re.test(attribs); + }; + } + + function never(attribs) + { + return false; + } +}; + +Changeset.identity = function (N) +{ + return Changeset.pack(N, N, "", ""); +}; + +Changeset.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) +{ + var oldLen = oldFullText.length; + + if (spliceStart >= oldLen) + { + spliceStart = oldLen - 1; + } + if (numRemoved > oldFullText.length - spliceStart - 1) + { + numRemoved = oldFullText.length - spliceStart - 1; + } + var oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved); + var newLen = oldLen + newText.length - oldText.length; + + var assem = Changeset.smartOpAssembler(); + assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); + assem.appendOpWithText('-', oldText); + assem.appendOpWithText('+', newText, optNewTextAPairs, pool); + assem.endDocument(); + return Changeset.pack(oldLen, newLen, assem.toString(), newText); +}; + +Changeset.toSplices = function (cs) +{ + // get a list of splices, [startChar, endChar, newText] + var unpacked = Changeset.unpack(cs); + var splices = []; + + var oldPos = 0; + var iter = Changeset.opIterator(unpacked.ops); + var charIter = Changeset.stringIterator(unpacked.charBank); + var inSplice = false; + while (iter.hasNext()) + { + var op = iter.next(); + if (op.opcode == '=') + { + oldPos += op.chars; + inSplice = false; + } + else + { + if (!inSplice) + { + splices.push([oldPos, oldPos, ""]); + inSplice = true; + } + if (op.opcode == '-') + { + oldPos += op.chars; + splices[splices.length - 1][1] += op.chars; + } + else if (op.opcode == '+') + { + splices[splices.length - 1][2] += charIter.take(op.chars); + } + } + } + + return splices; +}; + +Changeset.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter) +{ + var newStartChar = startChar; + var newEndChar = endChar; + var splices = Changeset.toSplices(cs); + var lengthChangeSoFar = 0; + for (var i = 0; i < splices.length; i++) + { + var splice = splices[i]; + var spliceStart = splice[0] + lengthChangeSoFar; + var spliceEnd = splice[1] + lengthChangeSoFar; + var newTextLength = splice[2].length; + var thisLengthChange = newTextLength - (spliceEnd - spliceStart); + + if (spliceStart <= newStartChar && spliceEnd >= newEndChar) + { + // splice fully replaces/deletes range + // (also case that handles insertion at a collapsed selection) + if (insertionsAfter) + { + newStartChar = newEndChar = spliceStart; + } + else + { + newStartChar = newEndChar = spliceStart + newTextLength; + } + } + else if (spliceEnd <= newStartChar) + { + // splice is before range + newStartChar += thisLengthChange; + newEndChar += thisLengthChange; + } + else if (spliceStart >= newEndChar) + { + // splice is after range + } + else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) + { + // splice is inside range + newEndChar += thisLengthChange; + } + else if (spliceEnd < newEndChar) + { + // splice overlaps beginning of range + newStartChar = spliceStart + newTextLength; + newEndChar += thisLengthChange; + } + else + { + // splice overlaps end of range + newEndChar = spliceStart; + } + + lengthChangeSoFar += thisLengthChange; + } + + return [newStartChar, newEndChar]; +}; + +Changeset.moveOpsToNewPool = function (cs, oldPool, newPool) +{ + // works on changeset or attribution string + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) + { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + var fromDollar = cs.substring(dollarPos); + // order of attribs stays the same + return upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) + { + var oldNum = Changeset.parseNum(a); + var pair = oldPool.getAttrib(oldNum); + var newNum = newPool.putAttrib(pair); + return '*' + Changeset.numToString(newNum); + }) + fromDollar; +}; + +Changeset.makeAttribution = function (text) +{ + var assem = Changeset.smartOpAssembler(); + assem.appendOpWithText('+', text); + return assem.toString(); +}; + +// callable on a changeset, attribution string, or attribs property of an op +Changeset.eachAttribNumber = function (cs, func) +{ + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) + { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + + upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) + { + func(Changeset.parseNum(a)); + return ''; + }); +}; + +// callable on a changeset, attribution string, or attribs property of an op, +// though it may easily create adjacent ops that can be merged. +Changeset.filterAttribNumbers = function (cs, filter) +{ + return Changeset.mapAttribNumbers(cs, filter); +}; + +Changeset.mapAttribNumbers = function (cs, func) +{ + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) + { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + + var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function (s, a) + { + var n = func(Changeset.parseNum(a)); + if (n === true) + { + return s; + } + else if ((typeof n) === "number") + { + return '*' + Changeset.numToString(n); + } + else + { + return ''; + } + }); + + return newUpToDollar + cs.substring(dollarPos); +}; + +Changeset.makeAText = function (text, attribs) +{ + return { + text: text, + attribs: (attribs || Changeset.makeAttribution(text)) + }; +}; + +Changeset.applyToAText = function (cs, atext, pool) +{ + return { + text: Changeset.applyToText(cs, atext.text), + attribs: Changeset.applyToAttribution(cs, atext.attribs, pool) + }; +}; + +Changeset.cloneAText = function (atext) +{ + return { + text: atext.text, + attribs: atext.attribs + }; +}; + +Changeset.copyAText = function (atext1, atext2) +{ + atext2.text = atext1.text; + atext2.attribs = atext1.attribs; +}; + +Changeset.appendATextToAssembler = function (atext, assem) +{ + // intentionally skips last newline char of atext + var iter = Changeset.opIterator(atext.attribs); + var op = Changeset.newOp(); + while (iter.hasNext()) + { + iter.next(op); + if (!iter.hasNext()) + { + // last op, exclude final newline + if (op.lines <= 1) + { + op.lines = 0; + op.chars--; + if (op.chars) + { + assem.append(op); + } + } + else + { + var nextToLastNewlineEnd = atext.text.lastIndexOf('\n', atext.text.length - 2) + 1; + var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; + op.lines--; + op.chars -= (lastLineLength + 1); + assem.append(op); + op.lines = 0; + op.chars = lastLineLength; + if (op.chars) + { + assem.append(op); + } + } + } + else + { + assem.append(op); + } + } +}; + +Changeset.prepareForWire = function (cs, pool) +{ + var newPool = new AttribPool(); + var newCs = Changeset.moveOpsToNewPool(cs, pool, newPool); + return { + translated: newCs, + pool: newPool + }; +}; + +Changeset.isIdentity = function (cs) +{ + var unpacked = Changeset.unpack(cs); + return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; +}; + +Changeset.opAttributeValue = function (op, key, pool) +{ + return Changeset.attribsAttributeValue(op.attribs, key, pool); +}; + +Changeset.attribsAttributeValue = function (attribs, key, pool) +{ + var value = ''; + if (attribs) + { + Changeset.eachAttribNumber(attribs, function (n) + { + if (pool.getAttribKey(n) == key) + { + value = pool.getAttribValue(n); + } + }); + } + return value; +}; + +Changeset.builder = function (oldLen) +{ + var assem = Changeset.smartOpAssembler(); + var o = Changeset.newOp(); + var charBank = Changeset.stringAssembler(); + + var self = { + // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) + keep: function (N, L, attribs, pool) + { + o.opcode = '='; + o.attribs = (attribs && Changeset.makeAttribsString('=', attribs, pool)) || ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + keepText: function (text, attribs, pool) + { + assem.appendOpWithText('=', text, attribs, pool); + return self; + }, + insert: function (text, attribs, pool) + { + assem.appendOpWithText('+', text, attribs, pool); + charBank.append(text); + return self; + }, + remove: function (N, L) + { + o.opcode = '-'; + o.attribs = ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + toString: function () + { + assem.endDocument(); + var newLen = oldLen + assem.getLengthChange(); + return Changeset.pack(oldLen, newLen, assem.toString(), charBank.toString()); + } + }; + + return self; +}; + +Changeset.makeAttribsString = function (opcode, attribs, pool) +{ + // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work + if (!attribs) + { + return ''; + } + else if ((typeof attribs) == "string") + { + return attribs; + } + else if (pool && attribs && attribs.length) + { + if (attribs.length > 1) + { + attribs = attribs.slice(); + attribs.sort(); + } + var result = []; + for (var i = 0; i < attribs.length; i++) + { + var pair = attribs[i]; + if (opcode == '=' || (opcode == '+' && pair[1])) + { + result.push('*' + Changeset.numToString(pool.putAttrib(pair))); + } + } + return result.join(''); + } +}; + +// like "substring" but on a single-line attribution string +Changeset.subattribution = function (astr, start, optEnd) +{ + var iter = Changeset.opIterator(astr, 0); + var assem = Changeset.smartOpAssembler(); + var attOp = Changeset.newOp(); + var csOp = Changeset.newOp(); + var opOut = Changeset.newOp(); + + function doCsOp() + { + if (csOp.chars) + { + while (csOp.opcode && (attOp.opcode || iter.hasNext())) + { + if (!attOp.opcode) iter.next(attOp); + + if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && attOp.lines > 0 && csOp.lines <= 0) + { + csOp.lines++; + } + + Changeset._slicerZipperFunc(attOp, csOp, opOut, null); + if (opOut.opcode) + { + assem.append(opOut); + opOut.opcode = ''; + } + } + } + } + + csOp.opcode = '-'; + csOp.chars = start; + + doCsOp(); + + if (optEnd === undefined) + { + if (attOp.opcode) + { + assem.append(attOp); + } + while (iter.hasNext()) + { + iter.next(attOp); + assem.append(attOp); + } + } + else + { + csOp.opcode = '='; + csOp.chars = optEnd - start; + doCsOp(); + } + + return assem.toString(); +}; + +Changeset.inverse = function (cs, lines, alines, pool) +{ + // lines and alines are what the changeset is meant to apply to. + // They may be arrays or objects with .get(i) and .length methods. + // They include final newlines on lines. + + + function lines_get(idx) + { + if (lines.get) + { + return lines.get(idx); + } + else + { + return lines[idx]; + } + } + + function lines_length() + { + if ((typeof lines.length) == "number") + { + return lines.length; + } + else + { + return lines.length(); + } + } + + function alines_get(idx) + { + if (alines.get) + { + return alines.get(idx); + } + else + { + return alines[idx]; + } + } + + function alines_length() + { + if ((typeof alines.length) == "number") + { + return alines.length; + } + else + { + return alines.length(); + } + } + + var curLine = 0; + var curChar = 0; + var curLineOpIter = null; + var curLineOpIterLine; + var curLineNextOp = Changeset.newOp('+'); + + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var builder = Changeset.builder(unpacked.newLen); + + function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) + { + + if ((!curLineOpIter) || (curLineOpIterLine != curLine)) + { + // create curLineOpIter and advance it to curChar + curLineOpIter = Changeset.opIterator(alines_get(curLine)); + curLineOpIterLine = curLine; + var indexIntoLine = 0; + var done = false; + while (!done) + { + curLineOpIter.next(curLineNextOp); + if (indexIntoLine + curLineNextOp.chars >= curChar) + { + curLineNextOp.chars -= (curChar - indexIntoLine); + done = true; + } + else + { + indexIntoLine += curLineNextOp.chars; + } + } + } + + while (numChars > 0) + { + if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) + { + curLine++; + curChar = 0; + curLineOpIterLine = curLine; + curLineNextOp.chars = 0; + curLineOpIter = Changeset.opIterator(alines_get(curLine)); + } + if (!curLineNextOp.chars) + { + curLineOpIter.next(curLineNextOp); + } + var charsToUse = Math.min(numChars, curLineNextOp.chars); + func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); + numChars -= charsToUse; + curLineNextOp.chars -= charsToUse; + curChar += charsToUse; + } + + if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) + { + curLine++; + curChar = 0; + } + } + + function skip(N, L) + { + if (L) + { + curLine += L; + curChar = 0; + } + else + { + if (curLineOpIter && curLineOpIterLine == curLine) + { + consumeAttribRuns(N, function () + {}); + } + else + { + curChar += N; + } + } + } + + function nextText(numChars) + { + var len = 0; + var assem = Changeset.stringAssembler(); + var firstString = lines_get(curLine).substring(curChar); + len += firstString.length; + assem.append(firstString); + + var lineNum = curLine + 1; + while (len < numChars) + { + var nextString = lines_get(lineNum); + len += nextString.length; + assem.append(nextString); + lineNum++; + } + + return assem.toString().substring(0, numChars); + } + + function cachedStrFunc(func) + { + var cache = {}; + return function (s) + { + if (!cache[s]) + { + cache[s] = func(s); + } + return cache[s]; + }; + } + + var attribKeys = []; + var attribValues = []; + while (csIter.hasNext()) + { + var csOp = csIter.next(); + if (csOp.opcode == '=') + { + if (csOp.attribs) + { + attribKeys.length = 0; + attribValues.length = 0; + Changeset.eachAttribNumber(csOp.attribs, function (n) + { + attribKeys.push(pool.getAttribKey(n)); + attribValues.push(pool.getAttribValue(n)); + }); + var undoBackToAttribs = cachedStrFunc(function (attribs) + { + var backAttribs = []; + for (var i = 0; i < attribKeys.length; i++) + { + var appliedKey = attribKeys[i]; + var appliedValue = attribValues[i]; + var oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, pool); + if (appliedValue != oldValue) + { + backAttribs.push([appliedKey, oldValue]); + } + } + return Changeset.makeAttribsString('=', backAttribs, pool); + }); + consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) + { + builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs)); + }); + } + else + { + skip(csOp.chars, csOp.lines); + builder.keep(csOp.chars, csOp.lines); + } + } + else if (csOp.opcode == '+') + { + builder.remove(csOp.chars, csOp.lines); + } + else if (csOp.opcode == '-') + { + var textBank = nextText(csOp.chars); + var textBankIndex = 0; + consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) + { + builder.insert(textBank.substr(textBankIndex, len), attribs); + textBankIndex += len; + }); + } + } + + return Changeset.checkRep(builder.toString()); +}; diff --git a/static/js/linestylefilter_client.js b/static/js/linestylefilter_client.js new file mode 100644 index 000000000..c85093673 --- /dev/null +++ b/static/js/linestylefilter_client.js @@ -0,0 +1,342 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/linestylefilter.js +// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.linestylefilter +/** + * 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. + */ +// requires: easysync2.Changeset +// requires: top +// requires: plugins +// requires: undefined +var linestylefilter = {}; + +linestylefilter.ATTRIB_CLASSES = { + 'bold': 'tag:b', + 'italic': 'tag:i', + 'underline': 'tag:u', + 'strikethrough': 'tag:s' +}; + +linestylefilter.getAuthorClassName = function (author) +{ + return "author-" + author.replace(/[^a-y0-9]/g, function (c) + { + if (c == ".") return "-"; + return 'z' + c.charCodeAt(0) + 'z'; + }); +}; + +// lineLength is without newline; aline includes newline, +// but may be falsy if lineLength == 0 +linestylefilter.getLineStyleFilter = function (lineLength, aline, textAndClassFunc, apool) +{ + + var plugins_; + if (typeof (plugins) != 'undefined') + { + plugins_ = plugins; + } + else + { + plugins_ = parent.parent.plugins; + } + + if (lineLength == 0) return textAndClassFunc; + + var nextAfterAuthorColors = textAndClassFunc; + + var authorColorFunc = (function () + { + var lineEnd = lineLength; + var curIndex = 0; + var extraClasses; + var leftInAuthor; + + function attribsToClasses(attribs) + { + var classes = ''; + Changeset.eachAttribNumber(attribs, function (n) + { + var key = apool.getAttribKey(n); + if (key) + { + var value = apool.getAttribValue(n); + if (value) + { + if (key == 'author') + { + classes += ' ' + linestylefilter.getAuthorClassName(value); + } + else if (key == 'list') + { + classes += ' list:' + value; + } + else if (linestylefilter.ATTRIB_CLASSES[key]) + { + classes += ' ' + linestylefilter.ATTRIB_CLASSES[key]; + } + else + { + classes += plugins_.callHookStr("aceAttribsToClasses", { + linestylefilter: linestylefilter, + key: key, + value: value + }, " ", " ", ""); + } + } + } + }); + return classes.substring(1); + } + + var attributionIter = Changeset.opIterator(aline); + var nextOp, nextOpClasses; + + function goNextOp() + { + nextOp = attributionIter.next(); + nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); + } + goNextOp(); + + function nextClasses() + { + if (curIndex < lineEnd) + { + extraClasses = nextOpClasses; + leftInAuthor = nextOp.chars; + goNextOp(); + while (nextOp.opcode && nextOpClasses == extraClasses) + { + leftInAuthor += nextOp.chars; + goNextOp(); + } + } + } + nextClasses(); + + return function (txt, cls) + { + while (txt.length > 0) + { + if (leftInAuthor <= 0) + { + // prevent infinite loop if something funny's going on + return nextAfterAuthorColors(txt, cls); + } + var spanSize = txt.length; + if (spanSize > leftInAuthor) + { + spanSize = leftInAuthor; + } + var curTxt = txt.substring(0, spanSize); + txt = txt.substring(spanSize); + nextAfterAuthorColors(curTxt, (cls && cls + " ") + extraClasses); + curIndex += spanSize; + leftInAuthor -= spanSize; + if (leftInAuthor == 0) + { + nextClasses(); + } + } + }; + })(); + return authorColorFunc; +}; + +linestylefilter.getAtSignSplitterFilter = function (lineText, textAndClassFunc) +{ + var at = /@/g; + at.lastIndex = 0; + var splitPoints = null; + var execResult; + while ((execResult = at.exec(lineText))) + { + if (!splitPoints) + { + splitPoints = []; + } + splitPoints.push(execResult.index); + } + + if (!splitPoints) return textAndClassFunc; + + return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints); +}; + +linestylefilter.getRegexpFilter = function (regExp, tag) +{ + return function (lineText, textAndClassFunc) + { + regExp.lastIndex = 0; + var regExpMatchs = null; + var splitPoints = null; + var execResult; + while ((execResult = regExp.exec(lineText))) + { + if (!regExpMatchs) + { + regExpMatchs = []; + splitPoints = []; + } + var startIndex = execResult.index; + var regExpMatch = execResult[0]; + regExpMatchs.push([startIndex, regExpMatch]); + splitPoints.push(startIndex, startIndex + regExpMatch.length); + } + + if (!regExpMatchs) return textAndClassFunc; + + function regExpMatchForIndex(idx) + { + for (var k = 0; k < regExpMatchs.length; k++) + { + var u = regExpMatchs[k]; + if (idx >= u[0] && idx < u[0] + u[1].length) + { + return u[1]; + } + } + return false; + } + + var handleRegExpMatchsAfterSplit = (function () + { + var curIndex = 0; + return function (txt, cls) + { + var txtlen = txt.length; + var newCls = cls; + var regExpMatch = regExpMatchForIndex(curIndex); + if (regExpMatch) + { + newCls += " " + tag + ":" + regExpMatch; + } + textAndClassFunc(txt, newCls); + curIndex += txtlen; + }; + })(); + + return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints); + }; +}; + + +linestylefilter.REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; +linestylefilter.REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + linestylefilter.REGEX_WORDCHAR.source + ')'); +linestylefilter.REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + linestylefilter.REGEX_URLCHAR.source + '*(?![:.,;])' + linestylefilter.REGEX_URLCHAR.source, 'g'); +linestylefilter.getURLFilter = linestylefilter.getRegexpFilter( +linestylefilter.REGEX_URL, 'url'); + +linestylefilter.textAndClassFuncSplitter = function (func, splitPointsOpt) +{ + var nextPointIndex = 0; + var idx = 0; + + // don't split at 0 + while (splitPointsOpt && nextPointIndex < splitPointsOpt.length && splitPointsOpt[nextPointIndex] == 0) + { + nextPointIndex++; + } + + function spanHandler(txt, cls) + { + if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) + { + func(txt, cls); + idx += txt.length; + } + else + { + var splitPoints = splitPointsOpt; + var pointLocInSpan = splitPoints[nextPointIndex] - idx; + var txtlen = txt.length; + if (pointLocInSpan >= txtlen) + { + func(txt, cls); + idx += txt.length; + if (pointLocInSpan == txtlen) + { + nextPointIndex++; + } + } + else + { + if (pointLocInSpan > 0) + { + func(txt.substring(0, pointLocInSpan), cls); + idx += pointLocInSpan; + } + nextPointIndex++; + // recurse + spanHandler(txt.substring(pointLocInSpan), cls); + } + } + } + return spanHandler; +}; + +linestylefilter.getFilterStack = function (lineText, textAndClassFunc, browser) +{ + var func = linestylefilter.getURLFilter(lineText, textAndClassFunc); + + var plugins_; + if (typeof (plugins) != 'undefined') + { + plugins_ = plugins; + } + else + { + plugins_ = parent.parent.plugins; + } + + var hookFilters = plugins_.callHook("aceGetFilterStack", { + linestylefilter: linestylefilter, + browser: browser + }); + hookFilters.map(function (hookFilter) + { + func = hookFilter(lineText, func); + }); + + if (browser !== undefined && browser.msie) + { + // IE7+ will take an e-mail address like and linkify it to foo@bar.com. + // We then normalize it back to text with no angle brackets. It's weird. So always + // break spans at an "at" sign. + func = linestylefilter.getAtSignSplitterFilter( + lineText, func); + } + return func; +}; + +// domLineObj is like that returned by domline.createDomLine +linestylefilter.populateDomLine = function (textLine, aline, apool, domLineObj) +{ + // remove final newline from text if any + var text = textLine; + if (text.slice(-1) == '\n') + { + text = text.substring(0, text.length - 1); + } + + function textAndClassFunc(tokenText, tokenClass) + { + domLineObj.appendSpan(tokenText, tokenClass); + } + + var func = linestylefilter.getFilterStack(text, textAndClassFunc); + func = linestylefilter.getLineStyleFilter(text.length, aline, func, apool); + func(text, ''); +}; diff --git a/static/js/pad2.js b/static/js/pad2.js index fffb11706..90f51c7f9 100644 --- a/static/js/pad2.js +++ b/static/js/pad2.js @@ -75,10 +75,11 @@ function handshake() createCookie("token", token, 60); } - var msg = { "type":"CLIENT_READY", + var msg = { "component" : "pad", + "type":"CLIENT_READY", "padId": padId, "token": token, - "protocolVersion": 1}; + "protocolVersion": 2}; socket.send(msg); }); diff --git a/static/timeslider.html b/static/timeslider.html new file mode 100644 index 000000000..88cdb84fb --- /dev/null +++ b/static/timeslider.html @@ -0,0 +1,376 @@ + + + + + + + + + Etherpad Lite Timeslider + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ +
+ +
+ +
+ Etherpad v1.1 Etherpad is free software + +
+ Full screen +
+
+ +
+ +
+
+ +
+
+

Server Notice:

hide + +

+
+
+ + + + +
+ +
+ + + +
+ + + +
+
+
+
+
+ +
+ +
+ +
+ +
+ + + + + + +
+

Public Pad Latest text of pad + test Saved June 14, 2011

+
 
+ + + + + +
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+ + +