summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSwen <swen.jung@psx-technology.com>2013-01-30 20:16:51 +0100
committerSwen <swen.jung@psx-technology.com>2013-01-30 20:16:51 +0100
commit1f348d911f03403f91bafb66a7da4b5ca577647f (patch)
tree226dd8e424ded3d217e1e0bba9000f794d094d96 /src
parent80333d8e0a60017b5625ac885ff66a41d5ef01b8 (diff)
parent621a410750bf271c840b5a9ab411b860de10e130 (diff)
downloadetherpad-lite-1f348d911f03403f91bafb66a7da4b5ca577647f.zip
Merge branch 'develop' into admin-index-plugable
Diffstat (limited to 'src')
-rw-r--r--src/locales/en.json2
-rw-r--r--src/node/db/API.js81
-rw-r--r--src/node/db/AuthorManager.js4
-rw-r--r--src/node/db/Pad.js42
-rw-r--r--src/node/db/PadManager.js1
-rw-r--r--src/node/handler/APIHandler.js1
-rw-r--r--src/node/handler/PadMessageHandler.js9
-rw-r--r--src/node/utils/ExportHtml.js92
-rw-r--r--src/node/utils/padDiff.js554
-rw-r--r--src/node/utils/tar.json1
-rw-r--r--src/package.json2
-rw-r--r--src/static/css/pad.css92
-rw-r--r--src/static/img/gritter.pngbin0 -> 4880 bytes
-rw-r--r--src/static/js/Changeset.js118
-rw-r--r--src/static/js/broadcast_slider.js13
-rw-r--r--src/static/js/chat.js45
-rw-r--r--src/static/js/gritter.js417
-rw-r--r--src/static/js/pad.js111
-rw-r--r--src/templates/pad.html2
19 files changed, 1483 insertions, 104 deletions
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":"<style>\n.authora_HKIv23mEbachFYfH {background-color: #a979d9}\n.authora_n4gEeMLsv1GivNeh {background-color: #a9b5d9}\n.removed {text-decoration: line-through; -ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)'; filter: alpha(opacity=80); opacity: 0.8; }\n</style>Welcome to Etherpad Lite!<br><br>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!<br><br>Get involved with Etherpad at <a href=\"http&#x3a;&#x2F;&#x2F;etherpad&#x2e;org\">http:&#x2F;&#x2F;etherpad.org</a><br><span class=\"authora_HKIv23mEbachFYfH\">aw</span><br><br>","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+="<style>\n";
+
+ for (var a in apool.numToAttrib) {
+ var attr = apool.numToAttrib[a];
+
+ //skip non author attributes
+ if(attr[0] === "author" && attr[1] !== ""){
+ //add to props array
+ var propName = "author" + stripDotFromAuthorID(attr[1]);
+ var newLength = props.push(propName);
+ anumMap[a] = newLength -1;
+
+ css+="." + propName + " {background-color: " + authorColors[attr[1]]+ "}\n";
+ } else if(attr[0] === "removed") {
+ var propName = "removed";
+
+ var newLength = props.push(propName);
+ anumMap[a] = newLength -1;
+
+ css+=".removed {text-decoration: line-through; " +
+ "-ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)'; "+
+ "filter: alpha(opacity=80); "+
+ "opacity: 0.8; "+
+ "}\n";
+ }
+ }
+
+ css+="</style>";
+ }
props.forEach(function (propName, i)
{
@@ -125,22 +162,53 @@ function getHTMLFromAtext(pad, atext)
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
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('<span class="');
+ assem.append(spanClass);
+ assem.append('">');
+ } else {
+ assem.append('<');
+ assem.append(tags[i]);
+ assem.append('>');
+ }
}
function emitCloseTag(i)
{
openTags.shift();
- assem.append('</');
- assem.append(tags[i]);
- assem.append('>');
+ var spanClass = getSpanClassFor(i);
+
+ if(spanClass){
+ assem.append('</span>');
+ } else {
+ assem.append('</');
+ assem.append(tags[i]);
+ 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<changesets.length && (rev+i)<=self._toRev;i++){
+ var changeset = changesets[i];
+
+ //skip clearAuthorship Changesets
+ if(self._isClearAuthorship(changeset)){
+ continue;
+ }
+
+ changeset = self._extendChangesetWithAuthor(changeset, authors[i], self._pad.pool);
+
+ //add this author to the authorarray
+ addedAuthors.push(authors[i]);
+
+ //compose it with the superChangset
+ if(superChangeset === null){
+ superChangeset = changeset;
+ } else {
+ superChangeset = Changeset.composeWithDeletions(superChangeset, changeset, self._pad.pool);
+ }
+ }
+
+ //add the authors to the PadDiff authorArray
+ self._addAuthors(addedAuthors);
+
+ //lets continue with the next bulk
+ rev += bulkSize;
+ callback();
+ });
+ },
+
+ //after the loop has ended
+ function (err) {
+ //if there are only clearAuthorship changesets, we don't get a superChangeset, so we can skip this step
+ if(superChangeset){
+ var deletionChangeset = self._createDeletionChangeset(superChangeset,atext,self._pad.pool);
+
+ //apply the superChangeset, which includes all addings
+ atext = Changeset.applyToAText(superChangeset,atext,self._pad.pool);
+ //apply the deletionChangeset, which adds a deletions
+ atext = Changeset.applyToAText(deletionChangeset,atext,self._pad.pool);
+ }
+
+ callback(err, atext);
+ }
+ );
+ });
+}
+
+PadDiff.prototype.getHtml = function(callback){
+ //cache the html
+ if(this._html != null){
+ return callback(null, this._html);
+ }
+
+ var self = this;
+ var atext, html, authorColors;
+
+ async.series([
+ //get the diff atext
+ function(callback){
+ self._createDiffAtext(function(err, _atext){
+ if(err){
+ return callback(err);
+ }
+
+ atext = _atext;
+ callback();
+ });
+ },
+ //get the authorColor table
+ function(callback){
+ self._pad.getAllAuthorColors(function(err, _authorColors){
+ if(err){
+ return callback(err);
+ }
+
+ authorColors = _authorColors;
+ callback();
+ });
+ },
+ //convert the atext to html
+ function(callback){
+ html = exportHtml.getHTMLFromAtext(self._pad, atext, authorColors);
+ self._html = html;
+ callback();
+ }
+ ], function(err){
+ callback(err, html);
+ });
+};
+
+PadDiff.prototype.getAuthors = function(callback){
+ var self = this;
+
+ //check if html was already produced, if not produce it, this generates the author array at the same time
+ if(self._html == null){
+ self.getHtml(function(err){
+ if(err){
+ return callback(err);
+ }
+
+ callback(null, self._authors);
+ });
+ } else {
+ callback(null, self._authors);
+ }
+}
+
+PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool) {
+ //unpack
+ var unpacked = Changeset.unpack(changeset);
+
+ var iterator = Changeset.opIterator(unpacked.ops);
+ var assem = Changeset.opAssembler();
+
+ //create deleted attribs
+ var authorAttrib = apool.putAttrib(["author", author || ""]);
+ var deletedAttrib = apool.putAttrib(["removed", true]);
+ var attribs = "*" + Changeset.numToString(authorAttrib) + "*" + Changeset.numToString(deletedAttrib);
+
+ //iteratore over the operators of the changeset
+ while(iterator.hasNext()){
+ var operator = iterator.next();
+
+ //this is a delete operator, extend it with the author
+ if(operator.opcode === "-"){
+ operator.attribs = attribs;
+ }
+ //this is operator changes only attributes, let's mark which author did that
+ else if(operator.opcode === "=" && operator.attribs){
+ operator.attribs+="*"+Changeset.numToString(authorAttrib);
+ }
+
+ //append the new operator to our assembler
+ assem.append(operator);
+ }
+
+ //return the modified changeset
+ return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
+}
+
+//this method is 80% like Changeset.inverse. I just changed so instead of reverting, it adds deletions and attribute changes to to the atext.
+PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) {
+ var lines = Changeset.splitTextLines(startAText.text);
+ var alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text);
+
+ // lines and alines are what the exports 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 = [];
+
+ //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
--- /dev/null
+++ b/src/static/img/gritter.png
Binary files 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,10 +1,4 @@
/**
- * 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)
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -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("<b>"+authorName+"</b>" + ": " + 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("<b>"+authorName+"</b>" + ": " + 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: '<div class="gritter-close"></div>',
+ _tpl_title: '<span class="gritter-title">[[title]]</span>',
+ _tpl_item: '<div id="gritter-item-[[number]]" class="gritter-item-wrapper [[item_class]]" style="display:none"><div class="gritter-top"></div><div class="gritter-item">[[close]][[image]]<div class="[[class_name]]">[[title]]<p>[[text]]</p></div><div style="clear:both"></div></div><div class="gritter-bottom"></div></div>',
+ _tpl_wrap: '<div id="gritter-notice-wrapper"></div>',
+
+ /**
+ * 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 != '') ? '<img src="' + image + '" class="gritter-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)
- {
- if(IsnoColors == "true")
- {
- 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)
+
+ for(var i = 0; i < getParameters.length; i++)
{
- if(lang !== "")
+ var setting = getParameters[i];
+ var value = params[setting.name];
+
+ if(value && (value == setting.checkVal || setting.checkVal == null))
{
- 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(); %>
</div>
- <div id="chatthrob"></div>
-
<div id="chaticon" onclick="chat.show();return false;">
<span id="chatlabel" data-l10n-id="pad.chat"></span>
<span class="buttonicon buttonicon-chat"></span>