Merge branch 'develop' into admin-index-plugable

This commit is contained in:
Swen 2013-01-30 20:16:51 +01:00
commit 1f348d911f
22 changed files with 1595 additions and 105 deletions

View File

@ -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 # 1.2.6
* Fix: Package file UeberDB reference * Fix: Package file UeberDB reference
* New #users EEJS block for plugins * New #users EEJS block for plugins

View File

@ -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 returns the list of users that are currently editing this pad
*Example returns:* *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: []}}` * `{code: 0, message:"ok", data: {padUsers: []}}`
#### deletePad(padID) #### deletePad(padID)

View File

@ -14,7 +14,7 @@
"pad.toolbar.clearAuthorship.title": "Clear Authorship Colors", "pad.toolbar.clearAuthorship.title": "Clear Authorship Colors",
"pad.toolbar.import_export.title": "Import/Export from/to different file formats", "pad.toolbar.import_export.title": "Import/Export from/to different file formats",
"pad.toolbar.timeslider.title": "Timeslider", "pad.toolbar.timeslider.title": "Timeslider",
"pad.toolbar.savedRevision.title": "Saved Revisions", "pad.toolbar.savedRevision.title": "Save Revision",
"pad.toolbar.settings.title": "Settings", "pad.toolbar.settings.title": "Settings",
"pad.toolbar.embed.title": "Embed this pad", "pad.toolbar.embed.title": "Embed this pad",
"pad.toolbar.showusers.title": "Show the users on this pad", "pad.toolbar.showusers.title": "Show the users on this pad",

View File

@ -30,6 +30,7 @@ var async = require("async");
var exportHtml = require("../utils/ExportHtml"); var exportHtml = require("../utils/ExportHtml");
var importHtml = require("../utils/ImportHtml"); var importHtml = require("../utils/ImportHtml");
var cleanText = require("./Pad").cleanText; var cleanText = require("./Pad").cleanText;
var PadDiff = require("../utils/padDiff");
/**********************/ /**********************/
/**GROUP FUNCTIONS*****/ /**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 */ /** INTERNAL HELPER FUNCTIONS */
/******************************/ /******************************/

View File

@ -24,6 +24,10 @@ var db = require("./DB").db;
var async = require("async"); var async = require("async");
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; 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 * Checks if the author exists
*/ */

View File

@ -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) { Pad.prototype.getKeyRevisionNumber = function getKeyRevisionNumber(revNum) {
return Math.floor(revNum / 100) * 100; return Math.floor(revNum / 100) * 100;
}; };

View File

@ -56,7 +56,6 @@ var padList = {
}); });
} }
}); });
return this;
}, },
/** /**
* Returns all pads in alphabetical order as array. * Returns all pads in alphabetical order as array.

View File

@ -180,6 +180,7 @@ var version =
, "deleteGroup" : ["groupID"] , "deleteGroup" : ["groupID"]
, "listPads" : ["groupID"] , "listPads" : ["groupID"]
, "listAllPads" : [] , "listAllPads" : []
, "createDiffHTML" : ["padID", "startRev", "endRev"]
, "createPad" : ["padID", "text"] , "createPad" : ["padID", "text"]
, "createGroupPad" : ["groupID", "padName", "text"] , "createGroupPad" : ["groupID", "padName", "text"]
, "createAuthor" : ["name"] , "createAuthor" : ["name"]

View File

@ -210,6 +210,7 @@ exports.handleMessage = function(client, message)
} else if (message.data.type == "SAVE_REVISION") { } else if (message.data.type == "SAVE_REVISION") {
handleSaveRevisionMessage(client, message); handleSaveRevisionMessage(client, message);
} else if (message.data.type == "CLIENT_MESSAGE" && } else if (message.data.type == "CLIENT_MESSAGE" &&
message.data.payload != null &&
message.data.payload.type == "suggestUserName") { message.data.payload.type == "suggestUserName") {
handleSuggestUserName(client, message); handleSuggestUserName(client, message);
} else { } else {
@ -473,6 +474,11 @@ function handleSuggestUserName(client, message)
function handleUserInfoUpdate(client, message) function handleUserInfoUpdate(client, message)
{ {
//check if all ok //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) if(message.data.userInfo.colorId == null)
{ {
messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!"); messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!");
@ -1028,7 +1034,7 @@ function handleClientReady(client, message)
"globalPadId": message.padId, "globalPadId": message.padId,
"time": currentTime, "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", "clientIp": "127.0.0.1",
"userIsGuest": true, "userIsGuest": true,
"userColor": authorColorId, "userColor": authorColorId,
@ -1536,6 +1542,7 @@ exports.padUsers = function (padID, callback) {
} }
var aid = sessioninfos[ix].author; var aid = sessioninfos[ix].author;
authorManager.getAuthor( aid, function ( err, author ) { authorManager.getAuthor( aid, function ( err, author ) {
author.id = aid;
authors.push( author ); authors.push( author );
if ( authors.length === pad2sessions[padID].length ) { if ( authors.length === pad2sessions[padID].length ) {
callback(null, {padUsers: authors}); callback(null, {padUsers: authors});

View File

@ -1,12 +1,12 @@
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS, * distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -91,8 +91,9 @@ function getPadHTML(pad, revNum, callback)
} }
exports.getPadHTML = getPadHTML; exports.getPadHTML = getPadHTML;
exports.getHTMLFromAtext = getHTMLFromAtext;
function getHTMLFromAtext(pad, atext) function getHTMLFromAtext(pad, atext, authorColors)
{ {
var apool = pad.apool(); var apool = pad.apool();
var textLines = atext.text.slice(0, -1).split('\n'); 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 tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
var anumMap = {}; 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) 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> // <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
var taker = Changeset.stringIterator(text); var taker = Changeset.stringIterator(text);
var assem = Changeset.stringAssembler(); var assem = Changeset.stringAssembler();
var openTags = []; 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) function emitOpenTag(i)
{ {
openTags.unshift(i); openTags.unshift(i);
assem.append('<'); var spanClass = getSpanClassFor(i);
assem.append(tags[i]);
assem.append('>'); if(spanClass){
assem.append('<span class="');
assem.append(spanClass);
assem.append('">');
} else {
assem.append('<');
assem.append(tags[i]);
assem.append('>');
}
} }
function emitCloseTag(i) function emitCloseTag(i)
{ {
openTags.shift(); openTags.shift();
assem.append('</'); var spanClass = getSpanClassFor(i);
assem.append(tags[i]);
assem.append('>'); if(spanClass){
assem.append('</span>');
} else {
assem.append('</');
assem.append(tags[i]);
assem.append('>');
}
} }
function orderdCloseTags(tags2close) function orderdCloseTags(tags2close)
@ -303,7 +371,7 @@ function getHTMLFromAtext(pad, atext)
return _processSpaces(assem.toString()); return _processSpaces(assem.toString());
} // end getLineHTML } // end getLineHTML
var pieces = []; var pieces = [css];
// Need to deal with constraints imposed on HTML lists; can // Need to deal with constraints imposed on HTML lists; can
// only gain one level of nesting at once, can't change type // only gain one level of nesting at once, can't change type

554
src/node/utils/padDiff.js Normal file
View File

@ -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;

View File

@ -14,6 +14,7 @@
, "pad_savedrevs.js" , "pad_savedrevs.js"
, "pad_connectionstatus.js" , "pad_connectionstatus.js"
, "chat.js" , "chat.js"
, "gritter.js"
, "$tinycon/tinycon.js" , "$tinycon/tinycon.js"
, "excanvas.js" , "excanvas.js"
, "farbtastic.js" , "farbtastic.js"

View File

@ -46,5 +46,5 @@
"engines" : { "node" : ">=0.6.0", "engines" : { "node" : ">=0.6.0",
"npm" : ">=1.0" "npm" : ">=1.0"
}, },
"version" : "1.2.6" "version" : "1.2.7"
} }

View File

@ -925,3 +925,95 @@ input[type=checkbox] {
#wrongPassword{ #wrongPassword{
display:none; 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 */

BIN
src/static/img/gritter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -2182,3 +2182,121 @@ exports.followAttributes = function (att1, att2, pool) {
} }
return buf.toString(); 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;
}
}
}
};

View File

@ -107,6 +107,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
{ {
newpos = Number(newpos); newpos = Number(newpos);
if (newpos < 0 || newpos > sliderLength) return; if (newpos < 0 || newpos > sliderLength) return;
window.location.hash = "#" + newpos;
$("#ui-slider-handle").css('left', newpos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)); $("#ui-slider-handle").css('left', newpos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0));
$("a.tlink").map(function() $("a.tlink").map(function()
{ {
@ -481,6 +482,18 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
} }
$("#timeslider").show(); $("#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); setSliderLength(clientVars.collab_client_vars.rev);
setSliderPosition(clientVars.collab_client_vars.rev); setSliderPosition(clientVars.collab_client_vars.rev);

View File

@ -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) * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
* *
@ -22,7 +16,6 @@
var padutils = require('./pad_utils').padutils; var padutils = require('./pad_utils').padutils;
var padcookie = require('./pad_cookie').padcookie; var padcookie = require('./pad_cookie').padcookie;
var Tinycon = require('tinycon/tinycon'); var Tinycon = require('tinycon/tinycon');
var chat = (function() var chat = (function()
@ -36,6 +29,7 @@ var chat = (function()
{ {
$("#chaticon").hide(); $("#chaticon").hide();
$("#chatbox").show(); $("#chatbox").show();
$("#gritter-notice-wrapper").hide();
self.scrollDown(); self.scrollDown();
chatMentions = 0; chatMentions = 0;
Tinycon.setBubble(0); Tinycon.setBubble(0);
@ -62,6 +56,8 @@ var chat = (function()
$("#chatcounter").text("0"); $("#chatcounter").text("0");
$("#chaticon").show(); $("#chaticon").show();
$("#chatbox").hide(); $("#chatbox").hide();
$.gritter.removeAll();
$("#gritter-notice-wrapper").show();
}, },
scrollDown: function() scrollDown: function()
{ {
@ -122,7 +118,7 @@ var chat = (function()
$("#chattext").append(html); $("#chattext").append(html);
//should we increment the counter?? //should we increment the counter??
if(increment) if(increment && !isHistoryAdd)
{ {
var count = Number($("#chatcounter").text()); var count = Number($("#chatcounter").text());
count++; count++;
@ -130,17 +126,44 @@ var chat = (function()
// is the users focus already in the chatbox? // is the users focus already in the chatbox?
var alreadyFocused = $("#chatinput").is(":focus"); var alreadyFocused = $("#chatinput").is(":focus");
// does the user already have the chatbox open?
var chatOpen = $("#chatbox").is(":visible");
$("#chatcounter").text(count); $("#chatcounter").text(count);
// chat throb stuff -- Just make it throw for twice as long // 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 { // 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++; chatMentions++;
Tinycon.setBubble(chatMentions); Tinycon.setBubble(chatMentions);
} }
else 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 // Clear the chat mentions when the user clicks on the chat input box

417
src/static/js/gritter.js Normal file
View File

@ -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);

View File

@ -48,6 +48,7 @@ var colorutils = require('./colorutils').colorutils;
var createCookie = require('./pad_utils').createCookie; var createCookie = require('./pad_utils').createCookie;
var readCookie = require('./pad_utils').readCookie; var readCookie = require('./pad_utils').readCookie;
var randomString = require('./pad_utils').randomString; var randomString = require('./pad_utils').randomString;
var gritter = require('./gritter').gritter;
var hooks = require('./pluginfw/hooks'); var hooks = require('./pluginfw/hooks');
@ -101,86 +102,39 @@ function randomString()
return "t." + 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() function getParams()
{ {
var params = getUrlVars() var params = getUrlVars()
var showControls = params["showControls"];
var showChat = params["showChat"]; for(var i = 0; i < getParameters.length; i++)
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") var setting = getParameters[i];
var value = params[setting.name];
if(value && (value == setting.checkVal || setting.checkVal == null))
{ {
settings.noColors = true; setting.callback(value);
$('#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']);
} }
} }
} }
@ -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 = { var pad = {
// don't access these directly from outside this file, except // don't access these directly from outside this file, except
// for debugging // for debugging

View File

@ -365,8 +365,6 @@
<% e.end_block(); %> <% e.end_block(); %>
</div> </div>
<div id="chatthrob"></div>
<div id="chaticon" onclick="chat.show();return false;"> <div id="chaticon" onclick="chat.show();return false;">
<span id="chatlabel" data-l10n-id="pad.chat"></span> <span id="chatlabel" data-l10n-id="pad.chat"></span>
<span class="buttonicon buttonicon-chat"></span> <span class="buttonicon buttonicon-chat"></span>

View File

@ -57,4 +57,95 @@ describe("timeslider", function(){
}, 6000); }, 6000);
}, revs*timePerRev); }, 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);
});
}); });