= curChar) {
+ curLineNextOp.chars -= (curChar - indexIntoLine);
+ done = true;
+ } else {
+ indexIntoLine += curLineNextOp.chars;
+ }
+ }
+ }
+
+ while (numChars > 0) {
+ if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) {
+ curLine++;
+ curChar = 0;
+ curLineOpIterLine = curLine;
+ curLineNextOp.chars = 0;
+ curLineOpIter = Changeset.opIterator(alines_get(curLine));
+ }
+ if (!curLineNextOp.chars) {
+ curLineOpIter.next(curLineNextOp);
+ }
+ var charsToUse = Math.min(numChars, curLineNextOp.chars);
+ func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0);
+ numChars -= charsToUse;
+ curLineNextOp.chars -= charsToUse;
+ curChar += charsToUse;
+ }
+
+ if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) {
+ curLine++;
+ curChar = 0;
+ }
+ }
+
+ function skip(N, L) {
+ if (L) {
+ curLine += L;
+ curChar = 0;
+ } else {
+ if (curLineOpIter && curLineOpIterLine == curLine) {
+ consumeAttribRuns(N, function () {});
+ } else {
+ curChar += N;
+ }
+ }
+ }
+
+ function nextText(numChars) {
+ var len = 0;
+ var assem = Changeset.stringAssembler();
+ var firstString = lines_get(curLine).substring(curChar);
+ len += firstString.length;
+ assem.append(firstString);
+
+ var lineNum = curLine + 1;
+ while (len < numChars) {
+ var nextString = lines_get(lineNum);
+ len += nextString.length;
+ assem.append(nextString);
+ lineNum++;
+ }
+
+ return assem.toString().substring(0, numChars);
+ }
+
+ function cachedStrFunc(func) {
+ var cache = {};
+ return function (s) {
+ if (!cache[s]) {
+ cache[s] = func(s);
+ }
+ return cache[s];
+ };
+ }
+
+ var attribKeys = [];
+ var attribValues = [];
+
+ //iterate over all operators of this changeset
+ while (csIter.hasNext()) {
+ var csOp = csIter.next();
+
+ if (csOp.opcode == '=') {
+ var textBank = nextText(csOp.chars);
+
+ // decide if this equal operator is an attribution change or not. We can see this by checkinf if attribs is set.
+ // If the text this operator applies to is only a star, than this is a false positive and should be ignored
+ if (csOp.attribs && textBank != "*") {
+ var deletedAttrib = apool.putAttrib(["removed", true]);
+ var authorAttrib = apool.putAttrib(["author", ""]);;
+
+ attribKeys.length = 0;
+ attribValues.length = 0;
+ Changeset.eachAttribNumber(csOp.attribs, function (n) {
+ attribKeys.push(apool.getAttribKey(n));
+ attribValues.push(apool.getAttribValue(n));
+
+ if(apool.getAttribKey(n) === "author"){
+ authorAttrib = n;
+ };
+ });
+
+ var undoBackToAttribs = cachedStrFunc(function (attribs) {
+ var backAttribs = [];
+ for (var i = 0; i < attribKeys.length; i++) {
+ var appliedKey = attribKeys[i];
+ var appliedValue = attribValues[i];
+ var oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, apool);
+ if (appliedValue != oldValue) {
+ backAttribs.push([appliedKey, oldValue]);
+ }
+ }
+ return Changeset.makeAttribsString('=', backAttribs, apool);
+ });
+
+ var oldAttribsAddition = "*" + Changeset.numToString(deletedAttrib) + "*" + Changeset.numToString(authorAttrib);
+
+ var textLeftToProcess = textBank;
+
+ while(textLeftToProcess.length > 0){
+ //process till the next line break or process only one line break
+ var lengthToProcess = textLeftToProcess.indexOf("\n");
+ var lineBreak = false;
+ switch(lengthToProcess){
+ case -1:
+ lengthToProcess=textLeftToProcess.length;
+ break;
+ case 0:
+ lineBreak = true;
+ lengthToProcess=1;
+ break;
+ }
+
+ //get the text we want to procceed in this step
+ var processText = textLeftToProcess.substr(0, lengthToProcess);
+ textLeftToProcess = textLeftToProcess.substr(lengthToProcess);
+
+ if(lineBreak){
+ builder.keep(1, 1); //just skip linebreaks, don't do a insert + keep for a linebreak
+
+ //consume the attributes of this linebreak
+ consumeAttribRuns(1, function(){});
+ } else {
+ //add the old text via an insert, but add a deletion attribute + the author attribute of the author who deleted it
+ var textBankIndex = 0;
+ consumeAttribRuns(lengthToProcess, function (len, attribs, endsLine) {
+ //get the old attributes back
+ var attribs = (undoBackToAttribs(attribs) || "") + oldAttribsAddition;
+
+ builder.insert(processText.substr(textBankIndex, len), attribs);
+ textBankIndex += len;
+ });
+
+ builder.keep(lengthToProcess, 0);
+ }
+ }
+ } else {
+ skip(csOp.chars, csOp.lines);
+ builder.keep(csOp.chars, csOp.lines);
+ }
+ } else if (csOp.opcode == '+') {
+ builder.keep(csOp.chars, csOp.lines);
+ } else if (csOp.opcode == '-') {
+ var textBank = nextText(csOp.chars);
+ var textBankIndex = 0;
+
+ consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) {
+ builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs);
+ textBankIndex += len;
+ });
+ }
+ }
+
+ return Changeset.checkRep(builder.toString());
+};
+
+//export the constructor
+module.exports = PadDiff;
diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json
index 080da442..b010f851 100644
--- a/src/node/utils/tar.json
+++ b/src/node/utils/tar.json
@@ -14,6 +14,7 @@
, "pad_savedrevs.js"
, "pad_connectionstatus.js"
, "chat.js"
+ , "gritter.js"
, "$tinycon/tinycon.js"
, "excanvas.js"
, "farbtastic.js"
diff --git a/src/package.json b/src/package.json
index fd48bce6..6d05e6a2 100644
--- a/src/package.json
+++ b/src/package.json
@@ -46,5 +46,5 @@
"engines" : { "node" : ">=0.6.0",
"npm" : ">=1.0"
},
- "version" : "1.2.6"
+ "version" : "1.2.7"
}
diff --git a/src/static/css/pad.css b/src/static/css/pad.css
index bbbadbc1..6034b5ed 100644
--- a/src/static/css/pad.css
+++ b/src/static/css/pad.css
@@ -925,3 +925,95 @@ input[type=checkbox] {
#wrongPassword{
display:none;
}
+
+/* gritter stuff */
+#gritter-notice-wrapper {
+ position:fixed;
+ top:20px;
+ right:20px;
+ width:301px;
+ z-index:9999;
+}
+#gritter-notice-wrapper.bottom-right {
+ top: auto;
+ left: auto;
+ bottom: 20px;
+ right: 20px;
+}
+.gritter-item-wrapper {
+ position:relative;
+ margin:0 0 10px 0;
+}
+
+.gritter-top {
+ background:url(../../static/img/gritter.png) no-repeat left -30px;
+ height:10px;
+}
+.hover .gritter-top {
+ background-position:right -30px;
+}
+.gritter-bottom {
+ background:url(../../static/img/gritter.png) no-repeat left bottom;
+ height:8px;
+ margin:0;
+}
+.hover .gritter-bottom {
+ background-position: bottom right;
+}
+.gritter-item {
+ display:block;
+ background:url(../../static/img/gritter.png) no-repeat left -40px;
+ color:#eee;
+ padding:2px 11px 8px 11px;
+ font-size: 11px;
+ font-family:verdana;
+}
+.hover .gritter-item {
+ background-position:right -40px;
+}
+.gritter-item p {
+ padding:0;
+ margin:0;
+}
+.gritter-close {
+ display:none;
+ position:absolute;
+ top:5px;
+ left:3px;
+ background:url('../../static/img/gritter.png') no-repeat left top;
+ cursor:pointer;
+ width:30px;
+ height:30px;
+}
+.gritter-title {
+ font-size:14px;
+ font-weight:bold;
+ padding:0 0 7px 0;
+ display:block;
+ text-shadow:1px 1px 0 #000; /* Not supported by IE :( */
+}
+.gritter-image {
+ width:48px;
+ height:48px;
+ float:left;
+}
+.gritter-with-image,
+.gritter-without-image {
+ padding:0 0 5px 0;
+}
+.gritter-with-image {
+ width:220px;
+ float:right;
+}
+/* for the light (white) version of the gritter notice */
+.gritter-light .gritter-item,
+.gritter-light .gritter-bottom,
+.gritter-light .gritter-top,
+.gritter-close {
+ color: #222;
+}
+.gritter-light .gritter-title {
+ text-shadow: none;
+}
+
+/* End of gritter stuff */
diff --git a/src/static/img/gritter.png b/src/static/img/gritter.png
new file mode 100644
index 00000000..0ca3bc0a
Binary files /dev/null and b/src/static/img/gritter.png differ
diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js
index cfea4362..b1604212 100644
--- a/src/static/js/Changeset.js
+++ b/src/static/js/Changeset.js
@@ -2182,3 +2182,121 @@ exports.followAttributes = function (att1, att2, pool) {
}
return buf.toString();
};
+
+exports.composeWithDeletions = function (cs1, cs2, pool) {
+ var unpacked1 = exports.unpack(cs1);
+ var unpacked2 = exports.unpack(cs2);
+ var len1 = unpacked1.oldLen;
+ var len2 = unpacked1.newLen;
+ exports.assert(len2 == unpacked2.oldLen, "mismatched composition");
+ var len3 = unpacked2.newLen;
+ var bankIter1 = exports.stringIterator(unpacked1.charBank);
+ var bankIter2 = exports.stringIterator(unpacked2.charBank);
+ var bankAssem = exports.stringAssembler();
+
+ var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) {
+ var op1code = op1.opcode;
+ var op2code = op2.opcode;
+ if (op1code == '+' && op2code == '-') {
+ bankIter1.skip(Math.min(op1.chars, op2.chars));
+ }
+ exports._slicerZipperFuncWithDeletions(op1, op2, opOut, pool);
+ if (opOut.opcode == '+') {
+ if (op2code == '+') {
+ bankAssem.append(bankIter2.take(opOut.chars));
+ } else {
+ bankAssem.append(bankIter1.take(opOut.chars));
+ }
+ }
+ });
+
+ return exports.pack(len1, len3, newOps, bankAssem.toString());
+};
+
+// This function is 95% like _slicerZipperFunc, we just changed two lines to ensure it merges the attribs of deletions properly.
+// This is necassary for correct paddiff. But to ensure these changes doesn't affect anything else, we've created a seperate function only used for paddiffs
+exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) {
+ // attOp is the op from the sequence that is being operated on, either an
+ // attribution string or the earlier of two exportss being composed.
+ // pool can be null if definitely not needed.
+ //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
+ if (attOp.opcode == '-') {
+ exports.copyOp(attOp, opOut);
+ attOp.opcode = '';
+ } else if (!attOp.opcode) {
+ exports.copyOp(csOp, opOut);
+ csOp.opcode = '';
+ } else {
+ switch (csOp.opcode) {
+ case '-':
+ {
+ if (csOp.chars <= attOp.chars) {
+ // delete or delete part
+ if (attOp.opcode == '=') {
+ opOut.opcode = '-';
+ opOut.chars = csOp.chars;
+ opOut.lines = csOp.lines;
+ opOut.attribs = csOp.attribs; //changed by yammer
+ }
+ attOp.chars -= csOp.chars;
+ attOp.lines -= csOp.lines;
+ csOp.opcode = '';
+ if (!attOp.chars) {
+ attOp.opcode = '';
+ }
+ } else {
+ // delete and keep going
+ if (attOp.opcode == '=') {
+ opOut.opcode = '-';
+ opOut.chars = attOp.chars;
+ opOut.lines = attOp.lines;
+ opOut.attribs = csOp.attribs; //changed by yammer
+ }
+ csOp.chars -= attOp.chars;
+ csOp.lines -= attOp.lines;
+ attOp.opcode = '';
+ }
+ break;
+ }
+ case '+':
+ {
+ // insert
+ exports.copyOp(csOp, opOut);
+ csOp.opcode = '';
+ break;
+ }
+ case '=':
+ {
+ if (csOp.chars <= attOp.chars) {
+ // keep or keep part
+ opOut.opcode = attOp.opcode;
+ opOut.chars = csOp.chars;
+ opOut.lines = csOp.lines;
+ opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
+ csOp.opcode = '';
+ attOp.chars -= csOp.chars;
+ attOp.lines -= csOp.lines;
+ if (!attOp.chars) {
+ attOp.opcode = '';
+ }
+ } else {
+ // keep and keep going
+ opOut.opcode = attOp.opcode;
+ opOut.chars = attOp.chars;
+ opOut.lines = attOp.lines;
+ opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
+ attOp.opcode = '';
+ csOp.chars -= attOp.chars;
+ csOp.lines -= attOp.lines;
+ }
+ break;
+ }
+ case '':
+ {
+ exports.copyOp(attOp, opOut);
+ attOp.opcode = '';
+ break;
+ }
+ }
+ }
+};
diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js
index 221666de..08ac08b5 100644
--- a/src/static/js/broadcast_slider.js
+++ b/src/static/js/broadcast_slider.js
@@ -107,6 +107,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
{
newpos = Number(newpos);
if (newpos < 0 || newpos > sliderLength) return;
+ window.location.hash = "#" + newpos;
$("#ui-slider-handle").css('left', newpos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0));
$("a.tlink").map(function()
{
@@ -481,6 +482,18 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
}
$("#timeslider").show();
+
+ var startPos = clientVars.collab_client_vars.rev;
+ if(window.location.hash.length > 1)
+ {
+ var hashRev = Number(window.location.hash.substr(1));
+ if(!isNaN(hashRev))
+ {
+ // this is necessary because of the socket.io-event which loads the changesets
+ setTimeout(function() { setSliderPosition(hashRev); }, 1);
+ }
+ }
+
setSliderLength(clientVars.collab_client_vars.rev);
setSliderPosition(clientVars.collab_client_vars.rev);
diff --git a/src/static/js/chat.js b/src/static/js/chat.js
index 205294a8..2dff2edf 100644
--- a/src/static/js/chat.js
+++ b/src/static/js/chat.js
@@ -1,9 +1,3 @@
-/**
- * This code is mostly from the old Etherpad. Please help us to comment this code.
- * This helps other people to understand this code better and helps them to improve it.
- * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
- */
-
/**
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
@@ -22,7 +16,6 @@
var padutils = require('./pad_utils').padutils;
var padcookie = require('./pad_cookie').padcookie;
-
var Tinycon = require('tinycon/tinycon');
var chat = (function()
@@ -36,6 +29,7 @@ var chat = (function()
{
$("#chaticon").hide();
$("#chatbox").show();
+ $("#gritter-notice-wrapper").hide();
self.scrollDown();
chatMentions = 0;
Tinycon.setBubble(0);
@@ -62,6 +56,8 @@ var chat = (function()
$("#chatcounter").text("0");
$("#chaticon").show();
$("#chatbox").hide();
+ $.gritter.removeAll();
+ $("#gritter-notice-wrapper").show();
},
scrollDown: function()
{
@@ -122,7 +118,7 @@ var chat = (function()
$("#chattext").append(html);
//should we increment the counter??
- if(increment)
+ if(increment && !isHistoryAdd)
{
var count = Number($("#chatcounter").text());
count++;
@@ -130,17 +126,44 @@ var chat = (function()
// is the users focus already in the chatbox?
var alreadyFocused = $("#chatinput").is(":focus");
+ // does the user already have the chatbox open?
+ var chatOpen = $("#chatbox").is(":visible");
+
$("#chatcounter").text(count);
// chat throb stuff -- Just make it throw for twice as long
- if(wasMentioned && !alreadyFocused && !isHistoryAdd)
+ if(wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen)
{ // If the user was mentioned show for twice as long and flash the browser window
- $('#chatthrob').html(""+authorName+"" + ": " + text).show().delay(4000).hide(400);
+ $.gritter.add({
+ // (string | mandatory) the heading of the notification
+ title: authorName,
+ // (string | mandatory) the text inside the notification
+ text: text,
+ // (bool | optional) if you want it to fade out on its own or just sit there
+ sticky: true,
+ // (int | optional) the time you want it to be alive for before fading out
+ time: '2000'
+ });
+
chatMentions++;
Tinycon.setBubble(chatMentions);
}
else
{
- $('#chatthrob').html(""+authorName+"" + ": " + text).show().delay(2000).hide(400);
+ if(!chatOpen){
+ $.gritter.add({
+ // (string | mandatory) the heading of the notification
+ title: authorName,
+ // (string | mandatory) the text inside the notification
+ text: text,
+
+ // (bool | optional) if you want it to fade out on its own or just sit there
+ sticky: false,
+ // (int | optional) the time you want it to be alive for before fading out
+ time: '4000'
+ });
+ Tinycon.setBubble(count);
+
+ }
}
}
// Clear the chat mentions when the user clicks on the chat input box
diff --git a/src/static/js/gritter.js b/src/static/js/gritter.js
new file mode 100644
index 00000000..c32cc758
--- /dev/null
+++ b/src/static/js/gritter.js
@@ -0,0 +1,417 @@
+/*
+ * Gritter for jQuery
+ * http://www.boedesign.com/
+ *
+ * Copyright (c) 2012 Jordan Boesch
+ * Dual licensed under the MIT and GPL licenses.
+ *
+ * Date: February 24, 2012
+ * Version: 1.7.4
+ */
+
+(function($){
+ /**
+ * Set it up as an object under the jQuery namespace
+ */
+ $.gritter = {};
+
+ /**
+ * Set up global options that the user can over-ride
+ */
+ $.gritter.options = {
+ position: '',
+ class_name: '', // could be set to 'gritter-light' to use white notifications
+ fade_in_speed: 'medium', // how fast notifications fade in
+ fade_out_speed: 1000, // how fast the notices fade out
+ time: 6000 // hang on the screen for...
+ }
+
+ /**
+ * Add a gritter notification to the screen
+ * @see Gritter#add();
+ */
+ $.gritter.add = function(params){
+
+ try {
+ return Gritter.add(params || {});
+ } catch(e) {
+
+ var err = 'Gritter Error: ' + e;
+ (typeof(console) != 'undefined' && console.error) ?
+ console.error(err, params) :
+ alert(err);
+
+ }
+
+ }
+
+ /**
+ * Remove a gritter notification from the screen
+ * @see Gritter#removeSpecific();
+ */
+ $.gritter.remove = function(id, params){
+ Gritter.removeSpecific(id, params || {});
+ }
+
+ /**
+ * Remove all notifications
+ * @see Gritter#stop();
+ */
+ $.gritter.removeAll = function(params){
+ Gritter.stop(params || {});
+ }
+
+ /**
+ * Big fat Gritter object
+ * @constructor (not really since its object literal)
+ */
+ var Gritter = {
+
+ // Public - options to over-ride with $.gritter.options in "add"
+ position: '',
+ fade_in_speed: '',
+ fade_out_speed: '',
+ time: '',
+
+ // Private - no touchy the private parts
+ _custom_timer: 0,
+ _item_count: 0,
+ _is_setup: 0,
+ _tpl_close: '',
+ _tpl_title: '[[title]]',
+ _tpl_item: '',
+ _tpl_wrap: '',
+
+ /**
+ * Add a gritter notification to the screen
+ * @param {Object} params The object that contains all the options for drawing the notification
+ * @return {Integer} The specific numeric id to that gritter notification
+ */
+ add: function(params){
+ // Handle straight text
+ if(typeof(params) == 'string'){
+ params = {text:params};
+ }
+
+ // We might have some issues if we don't have a title or text!
+ if(!params.text){
+ throw 'You must supply "text" parameter.';
+ }
+
+ // Check the options and set them once
+ if(!this._is_setup){
+ this._runSetup();
+ }
+
+ // Basics
+ var title = params.title,
+ text = params.text,
+ image = params.image || '',
+ sticky = params.sticky || false,
+ item_class = params.class_name || $.gritter.options.class_name,
+ position = $.gritter.options.position,
+ time_alive = params.time || '';
+
+ this._verifyWrapper();
+
+ this._item_count++;
+ var number = this._item_count,
+ tmp = this._tpl_item;
+
+ // Assign callbacks
+ $(['before_open', 'after_open', 'before_close', 'after_close']).each(function(i, val){
+ Gritter['_' + val + '_' + number] = ($.isFunction(params[val])) ? params[val] : function(){}
+ });
+
+ // Reset
+ this._custom_timer = 0;
+
+ // A custom fade time set
+ if(time_alive){
+ this._custom_timer = time_alive;
+ }
+
+ var image_str = (image != '') ? '' : '',
+ class_name = (image != '') ? 'gritter-with-image' : 'gritter-without-image';
+
+ // String replacements on the template
+ if(title){
+ title = this._str_replace('[[title]]',title,this._tpl_title);
+ }else{
+ title = '';
+ }
+
+ tmp = this._str_replace(
+ ['[[title]]', '[[text]]', '[[close]]', '[[image]]', '[[number]]', '[[class_name]]', '[[item_class]]'],
+ [title, text, this._tpl_close, image_str, this._item_count, class_name, item_class], tmp
+ );
+
+ // If it's false, don't show another gritter message
+ if(this['_before_open_' + number]() === false){
+ return false;
+ }
+
+ $('#gritter-notice-wrapper').addClass(position).append(tmp);
+
+ var item = $('#gritter-item-' + this._item_count);
+
+ item.fadeIn(this.fade_in_speed, function(){
+ Gritter['_after_open_' + number]($(this));
+ });
+
+ if(!sticky){
+ this._setFadeTimer(item, number);
+ }
+
+ // Bind the hover/unhover states
+ $(item).bind('mouseenter mouseleave', function(event){
+ if(event.type == 'mouseenter'){
+ if(!sticky){
+ Gritter._restoreItemIfFading($(this), number);
+ }
+ }
+ else {
+ if(!sticky){
+ Gritter._setFadeTimer($(this), number);
+ }
+ }
+ Gritter._hoverState($(this), event.type);
+ });
+
+ // Clicking (X) makes the perdy thing close
+ $(item).find('.gritter-close').click(function(){
+ Gritter.removeSpecific(number, {}, null, true);
+ });
+
+ return number;
+
+ },
+
+ /**
+ * If we don't have any more gritter notifications, get rid of the wrapper using this check
+ * @private
+ * @param {Integer} unique_id The ID of the element that was just deleted, use it for a callback
+ * @param {Object} e The jQuery element that we're going to perform the remove() action on
+ * @param {Boolean} manual_close Did we close the gritter dialog with the (X) button
+ */
+ _countRemoveWrapper: function(unique_id, e, manual_close){
+
+ // Remove it then run the callback function
+ e.remove();
+ this['_after_close_' + unique_id](e, manual_close);
+
+ // Check if the wrapper is empty, if it is.. remove the wrapper
+ if($('.gritter-item-wrapper').length == 0){
+ $('#gritter-notice-wrapper').remove();
+ }
+
+ },
+
+ /**
+ * Fade out an element after it's been on the screen for x amount of time
+ * @private
+ * @param {Object} e The jQuery element to get rid of
+ * @param {Integer} unique_id The id of the element to remove
+ * @param {Object} params An optional list of params to set fade speeds etc.
+ * @param {Boolean} unbind_events Unbind the mouseenter/mouseleave events if they click (X)
+ */
+ _fade: function(e, unique_id, params, unbind_events){
+
+ var params = params || {},
+ fade = (typeof(params.fade) != 'undefined') ? params.fade : true,
+ fade_out_speed = params.speed || this.fade_out_speed,
+ manual_close = unbind_events;
+
+ this['_before_close_' + unique_id](e, manual_close);
+
+ // If this is true, then we are coming from clicking the (X)
+ if(unbind_events){
+ e.unbind('mouseenter mouseleave');
+ }
+
+ // Fade it out or remove it
+ if(fade){
+
+ e.animate({
+ opacity: 0
+ }, fade_out_speed, function(){
+ e.animate({ height: 0 }, 300, function(){
+ Gritter._countRemoveWrapper(unique_id, e, manual_close);
+ })
+ })
+
+ }
+ else {
+
+ this._countRemoveWrapper(unique_id, e);
+
+ }
+
+ },
+
+ /**
+ * Perform actions based on the type of bind (mouseenter, mouseleave)
+ * @private
+ * @param {Object} e The jQuery element
+ * @param {String} type The type of action we're performing: mouseenter or mouseleave
+ */
+ _hoverState: function(e, type){
+
+ // Change the border styles and add the (X) close button when you hover
+ if(type == 'mouseenter'){
+
+ e.addClass('hover');
+
+ // Show close button
+ e.find('.gritter-close').show();
+
+ }
+ // Remove the border styles and hide (X) close button when you mouse out
+ else {
+
+ e.removeClass('hover');
+
+ // Hide close button
+ e.find('.gritter-close').hide();
+
+ }
+
+ },
+
+ /**
+ * Remove a specific notification based on an ID
+ * @param {Integer} unique_id The ID used to delete a specific notification
+ * @param {Object} params A set of options passed in to determine how to get rid of it
+ * @param {Object} e The jQuery element that we're "fading" then removing
+ * @param {Boolean} unbind_events If we clicked on the (X) we set this to true to unbind mouseenter/mouseleave
+ */
+ removeSpecific: function(unique_id, params, e, unbind_events){
+
+ if(!e){
+ var e = $('#gritter-item-' + unique_id);
+ }
+
+ // We set the fourth param to let the _fade function know to
+ // unbind the "mouseleave" event. Once you click (X) there's no going back!
+ this._fade(e, unique_id, params || {}, unbind_events);
+
+ },
+
+ /**
+ * If the item is fading out and we hover over it, restore it!
+ * @private
+ * @param {Object} e The HTML element to remove
+ * @param {Integer} unique_id The ID of the element
+ */
+ _restoreItemIfFading: function(e, unique_id){
+
+ clearTimeout(this['_int_id_' + unique_id]);
+ e.stop().css({ opacity: '', height: '' });
+
+ },
+
+ /**
+ * Setup the global options - only once
+ * @private
+ */
+ _runSetup: function(){
+
+ for(opt in $.gritter.options){
+ this[opt] = $.gritter.options[opt];
+ }
+ this._is_setup = 1;
+
+ },
+
+ /**
+ * Set the notification to fade out after a certain amount of time
+ * @private
+ * @param {Object} item The HTML element we're dealing with
+ * @param {Integer} unique_id The ID of the element
+ */
+ _setFadeTimer: function(e, unique_id){
+
+ var timer_str = (this._custom_timer) ? this._custom_timer : this.time;
+ this['_int_id_' + unique_id] = setTimeout(function(){
+ Gritter._fade(e, unique_id);
+ }, timer_str);
+
+ },
+
+ /**
+ * Bring everything to a halt
+ * @param {Object} params A list of callback functions to pass when all notifications are removed
+ */
+ stop: function(params){
+
+ // callbacks (if passed)
+ var before_close = ($.isFunction(params.before_close)) ? params.before_close : function(){};
+ var after_close = ($.isFunction(params.after_close)) ? params.after_close : function(){};
+
+ var wrap = $('#gritter-notice-wrapper');
+ before_close(wrap);
+ wrap.fadeOut(function(){
+ $(this).remove();
+ after_close();
+ });
+
+ },
+
+ /**
+ * An extremely handy PHP function ported to JS, works well for templating
+ * @private
+ * @param {String/Array} search A list of things to search for
+ * @param {String/Array} replace A list of things to replace the searches with
+ * @return {String} sa The output
+ */
+ _str_replace: function(search, replace, subject, count){
+
+ var i = 0, j = 0, temp = '', repl = '', sl = 0, fl = 0,
+ f = [].concat(search),
+ r = [].concat(replace),
+ s = subject,
+ ra = r instanceof Array, sa = s instanceof Array;
+ s = [].concat(s);
+
+ if(count){
+ this.window[count] = 0;
+ }
+
+ for(i = 0, sl = s.length; i < sl; i++){
+
+ if(s[i] === ''){
+ continue;
+ }
+
+ for (j = 0, fl = f.length; j < fl; j++){
+
+ temp = s[i] + '';
+ repl = ra ? (r[j] !== undefined ? r[j] : '') : r[0];
+ s[i] = (temp).split(f[j]).join(repl);
+
+ if(count && s[i] !== temp){
+ this.window[count] += (temp.length-s[i].length) / f[j].length;
+ }
+
+ }
+ }
+
+ return sa ? s : s[0];
+
+ },
+
+ /**
+ * A check to make sure we have something to wrap our notices with
+ * @private
+ */
+ _verifyWrapper: function(){
+
+ if($('#gritter-notice-wrapper').length == 0){
+ $('body').append(this._tpl_wrap);
+ }
+
+ }
+
+ }
+
+})(jQuery);
diff --git a/src/static/js/pad.js b/src/static/js/pad.js
index 64d8b42b..27dd3b73 100644
--- a/src/static/js/pad.js
+++ b/src/static/js/pad.js
@@ -48,6 +48,7 @@ var colorutils = require('./colorutils').colorutils;
var createCookie = require('./pad_utils').createCookie;
var readCookie = require('./pad_utils').readCookie;
var randomString = require('./pad_utils').randomString;
+var gritter = require('./gritter').gritter;
var hooks = require('./pluginfw/hooks');
@@ -101,86 +102,39 @@ function randomString()
return "t." + randomstring;
}
+// This array represents all GET-parameters which can be used to change a setting.
+// name: the parameter-name, eg `?noColors=true` => `noColors`
+// checkVal: the callback is only executed when
+// * the parameter was supplied and matches checkVal
+// * the parameter was supplied and checkVal is null
+// callback: the function to call when all above succeeds, `val` is the value supplied by the user
+var getParameters = [
+ { name: "noColors", checkVal: "true", callback: function(val) { settings.noColors = true; $('#clearAuthorship').hide(); } },
+ { name: "showControls", checkVal: "false", callback: function(val) { $('#editbar').hide(); $('#editorcontainer').css({"top":"0px"}); } },
+ { name: "showChat", checkVal: "false", callback: function(val) { $('#chaticon').hide(); } },
+ { name: "showLineNumbers", checkVal: "false", callback: function(val) { settings.LineNumbersDisabled = true; } },
+ { name: "useMonospaceFont", checkVal: "true", callback: function(val) { settings.useMonospaceFontGlobal = true; } },
+ // If the username is set as a parameter we should set a global value that we can call once we have initiated the pad.
+ { name: "userName", checkVal: null, callback: function(val) { settings.globalUserName = decodeURIComponent(val); } },
+ // If the userColor is set as a parameter, set a global value to use once we have initiated the pad.
+ { name: "userColor", checkVal: null, callback: function(val) { settings.globalUserColor = decodeURIComponent(val); } },
+ { name: "rtl", checkVal: "true", callback: function(val) { settings.rtlIsTrue = true } },
+ { name: "alwaysShowChat", checkVal: "true", callback: function(val) { chat.stickToScreen(); } },
+ { name: "lang", checkVal: null, callback: function(val) { window.html10n.localize([val, 'en']); } }
+];
+
function getParams()
{
var params = getUrlVars()
- var showControls = params["showControls"];
- var showChat = params["showChat"];
- var userName = params["userName"];
- var userColor = params["userColor"];
- var showLineNumbers = params["showLineNumbers"];
- var useMonospaceFont = params["useMonospaceFont"];
- var IsnoColors = params["noColors"];
- var rtl = params["rtl"];
- var alwaysShowChat = params["alwaysShowChat"];
- var lang = params["lang"];
-
- if(IsnoColors)
+
+ for(var i = 0; i < getParameters.length; i++)
{
- if(IsnoColors == "true")
+ var setting = getParameters[i];
+ var value = params[setting.name];
+
+ if(value && (value == setting.checkVal || setting.checkVal == null))
{
- settings.noColors = true;
- $('#clearAuthorship').hide();
- }
- }
- if(showControls)
- {
- if(showControls == "false")
- {
- $('#editbar').hide();
- $('#editorcontainer').css({"top":"0px"});
- }
- }
- if(showChat)
- {
- if(showChat == "false")
- {
- $('#chaticon').hide();
- }
- }
- if(showLineNumbers)
- {
- if(showLineNumbers == "false")
- {
- settings.LineNumbersDisabled = true;
- }
- }
- if(useMonospaceFont)
- {
- if(useMonospaceFont == "true")
- {
- settings.useMonospaceFontGlobal = true;
- }
- }
- if(userName)
- {
- // If the username is set as a parameter we should set a global value that we can call once we have initiated the pad.
- settings.globalUserName = decodeURIComponent(userName);
- }
- if(userColor)
- // If the userColor is set as a parameter, set a global value to use once we have initiated the pad.
- {
- settings.globalUserColor = decodeURIComponent(userColor);
- }
- if(rtl)
- {
- if(rtl == "true")
- {
- settings.rtlIsTrue = true
- }
- }
- if(alwaysShowChat)
- {
- if(alwaysShowChat == "true")
- {
- chat.stickToScreen();
- }
- }
- if(lang)
- {
- if(lang !== "")
- {
- window.html10n.localize([lang, 'en']);
+ setting.callback(value);
}
}
}
@@ -410,6 +364,13 @@ function handshake()
});
}
+$.extend($.gritter.options, {
+ position: 'bottom-right', // defaults to 'top-right' but can be 'bottom-left', 'bottom-right', 'top-left', 'top-right' (added in 1.7.1)
+ fade_in_speed: 'medium', // how fast notifications fade in (string or int)
+ fade_out_speed: 2000, // how fast the notices fade out
+ time: 6000 // hang on the screen for...
+});
+
var pad = {
// don't access these directly from outside this file, except
// for debugging
diff --git a/src/templates/pad.html b/src/templates/pad.html
index cb88c1c1..76df5133 100644
--- a/src/templates/pad.html
+++ b/src/templates/pad.html
@@ -365,8 +365,6 @@
<% e.end_block(); %>
-
-
diff --git a/tests/frontend/specs/timeslider_revisions.js b/tests/frontend/specs/timeslider_revisions.js
index af59051a..52f48764 100644
--- a/tests/frontend/specs/timeslider_revisions.js
+++ b/tests/frontend/specs/timeslider_revisions.js
@@ -57,4 +57,95 @@ describe("timeslider", function(){
}, 6000);
}, revs*timePerRev);
});
+
+ it("changes the url when clicking on the timeslider", function(done) {
+ var inner$ = helper.padInner$;
+ var chrome$ = helper.padChrome$;
+
+ // make some changes to produce 7 revisions
+ var timePerRev = 900
+ , revs = 7;
+ this.timeout(revs*timePerRev+10000);
+ for(var i=0; i < revs; i++) {
+ setTimeout(function() {
+ // enter 'a' in the first text element
+ inner$("div").first().sendkeys('a');
+ }, timePerRev*i);
+ }
+
+ setTimeout(function() {
+ // go to timeslider
+ $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider');
+
+ setTimeout(function() {
+ var timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
+ var $sliderBar = timeslider$('#ui-slider-bar');
+
+ var latestContents = timeslider$('#padcontent').text();
+
+ var oldUrl = $('#iframe-container iframe')[0].contentWindow.location.hash;
+
+ // Click somewhere on the timeslider
+ var e = new jQuery.Event('mousedown');
+ e.clientX = e.pageX = 150;
+ e.clientY = e.pageY = 60;
+ $sliderBar.trigger(e);
+
+ helper.waitFor(function(){
+ return $('#iframe-container iframe')[0].contentWindow.location.hash != oldUrl;
+ }, 6000).always(function(){
+ expect( $('#iframe-container iframe')[0].contentWindow.location.hash ).not.to.eql( oldUrl );
+ done();
+ });
+ }, 6000);
+ }, revs*timePerRev);
+ });
+
+ it("jumps to a revision given in the url", function(done) {
+ var inner$ = helper.padInner$;
+ var chrome$ = helper.padChrome$;
+ this.timeout(11000);
+ inner$("div").first().sendkeys('a');
+
+ setTimeout(function() {
+ // go to timeslider
+ $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider#0');
+ var timeslider$;
+
+ helper.waitFor(function(){
+ timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
+ return timeslider$ && timeslider$('#padcontent').text().length == 230;
+ }, 6000).always(function(){
+ expect( timeslider$('#padcontent').text().length ).to.eql( 230 );
+ done();
+ });
+ }, 2500);
+ });
+
+ it("checks the export url", function(done) {
+ var inner$ = helper.padInner$;
+ var chrome$ = helper.padChrome$;
+ this.timeout(11000);
+ inner$("div").first().sendkeys('a');
+
+ setTimeout(function() {
+ // go to timeslider
+ $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider#0');
+ var timeslider$;
+ var exportLink;
+
+ helper.waitFor(function(){
+ timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
+ if(!timeslider$)
+ return false;
+ exportLink = timeslider$('#exportplaina').attr('href');
+ if(!exportLink)
+ return false;
+ return exportLink.substr(exportLink.length - 12) == "0/export/txt";
+ }, 6000).always(function(){
+ expect( exportLink.substr(exportLink.length - 12) ).to.eql( "0/export/txt" );
+ done();
+ });
+ }, 2500);
+ });
});