From a2c8d2124071d668bd3799c166004263d46fcc6a Mon Sep 17 00:00:00 2001 From: Gedion Date: Sat, 8 Sep 2012 12:03:13 -0500 Subject: [PATCH 01/32] added hooks to contentcollector.js --- src/static/js/contentcollector.js | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index b27dfc5e0..bb814212b 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -375,6 +375,17 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class if (dom.isNodeText(node)) { var txt = dom.nodeValue(node); + var tname = dom.nodeAttr(node.parentNode,"name"); + var txtFromHook = hooks.callAll('collectContentLineText', { + cc: this, + state: state, + tname: tname, + node:node, + text:txt, + styl: null, + cls: null + }); + var txt = txtFromHook||txt; var rest = ''; var x = 0; // offset into original text if (txt.length == 0) @@ -441,8 +452,32 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class { var tname = (dom.nodeTagName(node) || "").toLowerCase(); if (tname == "br") + /* + *Called from: src/static/js/contentcollector.js + * + * cc - the contentcollector object + * state - the current state of the change being made + * tname - the tag name of this node currently being processed + * + * This hook is provided to allow Whether the br tag should induce a new magic domline or not. + * The return value should be either true(break the line) or values. + * + */ { cc.startNewLine(state); + this.breakLine = true; + var tvalue = dom.nodeAttr(node, 'value'); + var induceLineBreak = hooks.callAll('collectContentLineBreak', { + cc: this, + state: state, + tname: tname, + tvalue:tvalue, + styl: null, + cls: null + }); + if(induceLineBreak){ + cc.startNewLine(state); + } } else if (tname == "script" || tname == "style") { From 6d1cba225978f76619659eb28e174e3f945d834f Mon Sep 17 00:00:00 2001 From: Gedion Date: Sat, 8 Sep 2012 12:11:04 -0500 Subject: [PATCH 02/32] added hooks to contentcollector.js --- src/static/js/contentcollector.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index bb814212b..a1860cbad 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -376,6 +376,18 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class { var txt = dom.nodeValue(node); var tname = dom.nodeAttr(node.parentNode,"name"); + /* + * Called from: src/static/js/contentcollector.js + * + * Context - + * cc - the contentcollector object + * state - the current state of the change being made + * tname - the tag name of this node currently being processed + * text - the text for that line + * This hook allows you to validate/manipulate the text before it sent to the server side. + * The return value should the validate/manipulated text. + * + */ var txtFromHook = hooks.callAll('collectContentLineText', { cc: this, state: state, @@ -453,14 +465,15 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class var tname = (dom.nodeTagName(node) || "").toLowerCase(); if (tname == "br") /* - *Called from: src/static/js/contentcollector.js + * Called from: src/static/js/contentcollector.js * + * Context - * cc - the contentcollector object * state - the current state of the change being made * tname - the tag name of this node currently being processed * * This hook is provided to allow Whether the br tag should induce a new magic domline or not. - * The return value should be either true(break the line) or values. + * The return value should be either true(break the line) or false. * */ { From c37c48cd12c82bbebf06e6f8d9090196b9533638 Mon Sep 17 00:00:00 2001 From: Gedion Date: Sat, 8 Sep 2012 13:45:33 -0500 Subject: [PATCH 03/32] added hooks and made some functions available to editor info object in ace --- src/static/js/ace2_inner.js | 99 ++++++++++++++++++++++++++++--- src/static/js/contentcollector.js | 26 ++++---- 2 files changed, 104 insertions(+), 21 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 07580faa5..c262deb4c 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -200,6 +200,11 @@ function Ace2Inner(){ var authorInfos = {}; // presence of key determines if author is present in doc + function getAuthorInfos(){ + return authorInfos; + }; + editorInfo.ace_getAuthorInfos= getAuthorInfos; + function setAuthorInfo(author, info) { if ((typeof author) != "string") @@ -884,7 +889,14 @@ function Ace2Inner(){ editorInfo.ace_setEditable = setEditable; editorInfo.ace_execCommand = execCommand; editorInfo.ace_replaceRange = replaceRange; - + editorInfo.ace_getAuthorInfos= getAuthorInfos; + editorInfo.ace_performDocumentReplaceRange = performDocumentReplaceRange; + editorInfo.ace_performDocumentReplaceCharRange = performDocumentReplaceCharRange; + editorInfo.ace_renumberList = renumberList; + editorInfo.ace_doReturnKey = doReturnKey; + editorInfo.ace_isBlockElement = isBlockElement; + editorInfo.ace_getLineListType = getLineListType; + editorInfo.ace_callWithAce = function(fn, callStack, normalize) { var wrapper = function() @@ -1686,11 +1698,55 @@ function Ace2Inner(){ if (selection && !selStart) { //if (domChanges) dmesg("selection not collected"); - selStart = getLineAndCharForPoint(selection.startPoint); + /* + * Called from: src/static/js/ace2_inner.js + * + * Context - + * callstack - a bunch of information about the current action + * editorInfo - information about the user who is making the change + * rep - information about where the change is being made + * root - the span element of the current line + * point - the starting element where the cursor currently resides + * documentAttributeManager - information about attributes in the document + * This hook is provided to allow a plugin to turn DOM node selection into [line,char] selection. + * The return value should be an array of [line,char] + * + */ + var selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', { + callstack: currentCallStack, + editorInfo: editorInfo, + rep: rep, + root:root, + point:selection.startPoint, + documentAttributeManager: documentAttributeManager + }); + selStart = selStartFromHook || getLineAndCharForPoint(selection.startPoint); } if (selection && !selEnd) { - selEnd = getLineAndCharForPoint(selection.endPoint); + /* + * Called from: src/static/js/ace2_inner.js + * + * Context - + * callstack - a bunch of information about the current action + * editorInfo - information about the user who is making the change + * rep - information about where the change is being made + * root - the span element of the current line + * point - the ending element where the cursor currently resides + * documentAttributeManager - information about attributes in the document + * This hook is provided to allow a plugin to turn DOM node selection into [line,char] selection. + * The return value should be an array of [line,char] + * + */ + var selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', { + callstack: currentCallStack, + editorInfo: editorInfo, + rep: rep, + root:root, + point:selection.endPoint, + documentAttributeManager: documentAttributeManager + }); + selEnd = selEndFromHook || getLineAndCharForPoint(selection.endPoint); } // selection from content collection can, in various ways, extend past final @@ -1845,17 +1901,20 @@ function Ace2Inner(){ { return rep.selStart[0]; } - + editorInfo.ace_caretLine = caretLine; + function caretColumn() { return rep.selStart[1]; } - + editorInfo.ace_caretColumn = caretColumn; + function caretDocChar() { return rep.lines.offsetOfIndex(caretLine()) + caretColumn(); } - + editorInfo.ace_caretDocChar = caretDocChar; + function handleReturnIndentation() { // on return, indent to level of previous line @@ -3447,7 +3506,8 @@ function Ace2Inner(){ { return !!REGEX_WORDCHAR.exec(c); } - + editorInfo.ace_isWordChar = isWordChar; + function isSpaceChar(c) { return !!REGEX_SPACE.exec(c); @@ -3555,7 +3615,30 @@ function Ace2Inner(){ if (!stopped) { - if (isTypeForSpecialKey && keyCode == 8) + /* + * Called from: src/static/js/ace2_inner.js + * + * Context - + * callstack - a bunch of information about the current action + * editorInfo - information about the user who is making the change + * rep - information about where the change is being made + * documentAttributeManager - information about attributes in the document + * evt - the fired event + * + * This hook is provided to allow a plugin to handle key events. + * The return value should true if you have handled the event. + * + */ + editorInfo.specialHandled = null; + specialHandled = hooks.callAll('aceKeyEvent', { + callstack: currentCallStack, + editorInfo: editorInfo, + rep: rep, + documentAttributeManager: documentAttributeManager, + evt:evt + }); + + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 8) { // "delete" key; in mozilla, if we're at the beginning of a line, normalize now, // or else deleting a blank line can take two delete presses. diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index a1860cbad..27d2940ab 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -376,18 +376,18 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class { var txt = dom.nodeValue(node); var tname = dom.nodeAttr(node.parentNode,"name"); - /* - * Called from: src/static/js/contentcollector.js - * - * Context - - * cc - the contentcollector object - * state - the current state of the change being made - * tname - the tag name of this node currently being processed - * text - the text for that line - * This hook allows you to validate/manipulate the text before it sent to the server side. - * The return value should the validate/manipulated text. - * - */ + /* + * Called from: src/static/js/contentcollector.js + * + * Context - + * cc - the contentcollector object + * state - the current state of the change being made + * tname - the tag name of this node currently being processed + * text - the text for that line + * This hook allows you to validate/manipulate the text before it sent to the server side. + * The return value should be the validated/manipulated text. + * + */ var txtFromHook = hooks.callAll('collectContentLineText', { cc: this, state: state, @@ -472,7 +472,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class * state - the current state of the change being made * tname - the tag name of this node currently being processed * - * This hook is provided to allow Whether the br tag should induce a new magic domline or not. + * This hook is provided to allow whether the br tag should induce a new magic domline or not. * The return value should be either true(break the line) or false. * */ From 9be69ef2582dfc2c1b050041d4586cbd90d20e2c Mon Sep 17 00:00:00 2001 From: Gedion Date: Tue, 11 Sep 2012 16:21:14 -0500 Subject: [PATCH 04/32] fixed plugins --- src/static/js/ace2_inner.js | 13 ++++++------- src/static/js/contentcollector.js | 14 ++++++++------ src/static/js/linestylefilter.js | 9 ++++++++- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index c262deb4c..4251d8786 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1719,8 +1719,8 @@ function Ace2Inner(){ root:root, point:selection.startPoint, documentAttributeManager: documentAttributeManager - }); - selStart = selStartFromHook || getLineAndCharForPoint(selection.startPoint); + }); + selStart = (selStartFromHook==null||selStartFromHook.length==0)?getLineAndCharForPoint(selection.startPoint):selStartFromHook; } if (selection && !selEnd) { @@ -1746,7 +1746,7 @@ function Ace2Inner(){ point:selection.endPoint, documentAttributeManager: documentAttributeManager }); - selEnd = selEndFromHook || getLineAndCharForPoint(selection.endPoint); + selEnd = (selEndFromHook==null||selEndFromHook.length==0)?getLineAndCharForPoint(selection.endPoint):selEndFromHook; } // selection from content collection can, in various ways, extend past final @@ -3628,16 +3628,15 @@ function Ace2Inner(){ * This hook is provided to allow a plugin to handle key events. * The return value should true if you have handled the event. * - */ - editorInfo.specialHandled = null; - specialHandled = hooks.callAll('aceKeyEvent', { + */ + var specialHandledInHook = hooks.callAll('aceKeyEvent', { callstack: currentCallStack, editorInfo: editorInfo, rep: rep, documentAttributeManager: documentAttributeManager, evt:evt }); - + specialHandled = (specialHandledInHook&&specialHandledInHook.length>0)?specialHandledInHook[0]:specialHandled; if ((!specialHandled) && isTypeForSpecialKey && keyCode == 8) { // "delete" key; in mozilla, if we're at the beginning of a line, normalize now, diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 27d2940ab..1f1f5f202 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -388,6 +388,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class * The return value should be the validated/manipulated text. * */ + //top.console.log(' nodevalue ',txt); var txtFromHook = hooks.callAll('collectContentLineText', { cc: this, state: state, @@ -397,7 +398,8 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class styl: null, cls: null }); - var txt = txtFromHook||txt; + var txt = (typeof(txtFromHook)=='object'&&txtFromHook.length==0)?dom.nodeValue(node):txtFromHook[0]; + var rest = ''; var x = 0; // offset into original text if (txt.length == 0) @@ -409,7 +411,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class if (endPoint && node == endPoint.node) { selEnd = _pointHere(0, state); - } + } } while (txt.length > 0) { @@ -476,8 +478,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class * The return value should be either true(break the line) or false. * */ - { - cc.startNewLine(state); + { this.breakLine = true; var tvalue = dom.nodeAttr(node, 'value'); var induceLineBreak = hooks.callAll('collectContentLineBreak', { @@ -487,8 +488,9 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class tvalue:tvalue, styl: null, cls: null - }); - if(induceLineBreak){ + }); + var startNewLine= (typeof(induceLineBreak)=='object'&&induceLineBreak.length==0)?true:induceLineBreak[0]; + if(startNewLine){ cc.startNewLine(state); } } diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 1cbfac29c..7da27b1df 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -146,9 +146,16 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun return function(txt, cls) { + + var disableAuthColorForThisLine = hooks.callAll("disableAuthorColorsForThisLine", { + linestylefilter: linestylefilter, + text: txt, + class: cls + }, " ", " ", ""); + var disableAuthors = (disableAuthColorForThisLine==null||disableAuthColorForThisLine.length==0)?false:disableAuthColorForThisLine[0]; while (txt.length > 0) { - if (leftInAuthor <= 0) + if (leftInAuthor <= 0 || disableAuthors) { // prevent infinite loop if something funny's going on return nextAfterAuthorColors(txt, cls); From 3364eb131e93b83c3cd889011e648a2a91778a61 Mon Sep 17 00:00:00 2001 From: Gedion Date: Tue, 11 Sep 2012 17:02:53 -0500 Subject: [PATCH 05/32] fixed comments --- src/static/js/ace2_inner.js | 44 +------------------------------ src/static/js/contentcollector.js | 26 +----------------- 2 files changed, 2 insertions(+), 68 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 4251d8786..9028dfcbe 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1698,20 +1698,6 @@ function Ace2Inner(){ if (selection && !selStart) { //if (domChanges) dmesg("selection not collected"); - /* - * Called from: src/static/js/ace2_inner.js - * - * Context - - * callstack - a bunch of information about the current action - * editorInfo - information about the user who is making the change - * rep - information about where the change is being made - * root - the span element of the current line - * point - the starting element where the cursor currently resides - * documentAttributeManager - information about attributes in the document - * This hook is provided to allow a plugin to turn DOM node selection into [line,char] selection. - * The return value should be an array of [line,char] - * - */ var selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', { callstack: currentCallStack, editorInfo: editorInfo, @@ -1724,20 +1710,6 @@ function Ace2Inner(){ } if (selection && !selEnd) { - /* - * Called from: src/static/js/ace2_inner.js - * - * Context - - * callstack - a bunch of information about the current action - * editorInfo - information about the user who is making the change - * rep - information about where the change is being made - * root - the span element of the current line - * point - the ending element where the cursor currently resides - * documentAttributeManager - information about attributes in the document - * This hook is provided to allow a plugin to turn DOM node selection into [line,char] selection. - * The return value should be an array of [line,char] - * - */ var selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', { callstack: currentCallStack, editorInfo: editorInfo, @@ -3615,20 +3587,6 @@ function Ace2Inner(){ if (!stopped) { - /* - * Called from: src/static/js/ace2_inner.js - * - * Context - - * callstack - a bunch of information about the current action - * editorInfo - information about the user who is making the change - * rep - information about where the change is being made - * documentAttributeManager - information about attributes in the document - * evt - the fired event - * - * This hook is provided to allow a plugin to handle key events. - * The return value should true if you have handled the event. - * - */ var specialHandledInHook = hooks.callAll('aceKeyEvent', { callstack: currentCallStack, editorInfo: editorInfo, @@ -3636,7 +3594,7 @@ function Ace2Inner(){ documentAttributeManager: documentAttributeManager, evt:evt }); - specialHandled = (specialHandledInHook&&specialHandledInHook.length>0)?specialHandledInHook[0]:specialHandled; + specialHandled = (specialHandledInHook&&specialHandledInHook.length>0)?specialHandledInHook[0]:specialHandled; if ((!specialHandled) && isTypeForSpecialKey && keyCode == 8) { // "delete" key; in mozilla, if we're at the beginning of a line, normalize now, diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 1f1f5f202..c1687489c 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -376,19 +376,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class { var txt = dom.nodeValue(node); var tname = dom.nodeAttr(node.parentNode,"name"); - /* - * Called from: src/static/js/contentcollector.js - * - * Context - - * cc - the contentcollector object - * state - the current state of the change being made - * tname - the tag name of this node currently being processed - * text - the text for that line - * This hook allows you to validate/manipulate the text before it sent to the server side. - * The return value should be the validated/manipulated text. - * - */ - //top.console.log(' nodevalue ',txt); + var txtFromHook = hooks.callAll('collectContentLineText', { cc: this, state: state, @@ -466,18 +454,6 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class { var tname = (dom.nodeTagName(node) || "").toLowerCase(); if (tname == "br") - /* - * Called from: src/static/js/contentcollector.js - * - * Context - - * cc - the contentcollector object - * state - the current state of the change being made - * tname - the tag name of this node currently being processed - * - * This hook is provided to allow whether the br tag should induce a new magic domline or not. - * The return value should be either true(break the line) or false. - * - */ { this.breakLine = true; var tvalue = dom.nodeAttr(node, 'value'); From a25feed1c2750c52e9aa43661b44fe2fb1aa53b5 Mon Sep 17 00:00:00 2001 From: Gedion Date: Tue, 11 Sep 2012 17:49:58 -0500 Subject: [PATCH 06/32] fixed indentation --- src/static/js/linestylefilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 7da27b1df..4231d26b6 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -147,7 +147,7 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun return function(txt, cls) { - var disableAuthColorForThisLine = hooks.callAll("disableAuthorColorsForThisLine", { + var disableAuthColorForThisLine = hooks.callAll("disableAuthorColorsForThisLine", { linestylefilter: linestylefilter, text: txt, class: cls From 40572b13b9814d8cbb32d5b6a569e942a8da97db Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 12 Sep 2012 17:15:38 +0200 Subject: [PATCH 07/32] Document, how return values of hooks are handled. --- doc/api/api.md | 2 +- doc/api/hooks.md | 11 +++++++++++ doc/api/hooks_client-side.md | 5 ----- doc/api/hooks_server-side.md | 5 ----- 4 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 doc/api/hooks.md diff --git a/doc/api/api.md b/doc/api/api.md index 830e5f4c4..b96fa0c8e 100644 --- a/doc/api/api.md +++ b/doc/api/api.md @@ -1,6 +1,6 @@ -# API @include embed_parameters @include http_api +@include hooks @include hooks_client-side @include hooks_server-side @include editorInfo diff --git a/doc/api/hooks.md b/doc/api/hooks.md new file mode 100644 index 000000000..c252aa840 --- /dev/null +++ b/doc/api/hooks.md @@ -0,0 +1,11 @@ +# Hooks +All hooks are called with two arguments: + +1. name - the name of the hook being called +2. context - an object with some relevant information about the context of the call + +## Return values +A hook should always return a list or undefined. Returning undefined is equivalent to returning an empty list. +All the returned lists are appended to each other, so if the return values where `[1, 2]`, `undefined`, `[3, 4,]`, `undefined` and `[5]`, the value returned by callHook would be `[1, 2, 3, 4, 5]`. + +This is, because it should never matter if you have one plugin or several plugins doing some work - a single plugin should be able to make callHook return the same value a set of plugins are able to return collectively. So, any plugin can return a list of values, of any length, not just one value. \ No newline at end of file diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index 50ec3f989..f706f6a12 100644 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -1,11 +1,6 @@ # Client-side hooks Most of these hooks are called during or in order to set up the formatting process. -All hooks registered to these events are called with two arguments: - -1. name - the name of the hook being called -2. context - an object with some relevant information about the context of the call - ## documentReady Called from: src/templates/pad.html diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 518e12130..06ec7374b 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -1,11 +1,6 @@ # Server-side hooks These hooks are called on server-side. -All hooks registered to these events are called with two arguments: - -1. name - the name of the hook being called -2. context - an object with some relevant information about the context of the call - ## loadSettings Called from: src/node/server.js From ad16c0d0d47a7240b5a550b6ddcf9a2dddcaba22 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 13 Sep 2012 16:13:54 +0200 Subject: [PATCH 08/32] Bump API version to v1.1 --- src/node/handler/APIHandler.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 9bffa2bcc..be14daa86 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -68,7 +68,9 @@ var version = , "isPasswordProtected" : ["padID"] , "listAuthorsOfPad" : ["padID"] , "padUsersCount" : ["padID"] - , "getAuthorName" : ["authorID"] + } +, "1.1": + { "getAuthorName" : ["authorID"] , "padUsers" : ["padID"] , "sendClientsMessage" : ["padID", "msg"] } From 71c9444694aea1d5f931963bb57f065b2b54efbd Mon Sep 17 00:00:00 2001 From: "NAGOYA, Yoshihiko" Date: Thu, 13 Sep 2012 23:19:53 +0900 Subject: [PATCH 09/32] fix for InternationalComposition(e.g., Japanese Input) moved inInternationalComposition from Ace2Inner to top window fix bindTheEventHandlers() because ie9 implement CompositionEvent when inInternationalComposition, NEW_CHANGES msg and ACCEPT_COMMIT msg are pushed msgQueue. when handleUserChanges(), apply msgQueue. --- src/static/js/ace2_inner.js | 20 +++-------- src/static/js/collab_client.js | 64 ++++++++++++++++++++++++++++++++-- src/static/js/pad.js | 16 +++++++++ 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 07580faa5..f56b05258 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1161,7 +1161,7 @@ function Ace2Inner(){ //if (! top.BEFORE) top.BEFORE = []; //top.BEFORE.push(magicdom.root.dom.innerHTML); //if (! isEditable) return; // and don't reschedule - if (inInternationalComposition) + if (window.parent.parent.inInternationalComposition) { // don't do idle input incorporation during international input composition idleWorkTimer.atLeast(500); @@ -1486,7 +1486,6 @@ function Ace2Inner(){ if (currentCallStack.domClean) return false; - inInternationalComposition = false; // if we need the document normalized, so be it currentCallStack.isUserChange = true; isTimeUp = (isTimeUp || @@ -3690,7 +3689,7 @@ function Ace2Inner(){ thisKeyDoesntTriggerNormalize = true; } - if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) + if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!window.parent.parent.inInternationalComposition)) { if (type != "keyup" || !incorpIfQuick()) { @@ -4550,19 +4549,9 @@ function Ace2Inner(){ } } - var inInternationalComposition = false; - function handleCompositionEvent(evt) { - // international input events, fired in FF3, at least; allow e.g. Japanese input - if (evt.type == "compositionstart") - { - inInternationalComposition = true; - } - else if (evt.type == "compositionend") - { - inInternationalComposition = false; - } + window.parent.parent.handleCompositionEvent(evt); } function bindTheEventHandlers() @@ -4577,7 +4566,8 @@ function Ace2Inner(){ $(document).on("click", handleIEOuterClick); } if (browser.msie) $(root).on("paste", handleIEPaste); - if ((!browser.msie) && document.documentElement) + // CompositionEvent is not implemented below IE version 8 + if ( !(browser.msie && browser.version < 9) && document.documentElement) { $(document.documentElement).on("compositionstart", handleCompositionEvent); $(document.documentElement).on("compositionend", handleCompositionEvent); diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index 7e9847108..d149b2565 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -62,6 +62,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) var caughtErrorCatchers = []; var caughtErrorTimes = []; var debugMessages = []; + var msgQueue = []; tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData); tellAceActiveAuthorInfo(initialUserInfo); @@ -110,6 +111,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) function handleUserChanges() { + if (window.parent.parent.inInternationalComposition) return; if ((!getSocket()) || channelState == "CONNECTING") { if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000)) @@ -128,12 +130,12 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) if (state != "IDLE") { - if (state == "COMMITTING" && (t - lastCommitTime) > 20000) + if (state == "COMMITTING" && msgQueue.length == 0 && (t - lastCommitTime) > 20000) { // a commit is taking too long setChannelState("DISCONNECTED", "slowcommit"); } - else if (state == "COMMITTING" && (t - lastCommitTime) > 5000) + else if (state == "COMMITTING" && msgQueue.length == 0 && (t - lastCommitTime) > 5000) { callbacks.onConnectionTrouble("SLOW"); } @@ -152,6 +154,36 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) return; } + // apply msgQueue changeset. + if (msgQueue.length != 0) { + while (msg = msgQueue.shift()) { + var newRev = msg.newRev; + rev=newRev; + if (msg.type == "ACCEPT_COMMIT") + { + editor.applyPreparedChangesetToBase(); + setStateIdle(); + callCatchingErrors("onInternalAction", function() + { + callbacks.onInternalAction("commitAcceptedByServer"); + }); + callCatchingErrors("onConnectionTrouble", function() + { + callbacks.onConnectionTrouble("OK"); + }); + handleUserChanges(); + } + else if (msg.type == "NEW_CHANGES") + { + var changeset = msg.changeset; + var author = (msg.author || ''); + var apool = msg.apool; + + editor.applyChangesToBase(changeset, author, apool); + } + } + } + var sentMessage = false; var userChangesData = editor.prepareUserChangeset(); if (userChangesData.changeset) @@ -254,6 +286,22 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) var changeset = msg.changeset; var author = (msg.author || ''); var apool = msg.apool; + + // When inInternationalComposition, msg pushed msgQueue. + if (msgQueue.length > 0 || window.parent.parent.inInternationalComposition) { + if (msgQueue.length > 0) oldRev = msgQueue[msgQueue.length - 1].newRev; + else oldRev = rev; + + if (newRev != (oldRev + 1)) + { + dmesg("bad message revision on NEW_CHANGES: " + newRev + " not " + (oldRev + 1)); + setChannelState("DISCONNECTED", "badmessage_newchanges"); + return; + } + msgQueue.push(msg); + return; + } + if (newRev != (rev + 1)) { dmesg("bad message revision on NEW_CHANGES: " + newRev + " not " + (rev + 1)); @@ -266,6 +314,18 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) else if (msg.type == "ACCEPT_COMMIT") { var newRev = msg.newRev; + if (msgQueue.length > 0) + { + if (newRev != (msgQueue[msgQueue.length - 1].newRev + 1)) + { + dmesg("bad message revision on ACCEPT_COMMIT: " + newRev + " not " + (msgQueue[msgQueue.length - 1][0] + 1)); + setChannelState("DISCONNECTED", "badmessage_acceptcommit"); + return; + } + msgQueue.push(msg); + return; + } + if (newRev != (rev + 1)) { dmesg("bad message revision on ACCEPT_COMMIT: " + newRev + " not " + (rev + 1)); diff --git a/src/static/js/pad.js b/src/static/js/pad.js index a22e181a2..b7fe9c3d4 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -50,6 +50,22 @@ var randomString = require('./pad_utils').randomString; var hooks = require('./pluginfw/hooks'); +window.inInternationalComposition = false; +var inInternationalComposition = window.inInternationalComposition; + +window.handleCompositionEvent = function handleCompositionEvent(evt) + { + // international input events, fired in FF3, at least; allow e.g. Japanese input + if (evt.type == "compositionstart") + { + this.inInternationalComposition = true; + } + else if (evt.type == "compositionend") + { + this.inInternationalComposition = false; + } + } + function createCookie(name, value, days, path) { if (days) From 6f37de2faef7b46ae5ad223aae1ead16aed1e140 Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 14 Sep 2012 17:33:45 +0200 Subject: [PATCH 10/32] Update src/package.json Bump to v 1.1.2 --- src/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package.json b/src/package.json index a2fc147ab..ca0bdbdca 100644 --- a/src/package.json +++ b/src/package.json @@ -42,5 +42,5 @@ "engines" : { "node" : ">=0.6.0", "npm" : ">=1.0" }, - "version" : "1.0.0" + "version" : "1.1.2" } From a0177e5d3ca132352d2a33a6a69e47c302c7be93 Mon Sep 17 00:00:00 2001 From: Chad Weider Date: Sun, 16 Sep 2012 18:07:55 -0700 Subject: [PATCH 11/32] Remember, the `class` symbol is reserved in some environments. Fixes issue introduced in 9be69ef2582dfc2c1b050041d4586cbd90d20e2c. --- src/static/js/linestylefilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 4231d26b6..c6434b6c2 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -150,7 +150,7 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun var disableAuthColorForThisLine = hooks.callAll("disableAuthorColorsForThisLine", { linestylefilter: linestylefilter, text: txt, - class: cls + "class": cls }, " ", " ", ""); var disableAuthors = (disableAuthColorForThisLine==null||disableAuthColorForThisLine.length==0)?false:disableAuthColorForThisLine[0]; while (txt.length > 0) From bbc8848af3460a8dca63f53bd575471faa7ec12e Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Mon, 17 Sep 2012 16:29:39 +0200 Subject: [PATCH 12/32] Still support API endpoints of v1 in v1.1 --- src/node/handler/APIHandler.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index be14daa86..2af7afef3 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -70,7 +70,35 @@ var version = , "padUsersCount" : ["padID"] } , "1.1": - { "getAuthorName" : ["authorID"] + { "createGroup" : [] + , "createGroupIfNotExistsFor" : ["groupMapper"] + , "deleteGroup" : ["groupID"] + , "listPads" : ["groupID"] + , "createPad" : ["padID", "text"] + , "createGroupPad" : ["groupID", "padName", "text"] + , "createAuthor" : ["name"] + , "createAuthorIfNotExistsFor": ["authorMapper" , "name"] + , "listPadsOfAuthor" : ["authorID"] + , "createSession" : ["groupID", "authorID", "validUntil"] + , "deleteSession" : ["sessionID"] + , "getSessionInfo" : ["sessionID"] + , "listSessionsOfGroup" : ["groupID"] + , "listSessionsOfAuthor" : ["authorID"] + , "getText" : ["padID", "rev"] + , "setText" : ["padID", "text"] + , "getHTML" : ["padID", "rev"] + , "setHTML" : ["padID", "html"] + , "getRevisionsCount" : ["padID"] + , "getLastEdited" : ["padID"] + , "deletePad" : ["padID"] + , "getReadOnlyID" : ["padID"] + , "setPublicStatus" : ["padID", "publicStatus"] + , "getPublicStatus" : ["padID"] + , "setPassword" : ["padID", "password"] + , "isPasswordProtected" : ["padID"] + , "listAuthorsOfPad" : ["padID"] + , "padUsersCount" : ["padID"] + , "getAuthorName" : ["authorID"] , "padUsers" : ["padID"] , "sendClientsMessage" : ["padID", "msg"] } From f8f002adc0ec152b15d67bca9b5288d6b46196e1 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Mon, 17 Sep 2012 23:03:56 +0200 Subject: [PATCH 13/32] Add listAllGroups API endpoint Adds a database key that lists all groups --- doc/api/http_api.md | 4 ++ doc/database.md | 3 ++ src/node/db/API.js | 1 + src/node/db/GroupManager.js | 70 +++++++++++++++++++++++++++++++++- src/node/handler/APIHandler.js | 31 ++++++++++++++- 5 files changed, 107 insertions(+), 2 deletions(-) diff --git a/doc/api/http_api.md b/doc/api/http_api.md index 058a2ba6c..3afab498f 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -135,6 +135,10 @@ Pads can belong to a group. The padID of grouppads is starting with a groupID li * `{code: 1, message:"pad does already exist", data: null}` * `{code: 1, message:"groupID does not exist", data: null}` +* **listAllGroups()** lists all existing groups

*Example returns:* + * `{code: 0, message:"ok", data: {groupIDs: ["g.mKjkmnAbSMtCt8eL", "g.3ADWx6sbGuAiUmCy"]}}` + * `{code: 0, message:"ok", data: {groupIDs: []}}` + ### Author These authors are bound to the attributes the users choose (color and name). diff --git a/doc/database.md b/doc/database.md index 2e06e2064..de3e9f547 100644 --- a/doc/database.md +++ b/doc/database.md @@ -2,6 +2,9 @@ ## Keys and their values +### groups +A list of all existing groups (a JSON object with groupIDs as keys and `1` as values). + ### pad:$PADID Saves all informations about pads diff --git a/src/node/db/API.js b/src/node/db/API.js index c5caae0be..4979e8c65 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -35,6 +35,7 @@ var cleanText = require("./Pad").cleanText; /**GROUP FUNCTIONS*****/ /**********************/ +exports.listAllGroups = groupManager.listAllGroups; exports.createGroup = groupManager.createGroup; exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor; exports.deleteGroup = groupManager.deleteGroup; diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index bd19507ff..81b0cb9ef 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -26,6 +26,24 @@ var db = require("./DB").db; var async = require("async"); var padManager = require("./PadManager"); var sessionManager = require("./SessionManager"); + +exports.listAllGroups = function(callback) { + db.get("groups", function (err, groups) { + if(ERR(err, callback)) return; + + // there are no groups + if(groups == null) { + callback(null, {groupIDs: []}); + return; + } + + var groupIDs = []; + for ( var groupID in groups ) { + groupIDs.push(groupID); + } + callback(null, {groupIDs: groupIDs}); + }); +} exports.deleteGroup = function(groupID, callback) { @@ -105,6 +123,39 @@ exports.deleteGroup = function(groupID, callback) db.remove("group2sessions:" + groupID); db.remove("group:" + groupID); callback(); + }, + //unlist the group + function(callback) + { + exports.listAllGroups(function(err, groups) { + if(ERR(err, callback)) return; + groups = groups? groups.groupIDs : []; + + // it's not listed + if(groups.indexOf(groupID) == -1) { + callback(); + return; + } + + groups.splice(groups.indexOf(groupID), 1); + + // store empty groupe list + if(groups.length == 0) { + db.set("groups", {}); + callback(); + return; + } + + // regenerate group list + var newGroups = {}; + async.forEach(groups, function(group, cb) { + newGroups[group] = 1; + cb(); + },function() { + db.set("groups", newGroups); + callback(); + }); + }); } ], function(err) { @@ -130,7 +181,24 @@ exports.createGroup = function(callback) //create the group db.set("group:" + groupID, {pads: {}}); - callback(null, {groupID: groupID}); + + //list the group + exports.listAllGroups(function(err, groups) { + if(ERR(err, callback)) return; + groups = groups? groups.groupIDs : []; + + groups.push(groupID); + + // regenerate group list + var newGroups = {}; + async.forEach(groups, function(group, cb) { + newGroups[group] = 1; + cb(); + },function() { + db.set("groups", newGroups); + callback(null, {groupID: groupID}); + }); + }); } exports.createGroupIfNotExistsFor = function(groupMapper, callback) diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index be14daa86..f99762cea 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -70,9 +70,38 @@ var version = , "padUsersCount" : ["padID"] } , "1.1": - { "getAuthorName" : ["authorID"] + { "createGroup" : [] + , "createGroupIfNotExistsFor" : ["groupMapper"] + , "deleteGroup" : ["groupID"] + , "listPads" : ["groupID"] + , "createPad" : ["padID", "text"] + , "createGroupPad" : ["groupID", "padName", "text"] + , "createAuthor" : ["name"] + , "createAuthorIfNotExistsFor": ["authorMapper" , "name"] + , "listPadsOfAuthor" : ["authorID"] + , "createSession" : ["groupID", "authorID", "validUntil"] + , "deleteSession" : ["sessionID"] + , "getSessionInfo" : ["sessionID"] + , "listSessionsOfGroup" : ["groupID"] + , "listSessionsOfAuthor" : ["authorID"] + , "getText" : ["padID", "rev"] + , "setText" : ["padID", "text"] + , "getHTML" : ["padID", "rev"] + , "setHTML" : ["padID", "html"] + , "getRevisionsCount" : ["padID"] + , "getLastEdited" : ["padID"] + , "deletePad" : ["padID"] + , "getReadOnlyID" : ["padID"] + , "setPublicStatus" : ["padID", "publicStatus"] + , "getPublicStatus" : ["padID"] + , "setPassword" : ["padID", "password"] + , "isPasswordProtected" : ["padID"] + , "listAuthorsOfPad" : ["padID"] + , "padUsersCount" : ["padID"] + , "getAuthorName" : ["authorID"] , "padUsers" : ["padID"] , "sendClientsMessage" : ["padID", "msg"] + , "listAllGroups" : [] } }; From 923b51033b1a0a2e358371650393c7460e65ad14 Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 18 Sep 2012 15:54:08 +0200 Subject: [PATCH 14/32] List 12 plugins instead of 4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 was a bit stingy :)  12 is a bit more friendly from a UX persepctive. --- src/static/js/admin/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js index 742c3bb22..93acf6ea5 100644 --- a/src/static/js/admin/plugins.js +++ b/src/static/js/admin/plugins.js @@ -15,7 +15,7 @@ $(document).ready(function () { $('.search-results').data('query', { pattern: '', offset: 0, - limit: 4, + limit: 12, }); var doUpdate = false; From 443a71bc9ce33b150fbfd06332a20b072250c24e Mon Sep 17 00:00:00 2001 From: johnyma22 Date: Tue, 18 Sep 2012 16:30:26 +0100 Subject: [PATCH 15/32] Fixed foreach loop on session IDs, was breaking EP on single session in cookie. --- src/node/db/SecurityManager.js | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 1894ee59a..7e3581746 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -123,26 +123,29 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback) } var sessionIDs = sessionCookie.split(','); - async.foreach(sessionIDs, function(sessionID) { - sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) { - //skip session if it doesn't exist - if(err && err.message == "sessionID does not exist") return; + if (sessionIDs){ + async.forEach(sessionIDs, function(sessionID, cb){ + sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) { + //skip session if it doesn't exist + if(err && err.message == "sessionID does not exist") return; - if(ERR(err, callback)) return; + if(ERR(err, callback)) return; - var now = Math.floor(new Date().getTime()/1000); + var now = Math.floor(new Date().getTime()/1000); - //is it for this group? - if(sessionInfo.groupID != groupID) return; + //is it for this group? + if(sessionInfo.groupID != groupID) return; - //is validUntil still ok? - if(sessionInfo.validUntil <= now) return; + //is validUntil still ok? + if(sessionInfo.validUntil <= now) return; - // There is a valid session - validSession = true; - sessionAuthor = sessionInfo.authorID; - }); - }, callback) + // There is a valid session + validSession = true; + sessionAuthor = sessionInfo.authorID; + cb(); // finish the current value and go to next + }); + }, callback) + } }, //get author for token function(callback) From b9da0e187edc05626f315d781ec2b0d626b88dba Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 19 Sep 2012 17:42:36 +0200 Subject: [PATCH 16/32] Revert "Fixed foreach loop on session IDs, was breaking EP on single session in cookie." This reverts commit 443a71bc9ce33b150fbfd06332a20b072250c24e. modified: src/node/db/SecurityManager.js --- src/node/db/SecurityManager.js | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 7e3581746..1894ee59a 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -123,29 +123,26 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback) } var sessionIDs = sessionCookie.split(','); - if (sessionIDs){ - async.forEach(sessionIDs, function(sessionID, cb){ - sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) { - //skip session if it doesn't exist - if(err && err.message == "sessionID does not exist") return; + async.foreach(sessionIDs, function(sessionID) { + sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) { + //skip session if it doesn't exist + if(err && err.message == "sessionID does not exist") return; - if(ERR(err, callback)) return; + if(ERR(err, callback)) return; - var now = Math.floor(new Date().getTime()/1000); + var now = Math.floor(new Date().getTime()/1000); - //is it for this group? - if(sessionInfo.groupID != groupID) return; + //is it for this group? + if(sessionInfo.groupID != groupID) return; - //is validUntil still ok? - if(sessionInfo.validUntil <= now) return; + //is validUntil still ok? + if(sessionInfo.validUntil <= now) return; - // There is a valid session - validSession = true; - sessionAuthor = sessionInfo.authorID; - cb(); // finish the current value and go to next - }); - }, callback) - } + // There is a valid session + validSession = true; + sessionAuthor = sessionInfo.authorID; + }); + }, callback) }, //get author for token function(callback) From a72ade4494ad0df10fd99bd7692d73546e5284f7 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 19 Sep 2012 17:48:26 +0200 Subject: [PATCH 17/32] Fix async.forEach in MultiSession code --- src/node/db/SecurityManager.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 1894ee59a..59e27b550 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -123,7 +123,7 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback) } var sessionIDs = sessionCookie.split(','); - async.foreach(sessionIDs, function(sessionID) { + async.forEach(sessionIDs, function(sessionID, callback) { sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) { //skip session if it doesn't exist if(err && err.message == "sessionID does not exist") return; @@ -141,8 +141,10 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback) // There is a valid session validSession = true; sessionAuthor = sessionInfo.authorID; + + callback(); }); - }, callback) + }, callback); }, //get author for token function(callback) From 49915dfeb8627e28104aee666eec019646eea5ef Mon Sep 17 00:00:00 2001 From: Chad Weider Date: Fri, 21 Sep 2012 21:58:49 -0700 Subject: [PATCH 18/32] Upgrade to Yajsml with another Windows backslash fix. --- src/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package.json b/src/package.json index ca0bdbdca..2285f0ca8 100644 --- a/src/package.json +++ b/src/package.json @@ -10,7 +10,7 @@ "name": "Robin Buse" } ], "dependencies" : { - "yajsml" : "1.1.4", + "yajsml" : "1.1.5", "request" : "2.9.100", "require-kernel" : "1.0.5", "resolve" : "0.2.x", From fa65f889ec8174b2b723fcc45b7d391780b9cd52 Mon Sep 17 00:00:00 2001 From: Chad Weider Date: Tue, 11 Sep 2012 20:52:48 -0700 Subject: [PATCH 19/32] Consolidate Ace2Editor frame's boot script. --- src/static/js/ace.js | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index db62deb42..e7e4fca86 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -24,6 +24,8 @@ // requires: plugins // requires: undefined +var KERNEL_SOURCE = '../static/js/require-kernel.js'; + Ace2Editor.registry = { nextId: 1 }; @@ -155,24 +157,6 @@ function Ace2Editor() return {embeded: embededFiles, remote: remoteFiles}; } - function pushRequireScriptTo(buffer) { - var KERNEL_SOURCE = '../static/js/require-kernel.js'; - var KERNEL_BOOT = '\ -require.setRootURI("../javascripts/src");\n\ -require.setLibraryURI("../javascripts/lib");\n\ -require.setGlobalKeyPath("require");\n\ -'; - if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) { - buffer.push('\ +\n\ + $ = jQuery = require("ep_etherpad-lite/static/js/rjquery").jQuery; // Expose jQuery #HACK\n\ + require("ep_etherpad-lite/static/js/ace2_inner");\n\ +\n\ '); - - iframeHTML.push('' + ) +} + function Ace2Editor() { var ace2 = Ace2Editor; @@ -226,23 +234,20 @@ function Ace2Editor() throw new Error("Require kernel could not be found."); } - iframeHTML.push('\ -\n\ -'); - iframeHTML.push('<\/script>'); +$ = jQuery = require("ep_etherpad-lite/static/js/rjquery").jQuery; // Expose jQuery #HACK\n\ +require("ep_etherpad-lite/static/js/ace2_inner");\n\ +')); iframeHTML.push(''); @@ -256,8 +261,32 @@ function Ace2Editor() var thisFunctionsName = "ChildAccessibleAce2Editor"; (function () {return this}())[thisFunctionsName] = Ace2Editor; - var outerScript = 'editorId = "' + info.id + '"; editorInfo = parent.' + thisFunctionsName + '.registry[editorId]; ' + 'window.onload = function() ' + '{ window.onload = null; setTimeout' + '(function() ' + '{ var iframe = document.createElement("IFRAME"); iframe.name = "ace_inner";' + 'iframe.scrolling = "no"; var outerdocbody = document.getElementById("outerdocbody"); ' + 'iframe.frameBorder = 0; iframe.allowTransparency = true; ' + // for IE - 'outerdocbody.insertBefore(iframe, outerdocbody.firstChild); ' + 'iframe.ace_outerWin = window; ' + 'readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; ' + 'var doc = iframe.contentWindow.document; doc.open(); var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');doc.write(text); doc.close(); ' + '}, 0); }'; + var outerScript = '\ +editorId = ' + JSON.stringify(info.id) + ';\n\ +editorInfo = parent[' + JSON.stringify(thisFunctionsName) + '].registry[editorId];\n\ +window.onload = function () {\n\ + window.onload = null;\n\ + setTimeout(function () {\n\ + var iframe = document.createElement("IFRAME");\n\ + iframe.name = "ace_inner";\n\ + iframe.scrolling = "no";\n\ + var outerdocbody = document.getElementById("outerdocbody");\n\ + iframe.frameBorder = 0;\n\ + iframe.allowTransparency = true; // for IE\n\ + outerdocbody.insertBefore(iframe, outerdocbody.firstChild);\n\ + iframe.ace_outerWin = window;\n\ + readyFunc = function () {\n\ + editorInfo.onEditorReady();\n\ + readyFunc = null;\n\ + editorInfo = null;\n\ + };\n\ + var doc = iframe.contentWindow.document;\n\ + doc.open();\n\ + var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');\n\ + doc.write(text);\n\ + doc.close();\n\ + }, 0);\n\ +}'; var outerHTML = [doctype, ''] @@ -275,7 +304,7 @@ function Ace2Editor() // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly // (throbs busy while typing) - outerHTML.push('', '\x3cscript>\n', outerScript.replace(/<\//g, '<\\/'), '\n\x3c/script>', '
x
'); + outerHTML.push('', scriptTag(outerScript), '
x
'); var outerFrame = document.createElement("IFRAME"); outerFrame.name = "ace_outer"; From 622819ba930b5d4685c053eefaac77c1776fe0b0 Mon Sep 17 00:00:00 2001 From: Chad Weider Date: Wed, 12 Sep 2012 00:04:15 -0700 Subject: [PATCH 21/32] Make intialization of Ace2Inner analogous to other page controllers. --- src/static/js/ace.js | 6 ++- src/static/js/ace2_inner.js | 94 +++++++++++++++++++------------------ 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 1561dea5e..e50f75c76 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -246,7 +246,11 @@ hooks.plugins = plugins;\n\ plugins.adoptPluginsFromAncestorsOf(window);\n\ \n\ $ = jQuery = require("ep_etherpad-lite/static/js/rjquery").jQuery; // Expose jQuery #HACK\n\ -require("ep_etherpad-lite/static/js/ace2_inner");\n\ +var Ace2Inner = require("ep_etherpad-lite/static/js/ace2_inner");\n\ +\n\ +plugins.ensure(function () {\n\ + Ace2Inner.init();\n\ +});\n\ ')); iframeHTML.push(''); diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 962eda2ac..652a3d259 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var editor, _, $, jQuery, plugins, Ace2Common; +var _, $, jQuery, plugins, Ace2Common; Ace2Common = require('./ace2_common'); @@ -5430,62 +5430,64 @@ function Ace2Inner(){ return documentAttributeManager.setAttributesOnRange.apply(documentAttributeManager, arguments); }; - $(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) + this.init = function () { + $(document).ready(function(){ + doc = document; // defined as a var in scope outside + inCallStack("setup", function() { - // cache CSS background images - try + 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) { - doc.execCommand("BackgroundImageCache", false, true); + // cache CSS background images + try + { + doc.execCommand("BackgroundImageCache", false, true); + } + catch (e) + { /* throws an error in some IE 6 but not others! */ + } } - catch (e) - { /* throws an error in some IE 6 but not others! */ - } - } - setClassPresence(root, "authorColors", true); - setClassPresence(root, "doesWrap", doesWrap); + setClassPresence(root, "authorColors", true); + setClassPresence(root, "doesWrap", doesWrap); - initDynamicCSS(); + initDynamicCSS(); - enforceEditability(); + 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"); + // 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(); + bindTheEventHandlers(); - }); + }); - hooks.callAll('aceInitialized', { - editorInfo: editorInfo, - rep: rep, - documentAttributeManager: documentAttributeManager - }); + hooks.callAll('aceInitialized', { + editorInfo: editorInfo, + rep: rep, + documentAttributeManager: documentAttributeManager + }); - scheduler.setTimeout(function() - { - parent.readyFunc(); // defined in code that sets up the inner iframe - }, 0); + scheduler.setTimeout(function() + { + parent.readyFunc(); // defined in code that sets up the inner iframe + }, 0); - isSetUp = true; - }); + isSetUp = true; + }); + } } -// Ensure that plugins are loaded before initializing the editor -plugins.ensure(function () { - var editor = new Ace2Inner(); -}); +exports.init = function () { + var editor = new Ace2Inner() + editor.init(); +}; From 087560ea6c9b75e2ffdb36d452463d6891f39e07 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sat, 22 Sep 2012 12:55:49 +0200 Subject: [PATCH 22/32] Let Github know our Dev Guidelines https://github.com/blog/1184-contributing-guidelines --- CONTRIBUTING.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..5f081f27e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Developer Guidelines + +Please talk to people on the mailing list before you change this page + +Mailing list: https://groups.google.com/forum/?fromgroups#!forum/etherpad-lite-dev + +IRC channels: [#etherpad](irc://freenode/#etherpad) ([webchat](webchat.freenode.net?channels=etherpad)), [#etherpad-lite-dev](irc://freenode/#etherpad-lite-dev) ([webchat](webchat.freenode.net?channels=etherpad-lite-dev)) + +**Our goal is to iterate in small steps. Release often, release early. Evolution instead of a revolution** + +## General goals of Etherpad Lite +* easy to install for admins +* easy to use for people +* using less resources on server side +* easy to embed for admins +* also runable as etherpad lite only +* keep it maintainable, we don't wanna end ob as the monster Etherpad was +* extensible, as much functionality should be extendable with plugins so changes don't have to be done in core + +## How to code: +* **Please write comments**. I don't mean you have to comment every line and every loop. I just mean, if you do anything thats a bit complex or a bit weird, please leave a comment. It's easy to do that if you do while you're writing the code. Keep in mind that you will probably leave the project at some point and that other people will read your code. Undocumented huge amounts of code are worthless +* Never ever use tabs +* Indentation: JS/CSS: 2 spaces; HTML: 4 spaces +* Don't overengineer. Don't try to solve any possible problem in one step. Try to solve problems as easy as possible and improve the solution over time +* Do generalize sooner or later - if an old solution hacked together according to the above point, poses more problems than it solves today, reengineer it, with the lessons learned taken into account. +* Keep it compatible to API-Clients/older DBs/configurations. Don't make incompatible changes the protocol/database format without good reasons + +## How to work with git +* Make a new branch for every feature you're working on. Don't work in your master branch. This ensures that you can work you can do lot of small pull requests instead of one big one with complete different features +* Don't use the online edit function of github. This only creates ugly and not working commits +* Test before you push. Sounds easy, it isn't +* Try to make clean commits that are easy readable +* Don't check in stuff that gets generated during build or runtime (like jquery, minified files, dbs etc...) +* Make pull requests from your feature branch to our develop branch once your feature is ready +* Make small pull requests that are easy to review but make sure they do add value by themselves / individually + +## Branching model in Etherpad Lite +see git flow http://nvie.com/posts/a-successful-git-branching-model/ + +* master, the stable. This is the branch everyone should use for production stuff +* develop, everything that is READY to go into master at some point in time. This stuff is tested and ready to go out +* release branches, stuff that should go into master very soon, only bugfixes go into these (see http://nvie.com/posts/a-successful-git-branching-model/ for why) +* you can set tags in the master branch, there is no real need for release branches imho +* The latest tag is not what is shown in github by default. Doing a clone of master should give you latest stable, not what is gonna be latest stable in a week, also, we should not be blocking new features to develop, just because we feel that we should be releasing it to master soon. This is the situation that release branches solve/handle. +* hotfix branches, fixes for bugs in master +* feature branches (in your own repos), these are the branches where you develop your features in. If its ready to go out, it will be merged into develop + +Over the time we pull features from feature branches into the develop branch. Every month we pull from develop into master. Bugs in master get fixed in hotfix branches. These branches will get merged into master AND develop. There should never be commits in master that aren't in develop + +## Documentation +The docs are in the `doc/` folder in the git repository, so people can easily find the suitable docs for the current git revision. + +Documentation should be kept up-to-date. This means, whenever you add a new API method, add a new hook or change the database model, pack the relevant changes to the docs in the same pull request. + +You can build the docs e.g. produce html, using `make docs`. At some point in the future we will provide an online documentation. The current documentation in the github wiki should always reflect the state of `master` (!), since there are no docs in master, yet. \ No newline at end of file From e16008b3719855d95cf0bb3402a0ac67eb319d47 Mon Sep 17 00:00:00 2001 From: Richard Braakman Date: Wed, 26 Sep 2012 03:01:53 +0300 Subject: [PATCH 23/32] Fix sessioninfos race that can cause crash during USER_CHANGES handling When stress testing etherpad-lite we occasionally got this error: TypeError: Cannot read property 'author' of undefined at /home/etherpad/etherpad-lite/src/node/handler/PadMessageHandler.js:556:47 handleUserChanges was accessing sessioninfos[client.id].author in a callback, after spending some time in the loop that updates the changeset to the latest revision. It's possible for a disconnect request to be processed during that loop so the session might no longer be there. This patch fixes it by looking up the author at the start of the function. --- src/node/handler/PadMessageHandler.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 913433b01..160686804 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -465,6 +465,7 @@ function handleUserChanges(client, message) var baseRev = message.data.baseRev; var wireApool = (new AttributePool()).fromJsonable(message.data.apool); var changeset = message.data.changeset; + var thisAuthor = sessioninfos[client.id].author; var r, apool, pad; @@ -545,8 +546,6 @@ function handleUserChanges(client, message) return; } - var thisAuthor = sessioninfos[client.id].author; - pad.appendRevision(changeset, thisAuthor); var correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); From f1b4206cadb716dd3c4b2c924e0b988431034263 Mon Sep 17 00:00:00 2001 From: Richard Braakman Date: Wed, 26 Sep 2012 03:01:24 +0300 Subject: [PATCH 24/32] Fix crash when client submits changeset based on too-old revision We had a problem with the server running out of stack space if a client submitted a changeset based on a revision more than about 1000 revs old. (944 was our cutoff but yours may vary). This happened in the wild with about 30 people editing via flaky wifi. A disconnected client would try to submit a fairly old changeset when reconnecting, and a few minutes was enough for 30 people to generate that many revs. The stack kept growing because pad.getRevisionChangeset was being answered from the cache, so no I/O interrupted the callback chain. (This was seen with mysql, I don't know about other backends.) This patch forces a nextTick every 200 revisions to solve this problem. --- src/node/handler/PadMessageHandler.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 160686804..6a462b019 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -526,7 +526,11 @@ function handleUserChanges(client, message) if(ERR(err, callback)) return; changeset = Changeset.follow(c, changeset, false, apool); - callback(null); + if ((r - baseRev) % 200 == 0) { // don't let the stack get too deep + async.nextTick(callback); + } else { + callback(null); + } }); }, //use the callback of the series function From 7aaef01346ca61d7778c15f9b35f72713065f297 Mon Sep 17 00:00:00 2001 From: Richard Braakman Date: Thu, 27 Sep 2012 23:05:18 +0300 Subject: [PATCH 25/32] Prettify session handling in handleUserChanges Also add a comment to explain what's going on with thisSession. No changes in behavior. --- src/node/handler/PadMessageHandler.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 6a462b019..8a5a92bb6 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -465,7 +465,9 @@ function handleUserChanges(client, message) var baseRev = message.data.baseRev; var wireApool = (new AttributePool()).fromJsonable(message.data.apool); var changeset = message.data.changeset; - var thisAuthor = sessioninfos[client.id].author; + // The client might disconnect between our callbacks. We should still + // finish processing the changeset, so keep a reference to the session. + var thisSession = sessioninfos[client.id]; var r, apool, pad; @@ -473,7 +475,7 @@ function handleUserChanges(client, message) //get the pad function(callback) { - padManager.getPad(sessioninfos[client.id].padId, function(err, value) + padManager.getPad(thisSession.padId, function(err, value) { if(ERR(err, callback)) return; pad = value; @@ -550,7 +552,7 @@ function handleUserChanges(client, message) return; } - pad.appendRevision(changeset, thisAuthor); + pad.appendRevision(changeset, thisSession.author); var correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); if (correctionChangeset) { From 413ddb393e98b966fa14c934753e63be18d81912 Mon Sep 17 00:00:00 2001 From: Richard Braakman Date: Fri, 28 Sep 2012 22:49:20 +0300 Subject: [PATCH 26/32] Add some explanatory comments to handleUserChanges() --- src/node/handler/PadMessageHandler.js | 41 +++++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 8a5a92bb6..f60d91da6 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -436,15 +436,22 @@ function handleUserInfoUpdate(client, message) } /** - * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations - * This Method is nearly 90% copied out of the Etherpad Source Code. So I can't tell you what happens here exactly - * Look at https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges() + * Handles a USER_CHANGES message, where the client submits its local + * edits as a changeset. + * + * This handler's job is to update the incoming changeset so that it applies + * to the latest revision, then add it to the pad, broadcast the changes + * to all other clients, and send a confirmation to the submitting client. + * + * This function is based on a similar one in the original Etherpad. + * See https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges() + * * @param client the client that send this message * @param message the message from the client */ function handleUserChanges(client, message) { - //check if all ok + // Make sure all required fields are present if(message.data.baseRev == null) { messageLogger.warn("Dropped message, USER_CHANGES Message has no baseRev!"); @@ -487,22 +494,23 @@ function handleUserChanges(client, message) { //ex. _checkChangesetAndPool - //Copied from Etherpad, don't know what it does exactly try { - //this looks like a changeset check, it throws errors sometimes + // Verify that the changeset has valid syntax and is in canonical form Changeset.checkRep(changeset); - + + // Verify that the attribute indexes used in the changeset are all + // defined in the accompanying attribute pool. Changeset.eachAttribNumber(changeset, function(n) { if (! wireApool.getAttrib(n)) { throw "Attribute pool is missing attribute "+n+" for changeset "+changeset; } }); } - //there is an error in this changeset, so just refuse it catch(e) { - console.warn("Can't apply USER_CHANGES "+changeset+", cause it faild checkRep"); + // There is an error in this changeset, so just refuse it + console.warn("Can't apply USER_CHANGES "+changeset+", because it failed checkRep"); client.json.send({disconnect:"badChangeset"}); return; } @@ -515,7 +523,10 @@ function handleUserChanges(client, message) //ex. applyUserChanges apool = pad.pool; r = baseRev; - + + // The client's changeset might not be based on the latest revision, + // since other clients are sending changes at the same time. + // Update the changeset so that it can be applied to the latest revision. //https://github.com/caolan/async#whilst async.whilst( function() { return r < pad.getHeadRevisionNumber(); }, @@ -526,8 +537,13 @@ function handleUserChanges(client, message) pad.getRevisionChangeset(r, function(err, c) { if(ERR(err, callback)) return; - + + // At this point, both "c" (from the pad) and "changeset" (from the + // client) are relative to revision r - 1. The follow function + // rebases "changeset" so that it is relative to revision r + // and can be applied after "c". changeset = Changeset.follow(c, changeset, false, apool); + if ((r - baseRev) % 200 == 0) { // don't let the stack get too deep async.nextTick(callback); } else { @@ -558,7 +574,8 @@ function handleUserChanges(client, message) if (correctionChangeset) { pad.appendRevision(correctionChangeset); } - + + // Make sure the pad always ends with an empty line. if (pad.text().lastIndexOf("\n\n") != pad.text().length-2) { var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length-1, 0, "\n"); pad.appendRevision(nlChangeset); From 2e72a1e4895688bc4cc8340a9b49f306b7d41600 Mon Sep 17 00:00:00 2001 From: Richard Braakman Date: Fri, 28 Sep 2012 23:03:42 +0300 Subject: [PATCH 27/32] Prevent server crash in handleClientReady The client might have disconnected between callbacks so don't try to write to the session before checking this. The main callback of this function now has a single check at its top. Removed a redundant check halfway through the callback. Also normalized use of client.id for the session index instead of a mix of client.id and sessionId. Added some explanatory comments. --- src/node/handler/PadMessageHandler.js | 48 ++++++++++++++------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index f60d91da6..10b259ae2 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -882,6 +882,13 @@ function handleClientReady(client, message) }, function(callback) { + //Check that the client is still here. It might have disconnected between callbacks. + if(sessioninfos[client.id] === undefined) + { + callback(); + return; + } + //Check if this author is already on the pad, if yes, kick the other sessions! if(pad2sessions[padIds.padId]) { @@ -896,10 +903,9 @@ function handleClientReady(client, message) } //Save in sessioninfos that this session belonges to this pad - var sessionId=String(client.id); - sessioninfos[sessionId].padId = padIds.padId; - sessioninfos[sessionId].readOnlyPadId = padIds.readOnlyPadId; - sessioninfos[sessionId].readonly = padIds.readonly; + sessioninfos[client.id].padId = padIds.padId; + sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId; + sessioninfos[client.id].readonly = padIds.readonly; //check if there is already a pad2sessions entry, if not, create one if(!pad2sessions[padIds.padId]) @@ -908,7 +914,7 @@ function handleClientReady(client, message) } //Saves in pad2sessions that this session belongs to this pad - pad2sessions[padIds.padId].push(sessionId); + pad2sessions[padIds.padId].push(client.id); //prepare all values for the wire var atext = Changeset.cloneAText(pad.atext); @@ -973,26 +979,22 @@ function handleClientReady(client, message) clientVars.userName = authorName; } - if(sessioninfos[client.id] !== undefined) + //If this is a reconnect, we don't have to send the client the ClientVars again + if(message.reconnect == true) { - //This is a reconnect, so we don't have to send the client the ClientVars again - if(message.reconnect == true) - { - //Save the revision in sessioninfos, we take the revision from the info the client send to us - sessioninfos[client.id].rev = message.client_rev; - } - //This is a normal first connect - else - { - //Send the clientVars to the Client - client.json.send({type: "CLIENT_VARS", data: clientVars}); - //Save the revision in sessioninfos - sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); - } - - //Save the revision and the author id in sessioninfos - sessioninfos[client.id].author = author; + //Save the revision in sessioninfos, we take the revision from the info the client send to us + sessioninfos[client.id].rev = message.client_rev; } + //This is a normal first connect + else + { + //Send the clientVars to the Client + client.json.send({type: "CLIENT_VARS", data: clientVars}); + //Save the current revision in sessioninfos, should be the same as in clientVars + sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); + } + + sessioninfos[client.id].author = author; //prepare the notification for the other users on the pad, that this user joined var messageToTheOtherUsers = { From 3fe3df91aee6618737feb2a4c1366276c14f0cb1 Mon Sep 17 00:00:00 2001 From: Gedion Date: Sun, 30 Sep 2012 17:13:14 -0500 Subject: [PATCH 28/32] update docs for new hooks and ace exposures --- doc/api/editorInfo.md | 11 ++++++ doc/api/hooks_client-side.md | 68 ++++++++++++++++++++++++++++++++++++ doc/api/hooks_server-side.md | 13 +++++++ 3 files changed, 92 insertions(+) diff --git a/doc/api/editorInfo.md b/doc/api/editorInfo.md index e4322e9e1..a212ff085 100644 --- a/doc/api/editorInfo.md +++ b/doc/api/editorInfo.md @@ -45,3 +45,14 @@ Returns the `rep` object. ## editorInfo.ace_doInsertUnorderedList(?) ## editorInfo.ace_doInsertOrderedList(?) ## editorInfo.ace_performDocumentApplyAttributesToRange() +## editorInfo.ace_getAuthorInfos() +## editorInfo.ace_performDocumentReplaceRange(?) +## editorInfo.ace_performDocumentReplaceCharRange(?) +## editorInfo.ace_renumberList(?) +## editorInfo.ace_doReturnKey() +## editorInfo.ace_isBlockElement(?) +## editorInfo.ace_getLineListType(?) +## editorInfo.ace_caretLine() +## editorInfo.ace_caretColumn() +## editorInfo.ace_caretDocChar() +## editorInfo.ace_isWordChar(?) diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index f706f6a12..707800f5e 100644 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -174,3 +174,71 @@ Things in context: This hook gets called every time the client receives a message of type `name`. This can most notably be used with the new HTTP API call, "sendClientsMessage", which sends a custom message type to all clients connected to a pad. You can also use this to handle existing types. `collab_client.js` has a pretty extensive list of message types, if you want to take a look. + +##aceStartLineAndCharForPoint-aceEndLineAndCharForPoint +Called from: src/static/js/ace2_inner.js + +Things in context: + +1. callstack - a bunch of information about the current action +2. editorInfo - information about the user who is making the change +3. rep - information about where the change is being made +4. root - the span element of the current line +5. point - the starting/ending element where the cursor highlights +6. documentAttributeManager - information about attributes in the document + +This hook is provided to allow a plugin to turn DOM node selection into [line,char] selection. +The return value should be an array of [line,char] + +##aceKeyEvent +Called from: src/static/js/ace2_inner.js + +Things in context: + +1. callstack - a bunch of information about the current action +2. editorInfo - information about the user who is making the change +3. rep - information about where the change is being made +4. documentAttributeManager - information about attributes in the document +5. evt - the fired event + +This hook is provided to allow a plugin to handle key events. +The return value should be true if you have handled the event. + +##collectContentLineText +Called from: src/static/js/contentcollector.js + +Things in context: + +1. cc - the contentcollector object +2. state - the current state of the change being made +3. tname - the tag name of this node currently being processed +4. text - the text for that line + +This hook allows you to validate/manipulate the text before it's sent to the server side. +The return value should be the validated/manipulated text. + +##collectContentLineBreak +Called from: src/static/js/contentcollector.js + +Things in context: + +1. cc - the contentcollector object +2. state - the current state of the change being made +3. tname - the tag name of this node currently being processed + +This hook is provided to allow whether the br tag should induce a new magic domline or not. +The return value should be either true(break the line) or false. + +##disableAuthorColorsForThisLine +Called from: src/static/js/linestylefilter.js + +Things in context: + +1. linestylefilter - the JavaScript object that's currently processing the ace attributes +2. text - the line text +3. class - line class + +This hook is provided to allow whether a given line should be deliniated with multiple authors. +Multiple authors in one line cause the creation of magic span lines. This might not be suit you and +now you can disable it and handle your own deliniation. +The return value should be either true(disable ) or false. diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 06ec7374b..854b43394 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -136,3 +136,16 @@ function handleMessage ( hook, context, callback ) { } }; ``` + + +## getLineHTMLForExport +Called from: src/node/utils/ExportHtml.js + +Things in context: + +1. apool - pool object +2. attribLine - line attributes +3. text - line text + +This hook will allow a plug-in developer to re-write each line when exporting to HTML. + From 61022be6e422e3608bb15cb230f862e0417239e8 Mon Sep 17 00:00:00 2001 From: Gedion Date: Mon, 1 Oct 2012 19:14:27 -0500 Subject: [PATCH 29/32] added comments to ace exposed methods --- doc/api/editorInfo.md | 22 +++++++++++++++++----- doc/api/hooks_client-side.md | 4 ++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/doc/api/editorInfo.md b/doc/api/editorInfo.md index a212ff085..6aec326e2 100644 --- a/doc/api/editorInfo.md +++ b/doc/api/editorInfo.md @@ -45,14 +45,26 @@ Returns the `rep` object. ## editorInfo.ace_doInsertUnorderedList(?) ## editorInfo.ace_doInsertOrderedList(?) ## editorInfo.ace_performDocumentApplyAttributesToRange() + ## editorInfo.ace_getAuthorInfos() -## editorInfo.ace_performDocumentReplaceRange(?) -## editorInfo.ace_performDocumentReplaceCharRange(?) -## editorInfo.ace_renumberList(?) +Returns an info object about the author. Object key = author_id and info includes athour's bg color value. +Use to define your own authorship. +## editorInfo.ace_performDocumentReplaceRange(start, end, newText) +This function replaces a range (from [x1,y1] to [x2,y2]) with `newText`. +## editorInfo.ace_performDocumentReplaceCharRange(startChar, endChar, newText) +This function replaces a range (from y1 to y2) with `newText`. +## editorInfo.ace_renumberList(lineNum) +If you delete a line, calling this method will fix the line numbering. ## editorInfo.ace_doReturnKey() -## editorInfo.ace_isBlockElement(?) -## editorInfo.ace_getLineListType(?) +Forces a return key at the current carret position. +## editorInfo.ace_isBlockElement(element) +Returns true if your passed elment is registered as a block element +## editorInfo.ace_getLineListType(lineNum) +Returns the line's html list type. ## editorInfo.ace_caretLine() +Returns X position of the caret. ## editorInfo.ace_caretColumn() +Returns Y position of the caret. ## editorInfo.ace_caretDocChar() +Returns the Y offset starting from [x=0,y=0] ## editorInfo.ace_isWordChar(?) diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index 707800f5e..55d1da000 100644 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -239,6 +239,6 @@ Things in context: 3. class - line class This hook is provided to allow whether a given line should be deliniated with multiple authors. -Multiple authors in one line cause the creation of magic span lines. This might not be suit you and +Multiple authors in one line cause the creation of magic span lines. This might not suit you and now you can disable it and handle your own deliniation. -The return value should be either true(disable ) or false. +The return value should be either true(disable) or false. From 60099030952e997c9e5bdc9e72da250afb35792c Mon Sep 17 00:00:00 2001 From: Gedion Date: Mon, 1 Oct 2012 19:18:19 -0500 Subject: [PATCH 30/32] added comments to ace exposed methods --- doc/api/editorInfo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/editorInfo.md b/doc/api/editorInfo.md index 6aec326e2..05656bce7 100644 --- a/doc/api/editorInfo.md +++ b/doc/api/editorInfo.md @@ -47,7 +47,7 @@ Returns the `rep` object. ## editorInfo.ace_performDocumentApplyAttributesToRange() ## editorInfo.ace_getAuthorInfos() -Returns an info object about the author. Object key = author_id and info includes athour's bg color value. +Returns an info object about the author. Object key = author_id and info includes author's bg color value. Use to define your own authorship. ## editorInfo.ace_performDocumentReplaceRange(start, end, newText) This function replaces a range (from [x1,y1] to [x2,y2]) with `newText`. From 56453409a5884f2142030c45f9b007ac30a1ebf7 Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 2 Oct 2012 02:19:44 +0200 Subject: [PATCH 31/32] Update src/static/js/pad_editbar.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Somehow was no more.  Now it is back..  Oh boy. --- src/static/js/pad_editbar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 59658a046..24dd3d6f0 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -249,13 +249,13 @@ var padeditbar = (function() { var basePath = document.location.href.substring(0, document.location.href.indexOf("/p/")); var readonlyLink = basePath + "/p/" + clientVars.readOnlyId; - $('#embedinput').val(""); $('#linkinput').val(readonlyLink); } else { var padurl = window.location.href.split("?")[0]; - $('#embedinput').val(""); $('#linkinput').val(padurl); } } From 7656001cb5e7f3024cdf51ca8fbf0f460fdae016 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Tue, 2 Oct 2012 20:11:18 +0200 Subject: [PATCH 32/32] Don't shut down the whole server, if error handling middleware is called. The errors passed to error handling middleware aren't that severe, so it's fine to just stay alive... --- src/node/hooks/express/errorhandling.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index cb8c58987..4f5dad4f6 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -38,7 +38,6 @@ exports.expressCreateServer = function (hook_name, args, cb) { args.app.error(function(err, req, res, next){ res.send(500); console.error(err.stack ? err.stack : err.toString()); - exports.gracefulShutdown(); }); //connect graceful shutdown with sigint and uncaughtexception