diff --git a/CHANGELOG.md b/CHANGELOG.md index d2cabd99..e0fde550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# 1.2.7 + * NEW: notifications are now modularized and can be stacked + * NEW: Visit a specific revision in the timeslider by suffixing #%revNumber% IE http://localhost/p/test/timeslider#12 + * NEW: Link to plugin on Admin page allows admins to easily see plugin details in a new window by clicking on the plugin name + * NEW: Automatically see plugins that require update and be able to one click update + * NEW: API endpoints for Chat .. getChatHistory, getChatHead + * NEW: API endpoint to see a pad diff in HTML format from revision x to revision y .. createPadDiffHTML + * NEW: Real time plugin search & unified menu UI for admin pages + * Fix: MAJOR issue where server could be crashed by malformed client message + * Fix: AuthorID is now included in padUsers API response + * Fix: make docs + * Fix: Timeslider UI bug with slider not being in position + * Fix: IE8 language issue where it wouldn't load pads due to IE8 suckling on the bussum of hatrid + * Fix: Import timeout issue + * Fix: Import now works if Params are set in pad URL + * Fix: Convert script + * Other: Various new language strings and update/bugfixes of others + * Other: Clean up the getParams functionality + * Other: Various new EEJS blocks: index, timeslider, html etc. + # 1.2.6 * Fix: Package file UeberDB reference * New #users EEJS block for plugins diff --git a/doc/api/http_api.md b/doc/api/http_api.md index a7fc0bed..0543ef71 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -355,7 +355,7 @@ returns the number of user that are currently editing this pad returns the list of users that are currently editing this pad *Example returns:* - * `{code: 0, message:"ok", data: {padUsers: [{colorId:"#c1a9d9","name":"username1","timestamp":1345228793126},{"colorId":"#d9a9cd","name":"Hmmm","timestamp":1345228796042}]}}` + * `{code: 0, message:"ok", data: {padUsers: [{colorId:"#c1a9d9","name":"username1","timestamp":1345228793126,"id":"a.n4gEeMLsvg12452n"},{"colorId":"#d9a9cd","name":"Hmmm","timestamp":1345228796042,"id":"a.n4gEeMLsvg12452n"}]}}` * `{code: 0, message:"ok", data: {padUsers: []}}` #### deletePad(padID) diff --git a/src/locales/en.json b/src/locales/en.json index eea35cc5..bef6dfd0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -14,7 +14,7 @@ "pad.toolbar.clearAuthorship.title": "Clear Authorship Colors", "pad.toolbar.import_export.title": "Import/Export from/to different file formats", "pad.toolbar.timeslider.title": "Timeslider", - "pad.toolbar.savedRevision.title": "Saved Revisions", + "pad.toolbar.savedRevision.title": "Save Revision", "pad.toolbar.settings.title": "Settings", "pad.toolbar.embed.title": "Embed this pad", "pad.toolbar.showusers.title": "Show the users on this pad", diff --git a/src/node/db/API.js b/src/node/db/API.js index cee63a9e..f99a43af 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -30,6 +30,7 @@ var async = require("async"); var exportHtml = require("../utils/ExportHtml"); var importHtml = require("../utils/ImportHtml"); var cleanText = require("./Pad").cleanText; +var PadDiff = require("../utils/padDiff"); /**********************/ /**GROUP FUNCTIONS*****/ @@ -656,6 +657,86 @@ exports.getChatHead = function(padID, callback) }); } +/** +createDiffHTML(padID, startRev, endRev) returns an object of diffs from 2 points in a pad + +Example returns: + +{"code":0,"message":"ok","data":{"html":"Welcome to Etherpad Lite!

This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!

Get involved with Etherpad at http://etherpad.org
aw

","authors":["a.HKIv23mEbachFYfH",""]}} +{"code":4,"message":"no or wrong API Key","data":null} +*/ +exports.createDiffHTML = function(padID, startRev, endRev, callback){ + //check if rev is a number + if(startRev !== undefined && typeof startRev != "number") + { + //try to parse the number + if(!isNaN(parseInt(startRev))) + { + startRev = parseInt(startRev, 10); + } + else + { + callback({stop: "startRev is not a number"}); + return; + } + } + + //check if rev is a number + if(endRev !== undefined && typeof endRev != "number") + { + //try to parse the number + if(!isNaN(parseInt(endRev))) + { + endRev = parseInt(endRev, 10); + } + else + { + callback({stop: "endRev is not a number"}); + return; + } + } + + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(err){ + return callback(err); + } + + try { + var padDiff = new PadDiff(pad, startRev, endRev); + } catch(e) { + return callback({stop:e.message}); + } + var html, authors; + + async.series([ + function(callback){ + padDiff.getHtml(function(err, _html){ + if(err){ + return callback(err); + } + + html = _html; + callback(); + }); + }, + function(callback){ + padDiff.getAuthors(function(err, _authors){ + if(err){ + return callback(err); + } + + authors = _authors; + callback(); + }); + } + ], function(err){ + callback(err, {html: html, authors: authors}) + }); + }); +} + /******************************/ /** INTERNAL HELPER FUNCTIONS */ /******************************/ diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 28b2dd91..667e0605 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -24,6 +24,10 @@ var db = require("./DB").db; var async = require("async"); var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +exports.getColorPalette = function(){ + return ["#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", "#4c9c82", "#12d1ad", "#2d8e80", "#7485c3", "#a091c7", "#3185ab", "#6818b4", "#e6e76d", "#a42c64", "#f386e5", "#4ecc0c", "#c0c236", "#693224", "#b5de6a", "#9b88fd", "#358f9b", "#496d2f", "#e267fe", "#d23056", "#1a1a64", "#5aa335", "#d722bb", "#86dc6c", "#b5a714", "#955b6a", "#9f2985", "#4b81c8", "#3d6a5b", "#434e16", "#d16084", "#af6a0e", "#8c8bd8"]; +}; + /** * Checks if the author exists */ diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index da1ce9e1..4701e82a 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -213,6 +213,48 @@ Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targe }); }; +Pad.prototype.getRevision = function getRevisionChangeset(revNum, callback) { + db.get("pad:"+this.id+":revs:"+revNum, callback); +}; + +Pad.prototype.getAllAuthorColors = function getAllAuthorColors(callback){ + var authors = this.getAllAuthors(); + var returnTable = {}; + var colorPalette = authorManager.getColorPalette(); + + async.forEach(authors, function(author, callback){ + authorManager.getAuthorColorId(author, function(err, colorId){ + if(err){ + return callback(err); + } + //colorId might be a hex color or an number out of the palette + returnTable[author]=colorPalette[colorId] || colorId; + + callback(); + }); + }, function(err){ + callback(err, returnTable); + }); +}; + +Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, endRev) { + startRev = parseInt(startRev, 10); + var head = this.getHeadRevisionNumber(); + endRev = endRev ? parseInt(endRev, 10) : head; + if(isNaN(startRev) || startRev < 0 || startRev > head) { + startRev = null; + } + if(isNaN(endRev) || endRev < startRev) { + endRev = null; + } else if(endRev > head) { + endRev = head; + } + if(startRev !== null && endRev !== null) { + return { startRev: startRev , endRev: endRev } + } + return null; +}; + Pad.prototype.getKeyRevisionNumber = function getKeyRevisionNumber(revNum) { return Math.floor(revNum / 100) * 100; }; diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 5e0af464..c3d86224 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -56,7 +56,6 @@ var padList = { }); } }); - return this; }, /** * Returns all pads in alphabetical order as array. diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 6085edbf..9f86277a 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -180,6 +180,7 @@ var version = , "deleteGroup" : ["groupID"] , "listPads" : ["groupID"] , "listAllPads" : [] + , "createDiffHTML" : ["padID", "startRev", "endRev"] , "createPad" : ["padID", "text"] , "createGroupPad" : ["groupID", "padName", "text"] , "createAuthor" : ["name"] diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 434c25ad..6781cd88 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -210,6 +210,7 @@ exports.handleMessage = function(client, message) } else if (message.data.type == "SAVE_REVISION") { handleSaveRevisionMessage(client, message); } else if (message.data.type == "CLIENT_MESSAGE" && + message.data.payload != null && message.data.payload.type == "suggestUserName") { handleSuggestUserName(client, message); } else { @@ -473,6 +474,11 @@ function handleSuggestUserName(client, message) function handleUserInfoUpdate(client, message) { //check if all ok + if(message.data.userInfo == null) + { + messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no userInfo!"); + return; + } if(message.data.userInfo.colorId == null) { messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!"); @@ -1028,7 +1034,7 @@ function handleClientReady(client, message) "globalPadId": message.padId, "time": currentTime, }, - "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", "#4c9c82", "#12d1ad", "#2d8e80", "#7485c3", "#a091c7", "#3185ab", "#6818b4", "#e6e76d", "#a42c64", "#f386e5", "#4ecc0c", "#c0c236", "#693224", "#b5de6a", "#9b88fd", "#358f9b", "#496d2f", "#e267fe", "#d23056", "#1a1a64", "#5aa335", "#d722bb", "#86dc6c", "#b5a714", "#955b6a", "#9f2985", "#4b81c8", "#3d6a5b", "#434e16", "#d16084", "#af6a0e", "#8c8bd8"], + "colorPalette": authorManager.getColorPalette(), "clientIp": "127.0.0.1", "userIsGuest": true, "userColor": authorColorId, @@ -1536,6 +1542,7 @@ exports.padUsers = function (padID, callback) { } var aid = sessioninfos[ix].author; authorManager.getAuthor( aid, function ( err, author ) { + author.id = aid; authors.push( author ); if ( authors.length === pad2sessions[padID].length ) { callback(null, {padUsers: authors}); diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index 35403013..06919488 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -1,12 +1,12 @@ /** * 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. @@ -91,8 +91,9 @@ function getPadHTML(pad, revNum, callback) } exports.getPadHTML = getPadHTML; +exports.getHTMLFromAtext = getHTMLFromAtext; -function getHTMLFromAtext(pad, atext) +function getHTMLFromAtext(pad, atext, authorColors) { var apool = pad.apool(); var textLines = atext.text.slice(0, -1).split('\n'); @@ -101,6 +102,42 @@ function getHTMLFromAtext(pad, atext) var tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; var anumMap = {}; + var css = ""; + + var stripDotFromAuthorID = function(id){ + return id.replace(/\./g,'_'); + }; + + if(authorColors){ + css+=""; + } props.forEach(function (propName, i) { @@ -125,22 +162,53 @@ function getHTMLFromAtext(pad, atext) // Just bold Bold and italics Just italics var taker = Changeset.stringIterator(text); var assem = Changeset.stringAssembler(); - var openTags = []; + + function getSpanClassFor(i){ + //return if author colors are disabled + if (!authorColors) return false; + + var property = props[i]; + + if(property.substr(0,6) === "author"){ + return stripDotFromAuthorID(property); + } + + if(property === "removed"){ + return "removed"; + } + + return false; + } + function emitOpenTag(i) { openTags.unshift(i); - assem.append('<'); - assem.append(tags[i]); - assem.append('>'); + var spanClass = getSpanClassFor(i); + + if(spanClass){ + assem.append(''); + } else { + assem.append('<'); + assem.append(tags[i]); + assem.append('>'); + } } function emitCloseTag(i) { openTags.shift(); - assem.append(''); + var spanClass = getSpanClassFor(i); + + if(spanClass){ + assem.append(''); + } else { + assem.append(''); + } } function orderdCloseTags(tags2close) @@ -303,7 +371,7 @@ function getHTMLFromAtext(pad, atext) return _processSpaces(assem.toString()); } // end getLineHTML - var pieces = []; + var pieces = [css]; // Need to deal with constraints imposed on HTML lists; can // only gain one level of nesting at once, can't change type diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js new file mode 100644 index 00000000..1b3cf58f --- /dev/null +++ b/src/node/utils/padDiff.js @@ -0,0 +1,554 @@ +var Changeset = require("../../static/js/Changeset"); +var async = require("async"); +var exportHtml = require('./ExportHtml'); + +function PadDiff (pad, fromRev, toRev){ + //check parameters + if(!pad || !pad.id || !pad.atext || !pad.pool) + { + throw new Error('Invalid pad'); + } + + var range = pad.getValidRevisionRange(fromRev, toRev); + if(!range) { throw new Error('Invalid revision range.' + + ' startRev: ' + fromRev + + ' endRev: ' + toRev); } + + this._pad = pad; + this._fromRev = range.startRev; + this._toRev = range.endRev; + this._html = null; + this._authors = []; +} + +PadDiff.prototype._isClearAuthorship = function(changeset){ + //unpack + var unpacked = Changeset.unpack(changeset); + + //check if there is nothing in the charBank + if(unpacked.charBank !== "") + return false; + + //check if oldLength == newLength + if(unpacked.oldLen !== unpacked.newLen) + return false; + + //lets iterator over the operators + var iterator = Changeset.opIterator(unpacked.ops); + + //get the first operator, this should be a clear operator + var clearOperator = iterator.next(); + + //check if there is only one operator + if(iterator.hasNext() === true) + return false; + + //check if this operator doesn't change text + if(clearOperator.opcode !== "=") + return false; + + //check that this operator applys to the complete text + //if the text ends with a new line, its exactly one character less, else it has the same length + if(clearOperator.chars !== unpacked.oldLen-1 && clearOperator.chars !== unpacked.oldLen) + return false; + + var attributes = []; + Changeset.eachAttribNumber(changeset, function(attrNum){ + attributes.push(attrNum); + }); + + //check that this changeset uses only one attribute + if(attributes.length !== 1) + return false; + + var appliedAttribute = this._pad.pool.getAttrib(attributes[0]); + + //check if the applied attribute is an anonymous author attribute + if(appliedAttribute[0] !== "author" || appliedAttribute[1] !== "") + return false; + + return true; +} + +PadDiff.prototype._createClearAuthorship = function(rev, callback){ + var self = this; + this._pad.getInternalRevisionAText(rev, function(err, atext){ + if(err){ + return callback(err); + } + + //build clearAuthorship changeset + var builder = Changeset.builder(atext.text.length); + builder.keepText(atext.text, [['author','']], self._pad.pool); + var changeset = builder.toString(); + + callback(null, changeset); + }); +} + +PadDiff.prototype._createClearStartAtext = function(rev, callback){ + var self = this; + + //get the atext of this revision + this._pad.getInternalRevisionAText(rev, function(err, atext){ + if(err){ + return callback(err); + } + + //create the clearAuthorship changeset + self._createClearAuthorship(rev, function(err, changeset){ + if(err){ + return callback(err); + } + + //apply the clearAuthorship changeset + var newAText = Changeset.applyToAText(changeset, atext, self._pad.pool); + + callback(null, newAText); + }); + }); +} + +PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) { + var self = this; + + //find out which revisions we need + var revisions = []; + for(var i=startRev;i<(startRev+count) && i<=this._pad.head;i++){ + revisions.push(i); + } + + var changesets = [], authors = []; + + //get all needed revisions + async.forEach(revisions, function(rev, callback){ + self._pad.getRevision(rev, function(err, revision){ + if(err){ + return callback(err) + } + + var arrayNum = rev-startRev; + + changesets[arrayNum] = revision.changeset; + authors[arrayNum] = revision.meta.author; + + callback(); + }); + }, function(err){ + callback(err, changesets, authors); + }); +} + +PadDiff.prototype._addAuthors = function(authors) { + var self = this; + //add to array if not in the array + authors.forEach(function(author){ + if(self._authors.indexOf(author) == -1){ + self._authors.push(author); + } + }); +} + +PadDiff.prototype._createDiffAtext = function(callback) { + var self = this; + var bulkSize = 100; + + //get the cleaned startAText + self._createClearStartAtext(self._fromRev, function(err, atext){ + if(err) { return callback(err); } + + var superChangeset = null; + + var rev = self._fromRev + 1; + + //async while loop + async.whilst( + //loop condition + function () { return rev <= self._toRev; }, + + //loop body + function (callback) { + //get the bulk + self._getChangesetsInBulk(rev,bulkSize,function(err, changesets, authors){ + var addedAuthors = []; + + //run trough all changesets + for(var i=0;i= 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 = []; + + //iterate over all operators of this changeset + while (csIter.hasNext()) { + var csOp = csIter.next(); + + if (csOp.opcode == '=') { + var textBank = nextText(csOp.chars); + + // decide if this equal operator is an attribution change or not. We can see this by checkinf if attribs is set. + // If the text this operator applies to is only a star, than this is a false positive and should be ignored + if (csOp.attribs && textBank != "*") { + var deletedAttrib = apool.putAttrib(["removed", true]); + var authorAttrib = apool.putAttrib(["author", ""]);; + + attribKeys.length = 0; + attribValues.length = 0; + Changeset.eachAttribNumber(csOp.attribs, function (n) { + attribKeys.push(apool.getAttribKey(n)); + attribValues.push(apool.getAttribValue(n)); + + if(apool.getAttribKey(n) === "author"){ + authorAttrib = 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, apool); + if (appliedValue != oldValue) { + backAttribs.push([appliedKey, oldValue]); + } + } + return Changeset.makeAttribsString('=', backAttribs, apool); + }); + + var oldAttribsAddition = "*" + Changeset.numToString(deletedAttrib) + "*" + Changeset.numToString(authorAttrib); + + var textLeftToProcess = textBank; + + while(textLeftToProcess.length > 0){ + //process till the next line break or process only one line break + var lengthToProcess = textLeftToProcess.indexOf("\n"); + var lineBreak = false; + switch(lengthToProcess){ + case -1: + lengthToProcess=textLeftToProcess.length; + break; + case 0: + lineBreak = true; + lengthToProcess=1; + break; + } + + //get the text we want to procceed in this step + var processText = textLeftToProcess.substr(0, lengthToProcess); + textLeftToProcess = textLeftToProcess.substr(lengthToProcess); + + if(lineBreak){ + builder.keep(1, 1); //just skip linebreaks, don't do a insert + keep for a linebreak + + //consume the attributes of this linebreak + consumeAttribRuns(1, function(){}); + } else { + //add the old text via an insert, but add a deletion attribute + the author attribute of the author who deleted it + var textBankIndex = 0; + consumeAttribRuns(lengthToProcess, function (len, attribs, endsLine) { + //get the old attributes back + var attribs = (undoBackToAttribs(attribs) || "") + oldAttribsAddition; + + builder.insert(processText.substr(textBankIndex, len), attribs); + textBankIndex += len; + }); + + builder.keep(lengthToProcess, 0); + } + } + } else { + skip(csOp.chars, csOp.lines); + builder.keep(csOp.chars, csOp.lines); + } + } else if (csOp.opcode == '+') { + builder.keep(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 + csOp.attribs); + textBankIndex += len; + }); + } + } + + return Changeset.checkRep(builder.toString()); +}; + +//export the constructor +module.exports = PadDiff; diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index 080da442..b010f851 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -14,6 +14,7 @@ , "pad_savedrevs.js" , "pad_connectionstatus.js" , "chat.js" + , "gritter.js" , "$tinycon/tinycon.js" , "excanvas.js" , "farbtastic.js" diff --git a/src/package.json b/src/package.json index fd48bce6..6d05e6a2 100644 --- a/src/package.json +++ b/src/package.json @@ -46,5 +46,5 @@ "engines" : { "node" : ">=0.6.0", "npm" : ">=1.0" }, - "version" : "1.2.6" + "version" : "1.2.7" } diff --git a/src/static/css/pad.css b/src/static/css/pad.css index bbbadbc1..6034b5ed 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -925,3 +925,95 @@ input[type=checkbox] { #wrongPassword{ display:none; } + +/* gritter stuff */ +#gritter-notice-wrapper { + position:fixed; + top:20px; + right:20px; + width:301px; + z-index:9999; +} +#gritter-notice-wrapper.bottom-right { + top: auto; + left: auto; + bottom: 20px; + right: 20px; +} +.gritter-item-wrapper { + position:relative; + margin:0 0 10px 0; +} + +.gritter-top { + background:url(../../static/img/gritter.png) no-repeat left -30px; + height:10px; +} +.hover .gritter-top { + background-position:right -30px; +} +.gritter-bottom { + background:url(../../static/img/gritter.png) no-repeat left bottom; + height:8px; + margin:0; +} +.hover .gritter-bottom { + background-position: bottom right; +} +.gritter-item { + display:block; + background:url(../../static/img/gritter.png) no-repeat left -40px; + color:#eee; + padding:2px 11px 8px 11px; + font-size: 11px; + font-family:verdana; +} +.hover .gritter-item { + background-position:right -40px; +} +.gritter-item p { + padding:0; + margin:0; +} +.gritter-close { + display:none; + position:absolute; + top:5px; + left:3px; + background:url('../../static/img/gritter.png') no-repeat left top; + cursor:pointer; + width:30px; + height:30px; +} +.gritter-title { + font-size:14px; + font-weight:bold; + padding:0 0 7px 0; + display:block; + text-shadow:1px 1px 0 #000; /* Not supported by IE :( */ +} +.gritter-image { + width:48px; + height:48px; + float:left; +} +.gritter-with-image, +.gritter-without-image { + padding:0 0 5px 0; +} +.gritter-with-image { + width:220px; + float:right; +} +/* for the light (white) version of the gritter notice */ +.gritter-light .gritter-item, +.gritter-light .gritter-bottom, +.gritter-light .gritter-top, +.gritter-close { + color: #222; +} +.gritter-light .gritter-title { + text-shadow: none; +} + +/* End of gritter stuff */ diff --git a/src/static/img/gritter.png b/src/static/img/gritter.png new file mode 100644 index 00000000..0ca3bc0a Binary files /dev/null and b/src/static/img/gritter.png differ diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index cfea4362..b1604212 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -2182,3 +2182,121 @@ exports.followAttributes = function (att1, att2, pool) { } return buf.toString(); }; + +exports.composeWithDeletions = function (cs1, cs2, pool) { + var unpacked1 = exports.unpack(cs1); + var unpacked2 = exports.unpack(cs2); + var len1 = unpacked1.oldLen; + var len2 = unpacked1.newLen; + exports.assert(len2 == unpacked2.oldLen, "mismatched composition"); + var len3 = unpacked2.newLen; + var bankIter1 = exports.stringIterator(unpacked1.charBank); + var bankIter2 = exports.stringIterator(unpacked2.charBank); + var bankAssem = exports.stringAssembler(); + + var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) { + var op1code = op1.opcode; + var op2code = op2.opcode; + if (op1code == '+' && op2code == '-') { + bankIter1.skip(Math.min(op1.chars, op2.chars)); + } + exports._slicerZipperFuncWithDeletions(op1, op2, opOut, pool); + if (opOut.opcode == '+') { + if (op2code == '+') { + bankAssem.append(bankIter2.take(opOut.chars)); + } else { + bankAssem.append(bankIter1.take(opOut.chars)); + } + } + }); + + return exports.pack(len1, len3, newOps, bankAssem.toString()); +}; + +// This function is 95% like _slicerZipperFunc, we just changed two lines to ensure it merges the attribs of deletions properly. +// This is necassary for correct paddiff. But to ensure these changes doesn't affect anything else, we've created a seperate function only used for paddiffs +exports._slicerZipperFuncWithDeletions= 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 exportss being composed. + // pool can be null if definitely not needed. + //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + if (attOp.opcode == '-') { + exports.copyOp(attOp, opOut); + attOp.opcode = ''; + } else if (!attOp.opcode) { + exports.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 = csOp.attribs; //changed by yammer + } + 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.attribs; //changed by yammer + } + csOp.chars -= attOp.chars; + csOp.lines -= attOp.lines; + attOp.opcode = ''; + } + break; + } + case '+': + { + // insert + exports.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 = exports.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 = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); + attOp.opcode = ''; + csOp.chars -= attOp.chars; + csOp.lines -= attOp.lines; + } + break; + } + case '': + { + exports.copyOp(attOp, opOut); + attOp.opcode = ''; + break; + } + } + } +}; diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js index 221666de..08ac08b5 100644 --- a/src/static/js/broadcast_slider.js +++ b/src/static/js/broadcast_slider.js @@ -107,6 +107,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) { newpos = Number(newpos); if (newpos < 0 || newpos > sliderLength) return; + window.location.hash = "#" + newpos; $("#ui-slider-handle").css('left', newpos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)); $("a.tlink").map(function() { @@ -481,6 +482,18 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) } $("#timeslider").show(); + + var startPos = clientVars.collab_client_vars.rev; + if(window.location.hash.length > 1) + { + var hashRev = Number(window.location.hash.substr(1)); + if(!isNaN(hashRev)) + { + // this is necessary because of the socket.io-event which loads the changesets + setTimeout(function() { setSliderPosition(hashRev); }, 1); + } + } + setSliderLength(clientVars.collab_client_vars.rev); setSliderPosition(clientVars.collab_client_vars.rev); diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 205294a8..2dff2edf 100644 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -1,9 +1,3 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - /** * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * @@ -22,7 +16,6 @@ var padutils = require('./pad_utils').padutils; var padcookie = require('./pad_cookie').padcookie; - var Tinycon = require('tinycon/tinycon'); var chat = (function() @@ -36,6 +29,7 @@ var chat = (function() { $("#chaticon").hide(); $("#chatbox").show(); + $("#gritter-notice-wrapper").hide(); self.scrollDown(); chatMentions = 0; Tinycon.setBubble(0); @@ -62,6 +56,8 @@ var chat = (function() $("#chatcounter").text("0"); $("#chaticon").show(); $("#chatbox").hide(); + $.gritter.removeAll(); + $("#gritter-notice-wrapper").show(); }, scrollDown: function() { @@ -122,7 +118,7 @@ var chat = (function() $("#chattext").append(html); //should we increment the counter?? - if(increment) + if(increment && !isHistoryAdd) { var count = Number($("#chatcounter").text()); count++; @@ -130,17 +126,44 @@ var chat = (function() // is the users focus already in the chatbox? var alreadyFocused = $("#chatinput").is(":focus"); + // does the user already have the chatbox open? + var chatOpen = $("#chatbox").is(":visible"); + $("#chatcounter").text(count); // chat throb stuff -- Just make it throw for twice as long - if(wasMentioned && !alreadyFocused && !isHistoryAdd) + if(wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) { // If the user was mentioned show for twice as long and flash the browser window - $('#chatthrob').html(""+authorName+"" + ": " + text).show().delay(4000).hide(400); + $.gritter.add({ + // (string | mandatory) the heading of the notification + title: authorName, + // (string | mandatory) the text inside the notification + text: text, + // (bool | optional) if you want it to fade out on its own or just sit there + sticky: true, + // (int | optional) the time you want it to be alive for before fading out + time: '2000' + }); + chatMentions++; Tinycon.setBubble(chatMentions); } else { - $('#chatthrob').html(""+authorName+"" + ": " + text).show().delay(2000).hide(400); + if(!chatOpen){ + $.gritter.add({ + // (string | mandatory) the heading of the notification + title: authorName, + // (string | mandatory) the text inside the notification + text: text, + + // (bool | optional) if you want it to fade out on its own or just sit there + sticky: false, + // (int | optional) the time you want it to be alive for before fading out + time: '4000' + }); + Tinycon.setBubble(count); + + } } } // Clear the chat mentions when the user clicks on the chat input box diff --git a/src/static/js/gritter.js b/src/static/js/gritter.js new file mode 100644 index 00000000..c32cc758 --- /dev/null +++ b/src/static/js/gritter.js @@ -0,0 +1,417 @@ +/* + * Gritter for jQuery + * http://www.boedesign.com/ + * + * Copyright (c) 2012 Jordan Boesch + * Dual licensed under the MIT and GPL licenses. + * + * Date: February 24, 2012 + * Version: 1.7.4 + */ + +(function($){ + /** + * Set it up as an object under the jQuery namespace + */ + $.gritter = {}; + + /** + * Set up global options that the user can over-ride + */ + $.gritter.options = { + position: '', + class_name: '', // could be set to 'gritter-light' to use white notifications + fade_in_speed: 'medium', // how fast notifications fade in + fade_out_speed: 1000, // how fast the notices fade out + time: 6000 // hang on the screen for... + } + + /** + * Add a gritter notification to the screen + * @see Gritter#add(); + */ + $.gritter.add = function(params){ + + try { + return Gritter.add(params || {}); + } catch(e) { + + var err = 'Gritter Error: ' + e; + (typeof(console) != 'undefined' && console.error) ? + console.error(err, params) : + alert(err); + + } + + } + + /** + * Remove a gritter notification from the screen + * @see Gritter#removeSpecific(); + */ + $.gritter.remove = function(id, params){ + Gritter.removeSpecific(id, params || {}); + } + + /** + * Remove all notifications + * @see Gritter#stop(); + */ + $.gritter.removeAll = function(params){ + Gritter.stop(params || {}); + } + + /** + * Big fat Gritter object + * @constructor (not really since its object literal) + */ + var Gritter = { + + // Public - options to over-ride with $.gritter.options in "add" + position: '', + fade_in_speed: '', + fade_out_speed: '', + time: '', + + // Private - no touchy the private parts + _custom_timer: 0, + _item_count: 0, + _is_setup: 0, + _tpl_close: '
', + _tpl_title: '[[title]]', + _tpl_item: '', + _tpl_wrap: '
', + + /** + * Add a gritter notification to the screen + * @param {Object} params The object that contains all the options for drawing the notification + * @return {Integer} The specific numeric id to that gritter notification + */ + add: function(params){ + // Handle straight text + if(typeof(params) == 'string'){ + params = {text:params}; + } + + // We might have some issues if we don't have a title or text! + if(!params.text){ + throw 'You must supply "text" parameter.'; + } + + // Check the options and set them once + if(!this._is_setup){ + this._runSetup(); + } + + // Basics + var title = params.title, + text = params.text, + image = params.image || '', + sticky = params.sticky || false, + item_class = params.class_name || $.gritter.options.class_name, + position = $.gritter.options.position, + time_alive = params.time || ''; + + this._verifyWrapper(); + + this._item_count++; + var number = this._item_count, + tmp = this._tpl_item; + + // Assign callbacks + $(['before_open', 'after_open', 'before_close', 'after_close']).each(function(i, val){ + Gritter['_' + val + '_' + number] = ($.isFunction(params[val])) ? params[val] : function(){} + }); + + // Reset + this._custom_timer = 0; + + // A custom fade time set + if(time_alive){ + this._custom_timer = time_alive; + } + + var image_str = (image != '') ? '' : '', + class_name = (image != '') ? 'gritter-with-image' : 'gritter-without-image'; + + // String replacements on the template + if(title){ + title = this._str_replace('[[title]]',title,this._tpl_title); + }else{ + title = ''; + } + + tmp = this._str_replace( + ['[[title]]', '[[text]]', '[[close]]', '[[image]]', '[[number]]', '[[class_name]]', '[[item_class]]'], + [title, text, this._tpl_close, image_str, this._item_count, class_name, item_class], tmp + ); + + // If it's false, don't show another gritter message + if(this['_before_open_' + number]() === false){ + return false; + } + + $('#gritter-notice-wrapper').addClass(position).append(tmp); + + var item = $('#gritter-item-' + this._item_count); + + item.fadeIn(this.fade_in_speed, function(){ + Gritter['_after_open_' + number]($(this)); + }); + + if(!sticky){ + this._setFadeTimer(item, number); + } + + // Bind the hover/unhover states + $(item).bind('mouseenter mouseleave', function(event){ + if(event.type == 'mouseenter'){ + if(!sticky){ + Gritter._restoreItemIfFading($(this), number); + } + } + else { + if(!sticky){ + Gritter._setFadeTimer($(this), number); + } + } + Gritter._hoverState($(this), event.type); + }); + + // Clicking (X) makes the perdy thing close + $(item).find('.gritter-close').click(function(){ + Gritter.removeSpecific(number, {}, null, true); + }); + + return number; + + }, + + /** + * If we don't have any more gritter notifications, get rid of the wrapper using this check + * @private + * @param {Integer} unique_id The ID of the element that was just deleted, use it for a callback + * @param {Object} e The jQuery element that we're going to perform the remove() action on + * @param {Boolean} manual_close Did we close the gritter dialog with the (X) button + */ + _countRemoveWrapper: function(unique_id, e, manual_close){ + + // Remove it then run the callback function + e.remove(); + this['_after_close_' + unique_id](e, manual_close); + + // Check if the wrapper is empty, if it is.. remove the wrapper + if($('.gritter-item-wrapper').length == 0){ + $('#gritter-notice-wrapper').remove(); + } + + }, + + /** + * Fade out an element after it's been on the screen for x amount of time + * @private + * @param {Object} e The jQuery element to get rid of + * @param {Integer} unique_id The id of the element to remove + * @param {Object} params An optional list of params to set fade speeds etc. + * @param {Boolean} unbind_events Unbind the mouseenter/mouseleave events if they click (X) + */ + _fade: function(e, unique_id, params, unbind_events){ + + var params = params || {}, + fade = (typeof(params.fade) != 'undefined') ? params.fade : true, + fade_out_speed = params.speed || this.fade_out_speed, + manual_close = unbind_events; + + this['_before_close_' + unique_id](e, manual_close); + + // If this is true, then we are coming from clicking the (X) + if(unbind_events){ + e.unbind('mouseenter mouseleave'); + } + + // Fade it out or remove it + if(fade){ + + e.animate({ + opacity: 0 + }, fade_out_speed, function(){ + e.animate({ height: 0 }, 300, function(){ + Gritter._countRemoveWrapper(unique_id, e, manual_close); + }) + }) + + } + else { + + this._countRemoveWrapper(unique_id, e); + + } + + }, + + /** + * Perform actions based on the type of bind (mouseenter, mouseleave) + * @private + * @param {Object} e The jQuery element + * @param {String} type The type of action we're performing: mouseenter or mouseleave + */ + _hoverState: function(e, type){ + + // Change the border styles and add the (X) close button when you hover + if(type == 'mouseenter'){ + + e.addClass('hover'); + + // Show close button + e.find('.gritter-close').show(); + + } + // Remove the border styles and hide (X) close button when you mouse out + else { + + e.removeClass('hover'); + + // Hide close button + e.find('.gritter-close').hide(); + + } + + }, + + /** + * Remove a specific notification based on an ID + * @param {Integer} unique_id The ID used to delete a specific notification + * @param {Object} params A set of options passed in to determine how to get rid of it + * @param {Object} e The jQuery element that we're "fading" then removing + * @param {Boolean} unbind_events If we clicked on the (X) we set this to true to unbind mouseenter/mouseleave + */ + removeSpecific: function(unique_id, params, e, unbind_events){ + + if(!e){ + var e = $('#gritter-item-' + unique_id); + } + + // We set the fourth param to let the _fade function know to + // unbind the "mouseleave" event. Once you click (X) there's no going back! + this._fade(e, unique_id, params || {}, unbind_events); + + }, + + /** + * If the item is fading out and we hover over it, restore it! + * @private + * @param {Object} e The HTML element to remove + * @param {Integer} unique_id The ID of the element + */ + _restoreItemIfFading: function(e, unique_id){ + + clearTimeout(this['_int_id_' + unique_id]); + e.stop().css({ opacity: '', height: '' }); + + }, + + /** + * Setup the global options - only once + * @private + */ + _runSetup: function(){ + + for(opt in $.gritter.options){ + this[opt] = $.gritter.options[opt]; + } + this._is_setup = 1; + + }, + + /** + * Set the notification to fade out after a certain amount of time + * @private + * @param {Object} item The HTML element we're dealing with + * @param {Integer} unique_id The ID of the element + */ + _setFadeTimer: function(e, unique_id){ + + var timer_str = (this._custom_timer) ? this._custom_timer : this.time; + this['_int_id_' + unique_id] = setTimeout(function(){ + Gritter._fade(e, unique_id); + }, timer_str); + + }, + + /** + * Bring everything to a halt + * @param {Object} params A list of callback functions to pass when all notifications are removed + */ + stop: function(params){ + + // callbacks (if passed) + var before_close = ($.isFunction(params.before_close)) ? params.before_close : function(){}; + var after_close = ($.isFunction(params.after_close)) ? params.after_close : function(){}; + + var wrap = $('#gritter-notice-wrapper'); + before_close(wrap); + wrap.fadeOut(function(){ + $(this).remove(); + after_close(); + }); + + }, + + /** + * An extremely handy PHP function ported to JS, works well for templating + * @private + * @param {String/Array} search A list of things to search for + * @param {String/Array} replace A list of things to replace the searches with + * @return {String} sa The output + */ + _str_replace: function(search, replace, subject, count){ + + var i = 0, j = 0, temp = '', repl = '', sl = 0, fl = 0, + f = [].concat(search), + r = [].concat(replace), + s = subject, + ra = r instanceof Array, sa = s instanceof Array; + s = [].concat(s); + + if(count){ + this.window[count] = 0; + } + + for(i = 0, sl = s.length; i < sl; i++){ + + if(s[i] === ''){ + continue; + } + + for (j = 0, fl = f.length; j < fl; j++){ + + temp = s[i] + ''; + repl = ra ? (r[j] !== undefined ? r[j] : '') : r[0]; + s[i] = (temp).split(f[j]).join(repl); + + if(count && s[i] !== temp){ + this.window[count] += (temp.length-s[i].length) / f[j].length; + } + + } + } + + return sa ? s : s[0]; + + }, + + /** + * A check to make sure we have something to wrap our notices with + * @private + */ + _verifyWrapper: function(){ + + if($('#gritter-notice-wrapper').length == 0){ + $('body').append(this._tpl_wrap); + } + + } + + } + +})(jQuery); diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 64d8b42b..27dd3b73 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -48,6 +48,7 @@ var colorutils = require('./colorutils').colorutils; var createCookie = require('./pad_utils').createCookie; var readCookie = require('./pad_utils').readCookie; var randomString = require('./pad_utils').randomString; +var gritter = require('./gritter').gritter; var hooks = require('./pluginfw/hooks'); @@ -101,86 +102,39 @@ function randomString() return "t." + randomstring; } +// This array represents all GET-parameters which can be used to change a setting. +// name: the parameter-name, eg `?noColors=true` => `noColors` +// checkVal: the callback is only executed when +// * the parameter was supplied and matches checkVal +// * the parameter was supplied and checkVal is null +// callback: the function to call when all above succeeds, `val` is the value supplied by the user +var getParameters = [ + { name: "noColors", checkVal: "true", callback: function(val) { settings.noColors = true; $('#clearAuthorship').hide(); } }, + { name: "showControls", checkVal: "false", callback: function(val) { $('#editbar').hide(); $('#editorcontainer').css({"top":"0px"}); } }, + { name: "showChat", checkVal: "false", callback: function(val) { $('#chaticon').hide(); } }, + { name: "showLineNumbers", checkVal: "false", callback: function(val) { settings.LineNumbersDisabled = true; } }, + { name: "useMonospaceFont", checkVal: "true", callback: function(val) { settings.useMonospaceFontGlobal = true; } }, + // If the username is set as a parameter we should set a global value that we can call once we have initiated the pad. + { name: "userName", checkVal: null, callback: function(val) { settings.globalUserName = decodeURIComponent(val); } }, + // If the userColor is set as a parameter, set a global value to use once we have initiated the pad. + { name: "userColor", checkVal: null, callback: function(val) { settings.globalUserColor = decodeURIComponent(val); } }, + { name: "rtl", checkVal: "true", callback: function(val) { settings.rtlIsTrue = true } }, + { name: "alwaysShowChat", checkVal: "true", callback: function(val) { chat.stickToScreen(); } }, + { name: "lang", checkVal: null, callback: function(val) { window.html10n.localize([val, 'en']); } } +]; + function getParams() { var params = getUrlVars() - var showControls = params["showControls"]; - var showChat = params["showChat"]; - var userName = params["userName"]; - var userColor = params["userColor"]; - var showLineNumbers = params["showLineNumbers"]; - var useMonospaceFont = params["useMonospaceFont"]; - var IsnoColors = params["noColors"]; - var rtl = params["rtl"]; - var alwaysShowChat = params["alwaysShowChat"]; - var lang = params["lang"]; - - if(IsnoColors) + + for(var i = 0; i < getParameters.length; i++) { - if(IsnoColors == "true") + var setting = getParameters[i]; + var value = params[setting.name]; + + if(value && (value == setting.checkVal || setting.checkVal == null)) { - settings.noColors = true; - $('#clearAuthorship').hide(); - } - } - if(showControls) - { - if(showControls == "false") - { - $('#editbar').hide(); - $('#editorcontainer').css({"top":"0px"}); - } - } - if(showChat) - { - if(showChat == "false") - { - $('#chaticon').hide(); - } - } - if(showLineNumbers) - { - if(showLineNumbers == "false") - { - settings.LineNumbersDisabled = true; - } - } - if(useMonospaceFont) - { - if(useMonospaceFont == "true") - { - settings.useMonospaceFontGlobal = true; - } - } - if(userName) - { - // If the username is set as a parameter we should set a global value that we can call once we have initiated the pad. - settings.globalUserName = decodeURIComponent(userName); - } - if(userColor) - // If the userColor is set as a parameter, set a global value to use once we have initiated the pad. - { - settings.globalUserColor = decodeURIComponent(userColor); - } - if(rtl) - { - if(rtl == "true") - { - settings.rtlIsTrue = true - } - } - if(alwaysShowChat) - { - if(alwaysShowChat == "true") - { - chat.stickToScreen(); - } - } - if(lang) - { - if(lang !== "") - { - window.html10n.localize([lang, 'en']); + setting.callback(value); } } } @@ -410,6 +364,13 @@ function handshake() }); } +$.extend($.gritter.options, { + position: 'bottom-right', // defaults to 'top-right' but can be 'bottom-left', 'bottom-right', 'top-left', 'top-right' (added in 1.7.1) + fade_in_speed: 'medium', // how fast notifications fade in (string or int) + fade_out_speed: 2000, // how fast the notices fade out + time: 6000 // hang on the screen for... +}); + var pad = { // don't access these directly from outside this file, except // for debugging diff --git a/src/templates/pad.html b/src/templates/pad.html index cb88c1c1..76df5133 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -365,8 +365,6 @@ <% e.end_block(); %> -
-
diff --git a/tests/frontend/specs/timeslider_revisions.js b/tests/frontend/specs/timeslider_revisions.js index af59051a..52f48764 100644 --- a/tests/frontend/specs/timeslider_revisions.js +++ b/tests/frontend/specs/timeslider_revisions.js @@ -57,4 +57,95 @@ describe("timeslider", function(){ }, 6000); }, revs*timePerRev); }); + + it("changes the url when clicking on the timeslider", function(done) { + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + // make some changes to produce 7 revisions + var timePerRev = 900 + , revs = 7; + this.timeout(revs*timePerRev+10000); + for(var i=0; i < revs; i++) { + setTimeout(function() { + // enter 'a' in the first text element + inner$("div").first().sendkeys('a'); + }, timePerRev*i); + } + + setTimeout(function() { + // go to timeslider + $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider'); + + setTimeout(function() { + var timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; + var $sliderBar = timeslider$('#ui-slider-bar'); + + var latestContents = timeslider$('#padcontent').text(); + + var oldUrl = $('#iframe-container iframe')[0].contentWindow.location.hash; + + // Click somewhere on the timeslider + var e = new jQuery.Event('mousedown'); + e.clientX = e.pageX = 150; + e.clientY = e.pageY = 60; + $sliderBar.trigger(e); + + helper.waitFor(function(){ + return $('#iframe-container iframe')[0].contentWindow.location.hash != oldUrl; + }, 6000).always(function(){ + expect( $('#iframe-container iframe')[0].contentWindow.location.hash ).not.to.eql( oldUrl ); + done(); + }); + }, 6000); + }, revs*timePerRev); + }); + + it("jumps to a revision given in the url", function(done) { + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + this.timeout(11000); + inner$("div").first().sendkeys('a'); + + setTimeout(function() { + // go to timeslider + $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider#0'); + var timeslider$; + + helper.waitFor(function(){ + timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; + return timeslider$ && timeslider$('#padcontent').text().length == 230; + }, 6000).always(function(){ + expect( timeslider$('#padcontent').text().length ).to.eql( 230 ); + done(); + }); + }, 2500); + }); + + it("checks the export url", function(done) { + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + this.timeout(11000); + inner$("div").first().sendkeys('a'); + + setTimeout(function() { + // go to timeslider + $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider#0'); + var timeslider$; + var exportLink; + + helper.waitFor(function(){ + timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; + if(!timeslider$) + return false; + exportLink = timeslider$('#exportplaina').attr('href'); + if(!exportLink) + return false; + return exportLink.substr(exportLink.length - 12) == "0/export/txt"; + }, 6000).always(function(){ + expect( exportLink.substr(exportLink.length - 12) ).to.eql( "0/export/txt" ); + done(); + }); + }, 2500); + }); });