diff --git a/settings.json.template b/settings.json.template index 6c0ebd385..3ae0a9815 100644 --- a/settings.json.template +++ b/settings.json.template @@ -39,9 +39,9 @@ but makes it impossible to debug the javascript/css */ "minify" : true, - /* How long may clients use served javascript code? Without versioning this - is may cause problems during deployment. */ - "maxAge" : 21600000, // 6 hours + /* How long may clients use served javascript code (in seconds)? Without versioning this + is may cause problems during deployment. Set to 0 to disable caching */ + "maxAge" : 21600, // 6 hours /* This is the path to the Abiword executable. Setting it to null, disables abiword. Abiword is needed to enable the import/export of pads*/ diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index b9ab6494d..c3ee231c3 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -128,7 +128,11 @@ exports.handleDisconnect = function(client) //Go trough all user that are still on the pad, and send them the USER_LEAVE message for(i in pad2sessions[sessionPad]) { - socketio.sockets.sockets[pad2sessions[sessionPad][i]].json.send(messageToTheOtherUsers); + var socket = socketio.sockets.sockets[pad2sessions[sessionPad][i]]; + if(socket !== undefined){ + socket.json.send(messageToTheOtherUsers); + } + } }); } @@ -738,9 +742,10 @@ function handleClientReady(client, message) { for(var i in pad2sessions[message.padId]) { - if(sessioninfos[pad2sessions[message.padId][i]].author == author) + if(sessioninfos[pad2sessions[message.padId][i]] && sessioninfos[pad2sessions[message.padId][i]].author == author) { - socketio.sockets.sockets[pad2sessions[message.padId][i]].json.send({disconnect:"userdup"}); + var socket = socketio.sockets.sockets[pad2sessions[message.padId][i]]; + if(socket) socket.json.send({disconnect:"userdup"}); } } } diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index e8f9afbb8..9209967ce 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -35,7 +35,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { // serve plugin definitions // not very static, but served here so that client can do require("pluginfw/static/js/plugin-definitions.js"); args.app.get('/pluginfw/plugin-definitions.json', function (req, res, next) { - res.header("Content-Type","application/json; charset: utf-8"); + res.header("Content-Type","application/json; charset=utf-8"); res.write(JSON.stringify({"plugins": plugins.plugins, "parts": plugins.parts})); res.end(); }); diff --git a/src/node/server.js b/src/node/server.js index 19df6e728..8e6d6fa0b 100644 --- a/src/node/server.js +++ b/src/node/server.js @@ -51,8 +51,9 @@ console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues") var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)"; -//cache 6 hours -exports.maxAge = 1000*60*60*6; +//cache 6 hours, by default +var hour = 60*60; +exports.maxAge = settings.maxAge || 6 * hour; //set loglevel log4js.setGlobalLogLevel(settings.loglevel); diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index 3f6550e77..f4af3b186 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -1,6 +1,7 @@ { "pad.js": [ "jquery.js" + , "underscore.js" , "security.js" , "pad.js" , "ace2_common.js" @@ -25,6 +26,7 @@ ] , "timeslider.js": [ "jquery.js" + , "underscore.js" , "security.js" , "undo-xpopup.js" , "json2.js" @@ -51,6 +53,8 @@ ] , "ace2_inner.js": [ "ace2_common.js" + , "underscore.js" + , "rjquery.js" , "AttributePool.js" , "Changeset.js" , "security.js" diff --git a/src/package.json b/src/package.json index b6cdd5df1..6253ecb16 100644 --- a/src/package.json +++ b/src/package.json @@ -23,7 +23,8 @@ "log4js" : "0.4.1", "jsdom-nocontextifiy" : "0.2.10", "async-stacktrace" : "0.0.2", - "npm" : "1.1" + "npm" : "1.1", + "underscore" : "1.3.1" }, "devDependencies": { "jshint" : "*" diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 1306dba01..685d45dfb 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -29,6 +29,7 @@ Ace2Editor.registry = { }; var hooks = require('./pluginfw/hooks'); +var _ = require('./underscore'); function Ace2Editor() { @@ -70,7 +71,7 @@ function Ace2Editor() function doActionsPendingInit() { - $.each(actionsPendingInit, function(i,fn){ + _.each(actionsPendingInit, function(fn,i){ fn() }); actionsPendingInit = []; @@ -87,7 +88,7 @@ function Ace2Editor() 'setUserChangeNotificationCallback', 'setAuthorInfo', 'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange']; - $.each(aceFunctionsPendingInit, function(i,fnName){ + _.each(aceFunctionsPendingInit, function(fnName,i){ var prefix = 'ace_'; var name = prefix + fnName; editor[fnName] = pendingInit(function(){ diff --git a/src/static/js/ace2_common.js b/src/static/js/ace2_common.js index 9f217045e..8a7d16ee3 100644 --- a/src/static/js/ace2_common.js +++ b/src/static/js/ace2_common.js @@ -29,58 +29,10 @@ function isNodeText(node) function object(o) { - var f = function() - {}; + var f = function(){}; f.prototype = o; return new f(); } - -function extend(obj, props) -{ - for (var p in props) - { - obj[p] = props[p]; - } - return obj; -} - -function forEach(array, func) -{ - for (var i = 0; i < array.length; i++) - { - var result = func(array[i], i); - if (result) break; - } -} - -function map(array, func) -{ - var result = []; - // must remain compatible with "arguments" pseudo-array - for (var i = 0; i < array.length; i++) - { - if (func) result.push(func(array[i], i)); - else result.push(array[i]); - } - return result; -} - -function filter(array, func) -{ - var result = []; - // must remain compatible with "arguments" pseudo-array - for (var i = 0; i < array.length; i++) - { - if (func(array[i], i)) result.push(array[i]); - } - return result; -} - -function isArray(testObject) -{ - return testObject && typeof testObject === 'object' && !(testObject.propertyIsEnumerable('length')) && typeof testObject.length === 'number'; -} - var userAgent = (((function () {return this;})().navigator || {}).userAgent || 'node-js').toLowerCase(); // Figure out what browser is being used (stolen from jquery 1.2.1) @@ -142,21 +94,13 @@ function htmlPrettyEscape(str) } var noop = function(){}; -var identity = function(x){return x}; exports.isNodeText = isNodeText; exports.object = object; -exports.extend = extend; -exports.forEach = forEach; -exports.map = map; -exports.filter = filter; -exports.isArray = isArray; exports.browser = browser; exports.getAssoc = getAssoc; exports.setAssoc = setAssoc; exports.binarySearch = binarySearch; exports.binarySearchInfinite = binarySearchInfinite; exports.htmlPrettyEscape = htmlPrettyEscape; -exports.map = map; exports.noop = noop; -exports.identity = identity; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index f30a0467d..a2c4afeb6 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -19,39 +19,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +var editor, _, $, jQuery, plugins, Ace2Common; -var Ace2Common = require('./ace2_common'); +Ace2Common = require('./ace2_common'); -// Extract useful method defined in the other module. -var isNodeText = Ace2Common.isNodeText; -var object = Ace2Common.object; -var extend = Ace2Common.extend; -var forEach = Ace2Common.forEach; -var map = Ace2Common.map; -var filter = Ace2Common.filter; -var isArray = Ace2Common.isArray; -var browser = Ace2Common.browser; -var getAssoc = Ace2Common.getAssoc; -var setAssoc = Ace2Common.setAssoc; -var binarySearchInfinite = Ace2Common.binarySearchInfinite; -var htmlPrettyEscape = Ace2Common.htmlPrettyEscape; -var map = Ace2Common.map; -var noop = Ace2Common.noop; +plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); +$ = jQuery = require('./rjquery').$; +_ = require("./underscore"); -var makeChangesetTracker = require('./changesettracker').makeChangesetTracker; -var colorutils = require('./colorutils').colorutils; -var makeContentCollector = require('./contentcollector').makeContentCollector; -var makeCSSManager = require('./cssmanager').makeCSSManager; -var domline = require('./domline').domline; -var AttribPool = require('./AttributePool'); -var Changeset = require('./Changeset'); -var linestylefilter = require('./linestylefilter').linestylefilter; -var SkipList = require('./skiplist'); -var undoModule = require('./undomodule').undoModule; -var makeVirtualLineView = require('./virtual_lines').makeVirtualLineView; +var isNodeText = Ace2Common.isNodeText, + browser = Ace2Common.browser, + getAssoc = Ace2Common.getAssoc, + setAssoc = Ace2Common.setAssoc, + isTextNode = Ace2Common.isTextNode, + binarySearchInfinite = Ace2Common.binarySearchInfinite, + htmlPrettyEscape = Ace2Common.htmlPrettyEscape, + noop = Ace2Common.noop; function Ace2Inner(){ - + + var makeChangesetTracker = require('./changesettracker').makeChangesetTracker; + var colorutils = require('./colorutils').colorutils; + var makeContentCollector = require('./contentcollector').makeContentCollector; + var makeCSSManager = require('./cssmanager').makeCSSManager; + var domline = require('./domline').domline; + var AttribPool = require('./AttributePool'); + var Changeset = require('./Changeset'); + var linestylefilter = require('./linestylefilter').linestylefilter; + var SkipList = require('./skiplist'); + var undoModule = require('./undomodule').undoModule; + var makeVirtualLineView = require('./virtual_lines').makeVirtualLineView; + var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" // changed to false var isSetUp = false; @@ -70,7 +68,6 @@ function Ace2Inner(){ var thisAuthor = ''; var disposed = false; - var editorInfo = parent.editorInfo; var iframe = window.frameElement; @@ -685,7 +682,7 @@ function Ace2Inner(){ } else { - lines = map(text.split('\n'), textify); + lines = _.map(text.split('\n'), textify); } var newText = "\n"; if (lines.length > 0) @@ -884,9 +881,7 @@ function Ace2Inner(){ { return fn(editorInfo); }; - - - + if (normalize !== undefined) { var wrapper1 = wrapper; @@ -1623,8 +1618,7 @@ function Ace2Inner(){ } //var fragment = magicdom.wrapDom(document.createDocumentFragment()); domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); - forEach(dirtyNodes, function(n) - { + _.each(dirtyNodes,function(n){ toDeleteAtEnd.push(n); }); var spliceHints = {}; @@ -1646,7 +1640,7 @@ function Ace2Inner(){ // update the representation p.mark("splice"); - forEach(splicesToDo, function(splice) + _.each(splicesToDo, function(splice) { doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); }); @@ -1656,14 +1650,14 @@ function Ace2Inner(){ //var isTimeUp = newTimeLimit(100); // do DOM inserts p.mark("insert"); - forEach(domInsertsNeeded, function(ins) + _.each(domInsertsNeeded,function(ins) { insertDomLines(ins[0], ins[1], isTimeUp); }); p.mark("del"); // delete old dom nodes - forEach(toDeleteAtEnd, function(n) + _.each(toDeleteAtEnd,function(n) { //var id = n.uniqueId(); // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) @@ -1773,7 +1767,7 @@ function Ace2Inner(){ var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp); - forEach(infoStructs, function(info) + _.each(infoStructs, function(info) { var p2 = PROFILER("insertLine", false); var node = info.node; @@ -2084,10 +2078,8 @@ function Ace2Inner(){ var linesMutatee = { splice: function(start, numRemoved, newLinesVA) { - domAndRepSplice(start, numRemoved, map(Array.prototype.slice.call(arguments, 2), function(s) - { - return s.slice(0, -1); - }), null); + var args = Array.prototype.slice.call(arguments, 2); + domAndRepSplice(start, numRemoved, _.map(args, function(s){ return s.slice(0, -1); }), null); }, get: function(i) { @@ -2099,7 +2091,7 @@ function Ace2Inner(){ }, slice_notused: function(start, end) { - return map(rep.lines.slice(start, end), function(e) + return _.map(rep.lines.slice(start, end), function(e) { return e.text + '\n'; }); @@ -2132,7 +2124,7 @@ function Ace2Inner(){ } } - var lineEntries = map(newLineStrings, createDomLineEntry); + var lineEntries = _.map(newLineStrings, createDomLineEntry); doRepLineSplice(startLine, deleteCount, lineEntries); @@ -2143,12 +2135,12 @@ function Ace2Inner(){ } else nodeToAddAfter = null; - insertDomLines(nodeToAddAfter, map(lineEntries, function(entry) + insertDomLines(nodeToAddAfter, _.map(lineEntries, function(entry) { return entry.domInfo; }), isTimeUp); - forEach(keysToDelete, function(k) + _.each(keysToDelete, function(k) { var n = doc.getElementById(k); n.parentNode.removeChild(n); @@ -2468,7 +2460,7 @@ function Ace2Inner(){ function doRepLineSplice(startLine, deleteCount, newLineEntries) { - forEach(newLineEntries, function(entry) + _.each(newLineEntries, function(entry) { entry.width = entry.text.length + 1; }); @@ -2483,7 +2475,7 @@ function Ace2Inner(){ currentCallStack.repChanged = true; var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length); - var newText = map(newLineEntries, function(e) + var newText = _.map(newLineEntries, function(e) { return e.text + '\n'; }).join(''); @@ -2513,7 +2505,7 @@ function Ace2Inner(){ selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart; } - var newText = map(newLineEntries, function(e) + var newText = _.map(newLineEntries, function(e) { return e.text + '\n'; }).join(''); @@ -3054,7 +3046,7 @@ function Ace2Inner(){ { // returns index of cleanRange containing i, or -1 if none var answer = -1; - forEach(cleanRanges, function(r, idx) + _.each(cleanRanges ,function(r, idx) { if (i >= r[1]) return false; // keep looking if (i < r[0]) return true; // not found, stop looking @@ -3847,7 +3839,7 @@ function Ace2Inner(){ function getRepHTML() { - return map(rep.lines.slice(), function(entry) + return _.map(rep.lines.slice(), function(entry) { var text = entry.text; var content; @@ -4533,7 +4525,7 @@ function Ace2Inner(){ enforceEditability(); - addClass(sideDiv, 'sidedivdelayed'); + $(sideDiv).addClass('sidedivdelayed'); } function getScrollXY() @@ -4586,14 +4578,12 @@ function Ace2Inner(){ function teardown() { - forEach(_teardownActions, function(a) + _.each(_teardownActions, function(a) { a(); }); } - bindEventHandler(window, "load", setup); - function setDesignMode(newVal) { try @@ -4670,20 +4660,20 @@ function Ace2Inner(){ function bindTheEventHandlers() { - bindEventHandler(document, "keydown", handleKeyEvent); - bindEventHandler(document, "keypress", handleKeyEvent); - bindEventHandler(document, "keyup", handleKeyEvent); - bindEventHandler(document, "click", handleClick); - bindEventHandler(root, "blur", handleBlur); + $(document).on("keydown", handleKeyEvent); + $(document).on("keypress", handleKeyEvent); + $(document).on("keyup", handleKeyEvent); + $(document).on("click", handleClick); + $(root).on("blur", handleBlur); if (browser.msie) { - bindEventHandler(document, "click", handleIEOuterClick); + $(document).on("click", handleIEOuterClick); } - if (browser.msie) bindEventHandler(root, "paste", handleIEPaste); + if (browser.msie) $(root).on("paste", handleIEPaste); if ((!browser.msie) && document.documentElement) { - bindEventHandler(document.documentElement, "compositionstart", handleCompositionEvent); - bindEventHandler(document.documentElement, "compositionend", handleCompositionEvent); + $(document.documentElement).on("compositionstart", handleCompositionEvent); + $(document.documentElement).on("compositionend", handleCompositionEvent); } } @@ -4730,91 +4720,10 @@ function Ace2Inner(){ elem.className = array.join(' '); } - function addClass(elem, className) - { - var seen = false; - var cc = getClassArray(elem, function(c) - { - if (c == className) seen = true; - return true; - }); - if (!seen) - { - cc.push(className); - setClassArray(elem, cc); - } - } - - function removeClass(elem, className) - { - var seen = false; - var cc = getClassArray(elem, function(c) - { - if (c == className) - { - seen = true; - return false; - } - return true; - }); - if (seen) - { - setClassArray(elem, cc); - } - } - function setClassPresence(elem, className, present) { - if (present) addClass(elem, className); - else removeClass(elem, className); - } - - function setup() - { - doc = document; // defined as a var in scope outside - inCallStack("setup", function() - { - var body = doc.getElementById("innerdocbody"); - root = body; // defined as a var in scope outside - if (browser.mozilla) addClass(root, "mozilla"); - if (browser.safari) addClass(root, "safari"); - if (browser.msie) addClass(root, "msie"); - if (browser.msie) - { - // cache CSS background images - try - { - doc.execCommand("BackgroundImageCache", false, true); - } - catch (e) - { /* throws an error in some IE 6 but not others! */ - } - } - setClassPresence(root, "authorColors", true); - setClassPresence(root, "doesWrap", doesWrap); - - initDynamicCSS(); - - enforceEditability(); - - // set up dom and rep - while (root.firstChild) root.removeChild(root.firstChild); - var oneEntry = createDomLineEntry(""); - doRepLineSplice(0, rep.lines.length(), [oneEntry]); - insertDomLines(null, [oneEntry.domInfo], null); - rep.alines = Changeset.splitAttributionLines( - Changeset.makeAttribution("\n"), "\n"); - - bindTheEventHandlers(); - - }); - - scheduler.setTimeout(function() - { - parent.readyFunc(); // defined in code that sets up the inner iframe - }, 0); - - isSetUp = true; + if (present) $(elem).addClass(className); + else $(elem).removeClass(elem, className); } function focus() @@ -4833,32 +4742,6 @@ function Ace2Inner(){ } } - function bindEventHandler(target, type, func) - { - var handler; - if ((typeof func._wrapper) != "function") - { - func._wrapper = function(event) - { - func(fixEvent(event || window.event || {})); - } - } - var handler = func._wrapper; - if (target.addEventListener) target.addEventListener(type, handler, false); - else target.attachEvent("on" + type, handler); - _teardownActions.push(function() - { - unbindEventHandler(target, type, func); - }); - } - - function unbindEventHandler(target, type, func) - { - var handler = func._wrapper; - if (target.removeEventListener) target.removeEventListener(type, handler, false); - else target.detachEvent("on" + type, handler); - } - function getSelectionPointX(point) { // doesn't work in wrap-mode @@ -5520,67 +5403,7 @@ function Ace2Inner(){ } }; })()); - - - // stolen from jquery-1.2.1 - - - function fixEvent(event) - { - // store a copy of the original event object - // and clone to set read-only properties - var originalEvent = event; - event = extend( - {}, originalEvent); - - // add preventDefault and stopPropagation since - // they will not work on the clone - event.preventDefault = function() - { - // if preventDefault exists run it on the original event - if (originalEvent.preventDefault) originalEvent.preventDefault(); - // otherwise set the returnValue property of the original event to false (IE) - originalEvent.returnValue = false; - }; - event.stopPropagation = function() - { - // if stopPropagation exists run it on the original event - if (originalEvent.stopPropagation) originalEvent.stopPropagation(); - // otherwise set the cancelBubble property of the original event to true (IE) - originalEvent.cancelBubble = true; - }; - - // Fix target property, if necessary - if (!event.target && event.srcElement) event.target = event.srcElement; - - // check if target is a textnode (safari) - if (browser.safari && event.target.nodeType == 3) event.target = originalEvent.target.parentNode; - - // Add relatedTarget, if necessary - if (!event.relatedTarget && event.fromElement) event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if (event.pageX == null && event.clientX != null) - { - var e = document.documentElement, - b = document.body; - event.pageX = event.clientX + (e && e.scrollLeft || b.scrollLeft || 0); - event.pageY = event.clientY + (e && e.scrollTop || b.scrollTop || 0); - } - - // Add which for key events - if (!event.which && (event.charCode || event.keyCode)) event.which = event.charCode || event.keyCode; - - // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) - if (!event.metaKey && event.ctrlKey) event.metaKey = event.ctrlKey; - - // Add which for click: 1 == left; 2 == middle; 3 == right - // Note: button is not normalized, so don't use it - if (!event.which && event.button) event.which = (event.button & 1 ? 1 : (event.button & 2 ? 3 : (event.button & 4 ? 2 : 0))); - - return event; - } - + var lineNumbersShown; var sideDivInner; @@ -5665,6 +5488,56 @@ function Ace2Inner(){ } } + $(document).ready(function(){ + doc = document; // defined as a var in scope outside + inCallStack("setup", function() + { + var body = doc.getElementById("innerdocbody"); + root = body; // defined as a var in scope outside + if (browser.mozilla) $(root).addClass("mozilla"); + if (browser.safari) $(root).addClass("safari"); + if (browser.msie) $(root).addClass("msie"); + if (browser.msie) + { + // cache CSS background images + try + { + doc.execCommand("BackgroundImageCache", false, true); + } + catch (e) + { /* throws an error in some IE 6 but not others! */ + } + } + setClassPresence(root, "authorColors", true); + setClassPresence(root, "doesWrap", doesWrap); + + initDynamicCSS(); + + enforceEditability(); + + // set up dom and rep + while (root.firstChild) root.removeChild(root.firstChild); + var oneEntry = createDomLineEntry(""); + doRepLineSplice(0, rep.lines.length(), [oneEntry]); + insertDomLines(null, [oneEntry.domInfo], null); + rep.alines = Changeset.splitAttributionLines( + Changeset.makeAttribution("\n"), "\n"); + + bindTheEventHandlers(); + + }); + + scheduler.setTimeout(function() + { + parent.readyFunc(); // defined in code that sets up the inner iframe + }, 0); + + isSetUp = true; + }); + } -exports.editor = new Ace2Inner(); +// Ensure that plugins are loaded before initializing the editor +plugins.ensure(function () { + var editor = new Ace2Inner(); +}); diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index 333e3b652..1c7bdcfda 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -26,10 +26,7 @@ var AttribPool = require('./AttributePool'); var Changeset = require('./Changeset'); var linestylefilter = require('./linestylefilter').linestylefilter; var colorutils = require('./colorutils').colorutils; -var Ace2Common = require('./ace2_common'); - -var map = Ace2Common.map; -var forEach = Ace2Common.forEach; +var _ = require('./underscore'); // These parameters were global, now they are injected. A reference to the // Timeslider controller would probably be more appropriate. @@ -155,7 +152,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro // splice the lines splice: function(start, numRemoved, newLinesVA) { - var newLines = map(Array.prototype.slice.call(arguments, 2), function(s) { + var newLines = _.map(Array.prototype.slice.call(arguments, 2), function(s) { return s; }); @@ -278,7 +275,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro debugLog('Time Delta: ', timeDelta) updateTimer(); - var authors = map(padContents.getActiveAuthors(), function(name) + var authors = _.map(padContents.getActiveAuthors(), function(name) { return authorData[name]; }); @@ -384,7 +381,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro changesetLoader.queueUp(start, 1, update); } - var authors = map(padContents.getActiveAuthors(), function(name){ + var authors = _.map(padContents.getActiveAuthors(), function(name){ return authorData[name]; }); BroadcastSlider.setAuthors(authors); @@ -527,7 +524,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro authorMap[obj.author] = obj.data; receiveAuthorData(authorMap); - var authors = map(padContents.getActiveAuthors(),function(name) { + var authors = _.map(padContents.getActiveAuthors(), function(name) { return authorData[name]; }); @@ -607,10 +604,13 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro setChannelState("DISCONNECTED", reason); } + /// Since its not used, import 'forEach' has been dropped /*window['onloadFuncts'] = []; window.onload = function () { window['isloaded'] = true; + + forEach(window['onloadFuncts'],function (funct) { funct(); diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js index 2b67ac73a..f88eb5ddc 100644 --- a/src/static/js/broadcast_slider.js +++ b/src/static/js/broadcast_slider.js @@ -22,7 +22,7 @@ // These parameters were global, now they are injected. A reference to the // Timeslider controller would probably be more appropriate. -var forEach = require('./ace2_common').forEach; +var _ = require('./underscore'); function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) { @@ -175,7 +175,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) $("#authorstable").empty(); var numAnonymous = 0; var numNamed = 0; - forEach(authors, function(author) + _.each(authors, function(author) { if (author.name) { @@ -469,7 +469,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) $("#timeslider").show(); setSliderLength(clientVars.totalRevs); setSliderPosition(clientVars.revNum); - forEach(clientVars.savedRevisions, function(revision) + _.each(clientVars.savedRevisions, function(revision) { addSavedRevision(revision.revNum, revision); }) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index b8f22e9e4..23b476675 100644 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -147,10 +147,11 @@ var chat = (function() } }); - for(var i in clientVars.chatHistory) - { - this.addMessage(clientVars.chatHistory[i], false); - } + var that = this; + $.each(clientVars.chatHistory, function(i, o){ + that.addMessage(o, false); + }) + $("#chatcounter").text(clientVars.chatHistory.length); } } diff --git a/src/static/js/domline.js b/src/static/js/domline.js index d6dbb74ab..d4f52a3ce 100644 --- a/src/static/js/domline.js +++ b/src/static/js/domline.js @@ -28,10 +28,11 @@ var Security = require('./security'); var hooks = require('./pluginfw/hooks'); +var _ = require('./underscore'); + var Ace2Common = require('./ace2_common'); -var map = Ace2Common.map; var noop = Ace2Common.noop; -var identity = Ace2Common.identity; + var domline = {}; @@ -89,8 +90,8 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) return domline.processSpaces(s, doesWrap); } - var perTextNodeProcess = (doesWrap ? identity : processSpaces); - var perHtmlLineProcess = (doesWrap ? processSpaces : identity); + var perTextNodeProcess = (doesWrap ? _.identity : processSpaces); + var perHtmlLineProcess = (doesWrap ? processSpaces : _.identity); var lineClass = 'ace-line'; result.appendSpan = function(txt, cls) { @@ -142,7 +143,7 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) var extraOpenTags = ""; var extraCloseTags = ""; - map(hooks.callAll("aceCreateDomLine", { + _.map(hooks.callAll("aceCreateDomLine", { domline: domline, cls: cls }), function(modifier) diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 04b8fb72d..f328bd56a 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -30,9 +30,8 @@ var Changeset = require('./Changeset'); var hooks = require('./pluginfw/hooks'); -var map = require('./ace2_common').map; - var linestylefilter = {}; +var _ = require('./underscore'); linestylefilter.ATTRIB_CLASSES = { 'bold': 'tag:b', @@ -302,7 +301,7 @@ linestylefilter.getFilterStack = function(lineText, textAndClassFunc, browser) linestylefilter: linestylefilter, browser: browser }); - map(hookFilters, function(hookFilter) + _.map(hookFilters ,function(hookFilter) { func = hookFilter(lineText, func); }); diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 1b09a6e5d..9c04023fc 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -1,10 +1,13 @@ var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); +var _; /* FIXME: Ugly hack, in the future, use same code for server & client */ if (plugins.isClient) { - var async = require("ep_etherpad-lite/static/js/pluginfw/async"); + var async = require("ep_etherpad-lite/static/js/pluginfw/async"); + _ = require("ep_etherpad-lite/static/js/underscore"); } else { var async = require("async"); + _ = require("underscore"); } var hookCallWrapper = function (hook, hook_name, args, cb) { @@ -34,7 +37,7 @@ exports.flatten = function (lst) { exports.callAll = function (hook_name, args) { if (plugins.hooks[hook_name] === undefined) return []; - return exports.flatten(plugins.hooks[hook_name].map(function (hook) { + return exports.flatten(_.map(plugins.hooks[hook_name], function (hook) { return hookCallWrapper(hook, hook_name, args); })); } diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index c5c219032..aa2dfafb2 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -1,5 +1,7 @@ exports.isClient = typeof global != "object"; +var _; + if (!exports.isClient) { var npm = require("npm/lib/npm.js"); var readInstalled = require("npm/lib/utils/read-installed.js"); @@ -10,6 +12,12 @@ if (!exports.isClient) { var fs = require("fs"); var tsort = require("./tsort"); var util = require("util"); + _ = require("underscore"); +}else{ + var $, jQuery + $ = jQuery = require("ep_etherpad-lite/static/js/rjquery").$; + _ = require("ep_etherpad-lite/static/js/underscore"); + } exports.prefix = 'ep_'; @@ -26,17 +34,17 @@ exports.ensure = function (cb) { } exports.formatPlugins = function () { - return Object.keys(exports.plugins).join(", "); + return _.keys(exports.plugins).join(", "); } exports.formatParts = function () { - return exports.parts.map(function (part) { return part.full_name; }).join("\n"); + return _.map(exports.parts, function (part) { return part.full_name; }).join("\n"); } exports.formatHooks = function () { var res = []; - Object.keys(exports.hooks).forEach(function (hook_name) { - exports.hooks[hook_name].forEach(function (hook) { + _.chain(exports.hooks).keys().forEach(function (hook_name) { + _.forEach(exports.hooks[hook_name], function (hook) { res.push(hook.hook_name + ": " + hook.hook_fn_name + " from " + hook.part.full_name); }); }); @@ -46,7 +54,7 @@ exports.formatHooks = function () { exports.loadFn = function (path) { var x = path.split(":"); var fn = require(x[0]); - x[1].split(".").forEach(function (name) { + _.each(x[1].split("."), function (name) { fn = fn[name]; }); return fn; @@ -54,8 +62,8 @@ exports.loadFn = function (path) { exports.extractHooks = function (parts, hook_set_name) { var hooks = {}; - parts.forEach(function (part) { - Object.keys(part[hook_set_name] || {}).forEach(function (hook_name) { + _.each(parts,function (part) { + _.chain(part[hook_set_name] || {}).keys().each(function (hook_name) { if (hooks[hook_name] === undefined) hooks[hook_name] = []; var hook_fn_name = part[hook_set_name][hook_name]; var hook_fn = exports.loadFn(part[hook_set_name][hook_name]); @@ -78,6 +86,9 @@ if (exports.isClient) { exports.hooks = exports.extractHooks(exports.parts, "client_hooks"); exports.loaded = true; cb(); + }).error(function(xhr, s, err){ + console.error("Failed to load plugin-definitions: " + err); + cb(); }); } } else { @@ -110,7 +121,7 @@ exports.getPackages = function (cb) { if (er) cb(er, null); var packages = {}; function flatten(deps) { - Object.keys(deps).forEach(function (name) { + _.chain(deps).keys().each(function (name) { if (name.indexOf(exports.prefix) == 0) { packages[name] = deps[name]; } @@ -137,7 +148,7 @@ exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) { var plugin = JSON.parse(data); plugin.package = packages[plugin_name]; plugins[plugin_name] = plugin; - plugin.parts.forEach(function (part) { + _.each(plugin.parts, function (part) { part.plugin = plugin_name; part.full_name = plugin_name + "/" + part.name; parts[part.full_name] = part; @@ -152,11 +163,11 @@ exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) { exports.partsToParentChildList = function (parts) { var res = []; - Object.keys(parts).forEach(function (name) { - (parts[name].post || []).forEach(function (child_name) { + _.chain(parts).keys().forEach(function (name) { + _.each(parts[name].post || [], function (child_name) { res.push([name, child_name]); }); - (parts[name].pre || []).forEach(function (parent_name) { + _.each(parts[name].pre || [], function (parent_name) { res.push([parent_name, name]); }); if (!parts[name].pre && !parts[name].post) { @@ -166,6 +177,8 @@ exports.partsToParentChildList = function (parts) { return res; } + +// Used only in Node, so no need for _ exports.sortParts = function(parts) { return tsort( exports.partsToParentChildList(parts) diff --git a/src/static/js/rjquery.js b/src/static/js/rjquery.js new file mode 100644 index 000000000..21af074b2 --- /dev/null +++ b/src/static/js/rjquery.js @@ -0,0 +1,5 @@ + +// Proviedes a require'able version of jQuery without leaking $ and jQuery; + +require('./jquery'); +exports.jQuery = exports.$ = $.noConflict(true); \ No newline at end of file diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 579dcb603..438b1695f 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -29,6 +29,7 @@ require('./undo-xpopup'); var createCookie = require('./pad_utils').createCookie; var readCookie = require('./pad_utils').readCookie; var randomString = require('./pad_utils').randomString; +var _ = require('./underscore'); var socket, token, padId, export_links; @@ -138,6 +139,7 @@ function handleClientVars(message) var export_rev_regex = /(\/\d+)?\/export/ BroadcastSlider.onSlider(function(revno) { + // export_links is a jQuery Array, so .each is allowed. export_links.each(function() { this.setAttribute('href', this.href.replace(export_rev_regex, '/' + revno + '/export')); diff --git a/src/static/js/underscore.js b/src/static/js/underscore.js new file mode 100644 index 000000000..208d4cd89 --- /dev/null +++ b/src/static/js/underscore.js @@ -0,0 +1,999 @@ +// Underscore.js 1.3.1 +// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Underscore is freely distributable under the MIT license. +// Portions of Underscore are inspired or borrowed from Prototype, +// Oliver Steele's Functional, and John Resig's Micro-Templating. +// For all details and documentation: +// http://documentcloud.github.com/underscore + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var slice = ArrayProto.slice, + unshift = ArrayProto.unshift, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { return new wrapper(obj); }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root['_'] = _; + } + + // Current version. + _.VERSION = '1.3.1'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (_.has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + if (obj.length === +obj.length) results.length = obj.length; + return results; + }; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var reversed = _.toArray(obj).reverse(); + if (context && !initial) iterator = _.bind(iterator, context); + return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + each(obj, function(value, index, list) { + if (!iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result || (result = iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if a given value is included in the array or object using `===`. + // Aliased as `contains`. + _.include = _.contains = function(obj, target) { + var found = false; + if (obj == null) return found; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + found = any(obj, function(value) { + return value === target; + }); + return found; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + return _.map(obj, function(value) { + return (_.isFunction(method) ? method || value : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Return the maximum element or (element-based computation). + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); + if (!iterator && _.isEmpty(obj)) return -Infinity; + var result = {computed : -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed >= result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); + if (!iterator && _.isEmpty(obj)) return Infinity; + var result = {computed : Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Shuffle an array. + _.shuffle = function(obj) { + var shuffled = [], rand; + each(obj, function(value, index, list) { + if (index == 0) { + shuffled[0] = value; + } else { + rand = Math.floor(Math.random() * (index + 1)); + shuffled[index] = shuffled[rand]; + shuffled[rand] = value; + } + }); + return shuffled; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, iterator, context) { + return _.pluck(_.map(obj, function(value, index, list) { + return { + value : value, + criteria : iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }), 'value'); + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = function(obj, val) { + var result = {}; + var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; + each(obj, function(value, index) { + var key = iterator(value, index); + (result[key] || (result[key] = [])).push(value); + }); + return result; + }; + + // Use a comparator function to figure out at what index an object should + // be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator) { + iterator || (iterator = _.identity); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >> 1; + iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely convert anything iterable into a real, live array. + _.toArray = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + if (_.isArray(iterable)) return slice.call(iterable); + if (_.isArguments(iterable)) return slice.call(iterable); + return _.values(iterable); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + return _.toArray(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head`. The **guard** check allows it to work + // with `_.map`. + _.first = _.head = function(array, n, guard) { + return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + }; + + // Returns everything but the last entry of the array. Especcialy useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if ((n != null) && !guard) { + return slice.call(array, Math.max(array.length - n, 0)); + } else { + return array[array.length - 1]; + } + }; + + // Returns everything but the first entry of the array. Aliased as `tail`. + // Especially useful on the arguments object. Passing an **index** will return + // the rest of the values in the array from that index onward. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = function(array, index, guard) { + return slice.call(array, (index == null) || guard ? 1 : index); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, function(value){ return !!value; }); + }; + + // Return a completely flattened version of an array. + _.flatten = function(array, shallow) { + return _.reduce(array, function(memo, value) { + if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); + memo[memo.length] = value; + return memo; + }, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator) { + var initial = iterator ? _.map(array, iterator) : array; + var result = []; + _.reduce(initial, function(memo, el, i) { + if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) { + memo[memo.length] = el; + result[result.length] = array[i]; + } + return memo; + }, []); + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(_.flatten(arguments, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. (Aliased as "intersect" for back-compat.) + _.intersection = _.intersect = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = _.flatten(slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.include(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var args = slice.call(arguments); + var length = _.max(_.pluck(args, 'length')); + var results = new Array(length); + for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); + return results; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i, l; + if (isSorted) { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); + for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item) { + if (array == null) return -1; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); + var i = array.length; + while (i--) if (i in array && array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var len = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(len); + + while(idx < len) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Binding with arguments is also known as `curry`. + // Delegates to **ECMAScript 5**'s native `Function.bind` if available. + // We check for `func.bind` first, to fail fast when `func` is undefined. + _.bind = function bind(func, context) { + var bound, args; + if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length == 0) funcs = _.functions(obj); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(func, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + var context, args, timeout, throttling, more; + var whenDone = _.debounce(function(){ more = throttling = false; }, wait); + return function() { + context = this; args = arguments; + var later = function() { + timeout = null; + if (more) func.apply(context, args); + whenDone(); + }; + if (!timeout) timeout = setTimeout(later, wait); + if (throttling) { + more = true; + } else { + func.apply(context, args); + } + whenDone(); + throttling = true; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. + _.debounce = function(func, wait) { + var timeout; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + func.apply(context, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + return memo = func.apply(this, arguments); + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func].concat(slice.call(arguments, 0)); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + if (times <= 0) return func(); + return function() { + if (--times < 1) { return func.apply(this, arguments); } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + return _.map(obj, _.identity); + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function. + function eq(a, b, stack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a._chain) a = a._wrapped; + if (b._chain) b = b._wrapped; + // Invoke a custom `isEqual` method if one is provided. + if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); + if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = stack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (stack[length] == a) return true; + } + // Add the first object to the stack of traversed objects. + stack.push(a); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + // Ensure commutative equality for sparse arrays. + if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; + } + } + } else { + // Objects with different constructors are not equivalent. + if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + stack.pop(); + return result; + } + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType == 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Is a given variable an arguments object? + _.isArguments = function(obj) { + return toString.call(obj) == '[object Arguments]'; + }; + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Is a given value a function? + _.isFunction = function(obj) { + return toString.call(obj) == '[object Function]'; + }; + + // Is a given value a string? + _.isString = function(obj) { + return toString.call(obj) == '[object String]'; + }; + + // Is a given value a number? + _.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; + }; + + // Is the given value `NaN`? + _.isNaN = function(obj) { + // `NaN` is the only value for which `===` is not reflexive. + return obj !== obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value a date? + _.isDate = function(obj) { + return toString.call(obj) == '[object Date]'; + }; + + // Is the given value a regular expression? + _.isRegExp = function(obj) { + return toString.call(obj) == '[object RegExp]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Has own property? + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function (n, iterator, context) { + for (var i = 0; i < n; i++) iterator.call(context, i); + }; + + // Escape a string for HTML interpolation. + _.escape = function(string) { + return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); + }; + + // Add your own custom functions to the Underscore object, ensuring that + // they're correctly added to the OOP wrapper as well. + _.mixin = function(obj) { + each(_.functions(obj), function(name){ + addToWrapper(name, _[name] = obj[name]); + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = idCounter++; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /.^/; + + // Within an interpolation, evaluation, or escaping, remove HTML escaping + // that had been previously added. + var unescape = function(code) { + return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'"); + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(str, data) { + var c = _.templateSettings; + var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + + 'with(obj||{}){__p.push(\'' + + str.replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(c.escape || noMatch, function(match, code) { + return "',_.escape(" + unescape(code) + "),'"; + }) + .replace(c.interpolate || noMatch, function(match, code) { + return "'," + unescape(code) + ",'"; + }) + .replace(c.evaluate || noMatch, function(match, code) { + return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('"; + }) + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n') + .replace(/\t/g, '\\t') + + "');}return __p.join('');"; + var func = new Function('obj', '_', tmpl); + if (data) return func(data, _); + return function(data) { + return func.call(this, data, _); + }; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // The OOP Wrapper + // --------------- + + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + var wrapper = function(obj) { this._wrapped = obj; }; + + // Expose `wrapper.prototype` as `_.prototype` + _.prototype = wrapper.prototype; + + // Helper function to continue chaining intermediate results. + var result = function(obj, chain) { + return chain ? _(obj).chain() : obj; + }; + + // A method to easily add functions to the OOP wrapper. + var addToWrapper = function(name, func) { + wrapper.prototype[name] = function() { + var args = slice.call(arguments); + unshift.call(args, this._wrapped); + return result(func.apply(_, args), this._chain); + }; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + wrapper.prototype[name] = function() { + var wrapped = this._wrapped; + method.apply(wrapped, arguments); + var length = wrapped.length; + if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; + return result(wrapped, this._chain); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + wrapper.prototype[name] = function() { + return result(method.apply(this._wrapped, arguments), this._chain); + }; + }); + + // Start chaining a wrapped Underscore object. + wrapper.prototype.chain = function() { + this._chain = true; + return this; + }; + + // Extracts the result from a wrapped and chained object. + wrapper.prototype.value = function() { + return this._wrapped; + }; + +}).call(this); diff --git a/src/static/js/undomodule.js b/src/static/js/undomodule.js index 8b0c0909c..4b55311bf 100644 --- a/src/static/js/undomodule.js +++ b/src/static/js/undomodule.js @@ -21,7 +21,7 @@ */ var Changeset = require('./Changeset'); -var extend = require('./ace2_common').extend; +var _ = require('./underscore'); var undoModule = (function() { @@ -52,7 +52,7 @@ var undoModule = (function() function pushEvent(event) { - var e = extend( + var e = _.extend( {}, event); e.elementType = UNDOABLE_EVENT; stackElements.push(e);