diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js
index 4bf46d9a9..6e1e6299d 100644
--- a/src/static/js/ace2_inner.js
+++ b/src/static/js/ace2_inner.js
@@ -20,4143 +20,4114 @@
* limitations under the License.
*/
-define(["ep_etherpad-lite/static/js/rjquery", "underscore", 'ep_etherpad-lite/static/js/pluginfw/hooks'], function ($, _, hooks) {
+define(["ep_etherpad-lite/static/js/rjquery", "underscore", 'ep_etherpad-lite/static/js/pluginfw/hooks', 'ep_etherpad-lite/static/js/linestylefilter', 'ep_etherpad-lite/static/js/domline'], function ($, _, hooks, linestylefilterMod, domlineMod) {
+ var linestylefilter = linestylefilterMod.linestylefilter;
+ var domline = domlineMod.domline;
var exports = {};
-var browser = require('./browser').browser;
-if(browser.msie){
- // Honestly fuck IE royally.
- // Basically every hack we have since V11 causes a problem
- if(parseInt(browser.version) >= 11){
- delete browser.msie;
- browser.chrome = true;
- browser.modernIE = true;
- }
-}
-
-var Ace2Common = require('./ace2_common');
-
-var isNodeText = Ace2Common.isNodeText,
- getAssoc = Ace2Common.getAssoc,
- setAssoc = Ace2Common.setAssoc,
- isTextNode = Ace2Common.isTextNode,
- binarySearchInfinite = Ace2Common.binarySearchInfinite,
- htmlPrettyEscape = Ace2Common.htmlPrettyEscape,
- noop = Ace2Common.noop;
-
-function Ace2Inner(editorInfo){
-
+ var browser = require('./browser').browser;
+ var Ace2Common = require('./ace2_common');
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 ChangesetUtils = require('./ChangesetUtils');
- var linestylefilter = require('./linestylefilter').linestylefilter;
var SkipList = require('./skiplist');
var undoModule = require('./undomodule').undoModule;
var AttributeManager = require('./AttributeManager');
- var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
- // changed to false
- var isSetUp = false;
-
- var THE_TAB = ' '; //4
- var MAX_LIST_LEVEL = 16;
-
- var LINE_NUMBER_PADDING_RIGHT = 4;
- var LINE_NUMBER_PADDING_LEFT = 4;
- var MIN_LINEDIV_WIDTH = 20;
- var EDIT_BODY_PADDING_TOP = 8;
- var EDIT_BODY_PADDING_LEFT = 8;
-
- var caughtErrors = [];
-
- var thisAuthor = '';
-
- var disposed = false;
- var sideDiv = $('#sidediv')[0];
- var lineMetricsDiv = $('#linemetricsdiv')[0];
- var innerdocbody = $('#innerdocbody')[0];
- initLineNumbers();
-
- var outsideKeyDown = noop;
-
- var outsideKeyPress = function(){return true;};
-
- var outsideNotifyDirty = noop;
-
- // selFocusAtStart -- determines whether the selection extends "backwards", so that the focus
- // point (controlled with the arrow keys) is at the beginning; not supported in IE, though
- // native IE selections have that behavior (which we try not to interfere with).
- // Must be false if selection is collapsed!
- var rep = {
- lines: new SkipList(),
- selStart: null,
- selEnd: null,
- selFocusAtStart: false,
- alltext: "",
- alines: [],
- apool: new AttribPool()
- };
-
- // lines, alltext, alines, and DOM are set up in init()
- if (undoModule.enabled)
- {
- undoModule.apool = rep.apool;
+ if(browser.msie){
+ // Honestly fuck IE royally.
+ // Basically every hack we have since V11 causes a problem
+ if(parseInt(browser.version) >= 11){
+ delete browser.msie;
+ browser.chrome = true;
+ browser.modernIE = true;
+ }
}
- var root, doc; // set in init()
- var isEditable = true;
- var doesWrap = true;
- var hasLineNumbers = true;
- var isStyled = true;
+ var isNodeText = Ace2Common.isNodeText,
+ getAssoc = Ace2Common.getAssoc,
+ setAssoc = Ace2Common.setAssoc,
+ isTextNode = Ace2Common.isTextNode,
+ binarySearchInfinite = Ace2Common.binarySearchInfinite,
+ htmlPrettyEscape = Ace2Common.htmlPrettyEscape,
+ noop = Ace2Common.noop;
- // space around the innermost iframe element
- var iframePadLeft = MIN_LINEDIV_WIDTH + LINE_NUMBER_PADDING_RIGHT + EDIT_BODY_PADDING_LEFT;
- var iframePadTop = EDIT_BODY_PADDING_TOP;
- var iframePadBottom = 0,
- iframePadRight = 0;
+ function Ace2Inner(editorInfo){
+ var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
+ // changed to false
+ var isSetUp = false;
- var console = (DEBUG && window.console);
- var documentAttributeManager;
+ var THE_TAB = ' '; //4
+ var MAX_LIST_LEVEL = 16;
- if (!window.console)
- {
- var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"];
- console = {};
- for (var i = 0; i < names.length; ++i)
- console[names[i]] = noop;
- //console.error = function(str) { alert(str); };
- }
+ var LINE_NUMBER_PADDING_RIGHT = 4;
+ var LINE_NUMBER_PADDING_LEFT = 4;
+ var MIN_LINEDIV_WIDTH = 20;
+ var EDIT_BODY_PADDING_TOP = 8;
+ var EDIT_BODY_PADDING_LEFT = 8;
- var PROFILER = window.PROFILER;
- if (!PROFILER)
- {
- PROFILER = function()
- {
- return {
- start: noop,
- mark: noop,
- literal: noop,
- end: noop,
- cancel: noop
- };
+ var caughtErrors = [];
+
+ var thisAuthor = '';
+
+ var disposed = false;
+ var sideDiv = $('#sidediv')[0];
+ var lineMetricsDiv = $('#linemetricsdiv')[0];
+ var innerdocbody = $('#innerdocbody')[0];
+ initLineNumbers();
+
+ var outsideKeyDown = noop;
+
+ var outsideKeyPress = function(){return true;};
+
+ var outsideNotifyDirty = noop;
+
+ // selFocusAtStart -- determines whether the selection extends "backwards", so that the focus
+ // point (controlled with the arrow keys) is at the beginning; not supported in IE, though
+ // native IE selections have that behavior (which we try not to interfere with).
+ // Must be false if selection is collapsed!
+ var rep = {
+ lines: new SkipList(),
+ selStart: null,
+ selEnd: null,
+ selFocusAtStart: false,
+ alltext: "",
+ alines: [],
+ apool: new AttribPool()
};
- }
- // "dmesg" is for displaying messages in the in-page output pane
- // visible when "?djs=1" is appended to the pad URL. It generally
- // remains a no-op unless djs is enabled, but we make a habit of
- // only calling it in error cases or while debugging.
- var dmesg = noop;
- window.dmesg = noop;
-
- var scheduler = parent; // hack for opera required
-
- var textFace = 'monospace';
- var textSize = 12;
-
-
- function textLineHeight()
- {
- return Math.round(textSize * 4 / 3);
- }
-
- var dynamicCSS = null;
- var outerDynamicCSS = null;
- var parentDynamicCSS = null;
-
- function initDynamicCSS()
- {
- dynamicCSS = makeCSSManager("dynamicsyntax");
- outerDynamicCSS = makeCSSManager("dynamicsyntax", "outer");
- parentDynamicCSS = makeCSSManager("dynamicsyntax", "parent");
- }
-
- var changesetTracker = makeChangesetTracker(scheduler, rep.apool, {
- withCallbacks: function(operationName, f)
+ // lines, alltext, alines, and DOM are set up in init()
+ if (undoModule.enabled)
{
- inCallStackIfNecessary(operationName, function()
+ undoModule.apool = rep.apool;
+ }
+
+ var root, doc; // set in init()
+ var isEditable = true;
+ var doesWrap = true;
+ var hasLineNumbers = true;
+ var isStyled = true;
+
+ // space around the innermost iframe element
+ var iframePadLeft = MIN_LINEDIV_WIDTH + LINE_NUMBER_PADDING_RIGHT + EDIT_BODY_PADDING_LEFT;
+ var iframePadTop = EDIT_BODY_PADDING_TOP;
+ var iframePadBottom = 0,
+ iframePadRight = 0;
+
+ var console = (DEBUG && window.console);
+ var documentAttributeManager;
+
+ if (!window.console)
+ {
+ var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"];
+ console = {};
+ for (var i = 0; i < names.length; ++i)
+ console[names[i]] = noop;
+ //console.error = function(str) { alert(str); };
+ }
+
+ var PROFILER = window.PROFILER;
+ if (!PROFILER)
+ {
+ PROFILER = function()
{
- fastIncorp(1);
- f(
+ return {
+ start: noop,
+ mark: noop,
+ literal: noop,
+ end: noop,
+ cancel: noop
+ };
+ };
+ }
+
+ // "dmesg" is for displaying messages in the in-page output pane
+ // visible when "?djs=1" is appended to the pad URL. It generally
+ // remains a no-op unless djs is enabled, but we make a habit of
+ // only calling it in error cases or while debugging.
+ var dmesg = noop;
+ window.dmesg = noop;
+
+ var scheduler = parent; // hack for opera required
+
+ var textFace = 'monospace';
+ var textSize = 12;
+
+
+ function textLineHeight()
+ {
+ return Math.round(textSize * 4 / 3);
+ }
+
+ var dynamicCSS = null;
+ var outerDynamicCSS = null;
+ var parentDynamicCSS = null;
+
+ function initDynamicCSS()
+ {
+ dynamicCSS = makeCSSManager("dynamicsyntax");
+ outerDynamicCSS = makeCSSManager("dynamicsyntax", "outer");
+ parentDynamicCSS = makeCSSManager("dynamicsyntax", "parent");
+ }
+
+ var changesetTracker = makeChangesetTracker(scheduler, rep.apool, {
+ withCallbacks: function(operationName, f)
+ {
+ inCallStackIfNecessary(operationName, function()
{
- setDocumentAttributedText: function(atext)
+ fastIncorp(1);
+ f(
{
- setDocAText(atext);
- },
- applyChangesetToDocument: function(changeset, preferInsertionAfterCaret)
- {
- var oldEventType = currentCallStack.editEvent.eventType;
- currentCallStack.startNewEvent("nonundoable");
+ setDocumentAttributedText: function(atext)
+ {
+ setDocAText(atext);
+ },
+ applyChangesetToDocument: function(changeset, preferInsertionAfterCaret)
+ {
+ var oldEventType = currentCallStack.editEvent.eventType;
+ currentCallStack.startNewEvent("nonundoable");
- performDocumentApplyChangeset(changeset, preferInsertionAfterCaret);
+ performDocumentApplyChangeset(changeset, preferInsertionAfterCaret);
- currentCallStack.startNewEvent(oldEventType);
- }
+ currentCallStack.startNewEvent(oldEventType);
+ }
+ });
});
- });
- }
- });
-
- var authorInfos = {}; // presence of key determines if author is present in doc
-
- function getAuthorInfos(){
- return authorInfos;
- };
- editorInfo.ace_getAuthorInfos= getAuthorInfos;
-
- function setAuthorStyle(author, info)
- {
- if (!dynamicCSS) {
- return;
- }
- var authorSelector = getAuthorColorClassSelector(getAuthorClassName(author));
-
- var authorStyleSet = hooks.callAll('aceSetAuthorStyle', {
- dynamicCSS: dynamicCSS,
- parentDynamicCSS: parentDynamicCSS,
- outerDynamicCSS: outerDynamicCSS,
- info: info,
- author: author,
- authorSelector: authorSelector,
+ }
});
- // Prevent default behaviour if any hook says so
- if (_.any(authorStyleSet, function(it) { return it }))
- {
- return
- }
+ var authorInfos = {}; // presence of key determines if author is present in doc
- if (!info)
+ function getAuthorInfos(){
+ return authorInfos;
+ };
+ editorInfo.ace_getAuthorInfos= getAuthorInfos;
+
+ function setAuthorStyle(author, info)
{
- dynamicCSS.removeSelectorStyle(authorSelector);
- parentDynamicCSS.removeSelectorStyle(authorSelector);
- }
- else
- {
- if (info.bgcolor)
+ if (!dynamicCSS) {
+ return;
+ }
+ var authorSelector = getAuthorColorClassSelector(getAuthorClassName(author));
+
+ var authorStyleSet = hooks.callAll('aceSetAuthorStyle', {
+ dynamicCSS: dynamicCSS,
+ parentDynamicCSS: parentDynamicCSS,
+ outerDynamicCSS: outerDynamicCSS,
+ info: info,
+ author: author,
+ authorSelector: authorSelector,
+ });
+
+ // Prevent default behaviour if any hook says so
+ if (_.any(authorStyleSet, function(it) { return it }))
{
- var bgcolor = info.bgcolor;
- if ((typeof info.fade) == "number")
+ return
+ }
+
+ if (!info)
+ {
+ dynamicCSS.removeSelectorStyle(authorSelector);
+ parentDynamicCSS.removeSelectorStyle(authorSelector);
+ }
+ else
+ {
+ if (info.bgcolor)
{
- bgcolor = fadeColor(bgcolor, info.fade);
- }
+ var bgcolor = info.bgcolor;
+ if ((typeof info.fade) == "number")
+ {
+ bgcolor = fadeColor(bgcolor, info.fade);
+ }
- var authorStyle = dynamicCSS.selectorStyle(authorSelector);
- var parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector);
- var anchorStyle = dynamicCSS.selectorStyle(authorSelector + ' > a')
+ var authorStyle = dynamicCSS.selectorStyle(authorSelector);
+ var parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector);
+ var anchorStyle = dynamicCSS.selectorStyle(authorSelector + ' > a')
- // author color
- authorStyle.backgroundColor = bgcolor;
- parentAuthorStyle.backgroundColor = bgcolor;
+ // author color
+ authorStyle.backgroundColor = bgcolor;
+ parentAuthorStyle.backgroundColor = bgcolor;
- // text contrast
- if(colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5)
- {
- authorStyle.color = '#ffffff';
- parentAuthorStyle.color = '#ffffff';
- }else{
- authorStyle.color = null;
- parentAuthorStyle.color = null;
- }
+ // text contrast
+ if(colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5)
+ {
+ authorStyle.color = '#ffffff';
+ parentAuthorStyle.color = '#ffffff';
+ }else{
+ authorStyle.color = null;
+ parentAuthorStyle.color = null;
+ }
- // anchor text contrast
- if(colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.55)
- {
- anchorStyle.color = colorutils.triple2css(colorutils.complementary(colorutils.css2triple(bgcolor)));
- }else{
- anchorStyle.color = null;
+ // anchor text contrast
+ if(colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.55)
+ {
+ anchorStyle.color = colorutils.triple2css(colorutils.complementary(colorutils.css2triple(bgcolor)));
+ }else{
+ anchorStyle.color = null;
+ }
}
}
}
- }
- function setAuthorInfo(author, info)
- {
- if ((typeof author) != "string")
+ function setAuthorInfo(author, info)
{
- throw new Error("setAuthorInfo: author (" + author + ") is not a string");
- }
- if (!info)
- {
- delete authorInfos[author];
- }
- else
- {
- authorInfos[author] = info;
- }
- setAuthorStyle(author, info);
- }
-
- function getAuthorClassName(author)
- {
- return "author-" + author.replace(/[^a-y0-9]/g, function(c)
- {
- if (c == ".") return "-";
- return 'z' + c.charCodeAt(0) + 'z';
- });
- }
-
- function className2Author(className)
- {
- if (className.substring(0, 7) == "author-")
- {
- return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, function(cc)
+ if ((typeof author) != "string")
{
- if (cc == '-') return '.';
- else if (cc.charAt(0) == 'z')
+ throw new Error("setAuthorInfo: author (" + author + ") is not a string");
+ }
+ if (!info)
+ {
+ delete authorInfos[author];
+ }
+ else
+ {
+ authorInfos[author] = info;
+ }
+ setAuthorStyle(author, info);
+ }
+
+ function getAuthorClassName(author)
+ {
+ return "author-" + author.replace(/[^a-y0-9]/g, function(c)
+ {
+ if (c == ".") return "-";
+ return 'z' + c.charCodeAt(0) + 'z';
+ });
+ }
+
+ function className2Author(className)
+ {
+ if (className.substring(0, 7) == "author-")
+ {
+ return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, function(cc)
{
- return String.fromCharCode(Number(cc.slice(1, -1)));
+ if (cc == '-') return '.';
+ else if (cc.charAt(0) == 'z')
+ {
+ return String.fromCharCode(Number(cc.slice(1, -1)));
+ }
+ else
+ {
+ return cc;
+ }
+ });
+ }
+ return null;
+ }
+
+ function getAuthorColorClassSelector(oneClassName)
+ {
+ return ".authorColors ." + oneClassName;
+ }
+
+ function setUpTrackingCSS()
+ {
+ if (dynamicCSS)
+ {
+ var backgroundHeight = lineMetricsDiv.offsetHeight;
+ var lineHeight = textLineHeight();
+ var extraBodding = 0;
+ var extraTodding = 0;
+ if (backgroundHeight < lineHeight)
+ {
+ extraBodding = Math.ceil((lineHeight - backgroundHeight) / 2);
+ extraTodding = lineHeight - backgroundHeight - extraBodding;
+ }
+ var spanStyle = dynamicCSS.selectorStyle("#innerdocbody span");
+ spanStyle.paddingTop = extraTodding + "px";
+ spanStyle.paddingBottom = extraBodding + "px";
+ }
+ }
+
+ function fadeColor(colorCSS, fadeFrac)
+ {
+ var color = colorutils.css2triple(colorCSS);
+ color = colorutils.blend(color, [1, 1, 1], fadeFrac);
+ return colorutils.triple2css(color);
+ }
+
+ editorInfo.ace_getRep = function()
+ {
+ return rep;
+ };
+
+ editorInfo.ace_getAuthor = function()
+ {
+ return thisAuthor;
+ }
+
+ var currentCallStack = null;
+
+ function inCallStack(type, action)
+ {
+ if (disposed) return;
+
+ if (currentCallStack)
+ {
+ console.log("Can't enter callstack " + type + ", already in " + currentCallStack.type);
+ }
+
+ var profiling = false;
+
+ function profileRest()
+ {
+ profiling = true;
+ console.profile();
+ }
+
+ function newEditEvent(eventType)
+ {
+ return {
+ eventType: eventType,
+ backset: null
+ };
+ }
+
+ function submitOldEvent(evt)
+ {
+ if (rep.selStart && rep.selEnd)
+ {
+ var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
+ var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
+ evt.selStart = selStartChar;
+ evt.selEnd = selEndChar;
+ evt.selFocusAtStart = rep.selFocusAtStart;
+ }
+ if (undoModule.enabled)
+ {
+ var undoWorked = false;
+ try
+ {
+ if (evt.eventType == "setup" || evt.eventType == "importText" || evt.eventType == "setBaseText")
+ {
+ undoModule.clearHistory();
+ }
+ else if (evt.eventType == "nonundoable")
+ {
+ if (evt.changeset)
+ {
+ undoModule.reportExternalChange(evt.changeset);
+ }
+ }
+ else
+ {
+ undoModule.reportEvent(evt);
+ }
+ undoWorked = true;
+ }
+ finally
+ {
+ if (!undoWorked)
+ {
+ undoModule.enabled = false; // for safety
+ }
+ }
+ }
+ }
+
+ function startNewEvent(eventType, dontSubmitOld)
+ {
+ var oldEvent = currentCallStack.editEvent;
+ if (!dontSubmitOld)
+ {
+ submitOldEvent(oldEvent);
+ }
+ currentCallStack.editEvent = newEditEvent(eventType);
+ return oldEvent;
+ }
+
+ currentCallStack = {
+ type: type,
+ docTextChanged: false,
+ selectionAffected: false,
+ userChangedSelection: false,
+ domClean: false,
+ profileRest: profileRest,
+ isUserChange: false,
+ // is this a "user change" type of call-stack
+ repChanged: false,
+ editEvent: newEditEvent(type),
+ startNewEvent: startNewEvent
+ };
+ var cleanExit = false;
+ var result;
+ try
+ {
+ result = action();
+
+ hooks.callAll('aceEditEvent', {
+ callstack: currentCallStack,
+ editorInfo: editorInfo,
+ rep: rep,
+ documentAttributeManager: documentAttributeManager
+ });
+
+ //console.log("Just did action for: "+type);
+ cleanExit = true;
+ }
+ /*
+ catch (e)
+ {
+ caughtErrors.push(
+ {
+ error: e,
+ time: +new Date()
+ });
+ dmesg(e.toString());
+ throw e;
+ }
+ */
+ finally
+ {
+ var cs = currentCallStack;
+ //console.log("Finished action for: "+type);
+ if (cleanExit)
+ {
+ submitOldEvent(cs.editEvent);
+ if (cs.domClean && cs.type != "setup")
+ {
+ // if (cs.isUserChange)
+ // {
+ // if (cs.repChanged) parenModule.notifyChange();
+ // else parenModule.notifyTick();
+ // }
+ if (cs.selectionAffected)
+ {
+ updateBrowserSelectionFromRep();
+ }
+ if ((cs.docTextChanged || cs.userChangedSelection) && cs.type != "applyChangesToBase")
+ {
+ scrollSelectionIntoView();
+ }
+ if (cs.docTextChanged && cs.type.indexOf("importText") < 0)
+ {
+ outsideNotifyDirty();
+ }
+ }
}
else
{
- return cc;
+ // non-clean exit
+ if (currentCallStack.type == "idleWorkTimer")
+ {
+ idleWorkTimer.atLeast(1000);
+ }
}
+ currentCallStack = null;
+ if (profiling) console.profileEnd();
+ }
+ return result;
+ }
+ editorInfo.ace_inCallStack = inCallStack;
+
+ function inCallStackIfNecessary(type, action)
+ {
+ if (!currentCallStack)
+ {
+ inCallStack(type, action);
+ }
+ else
+ {
+ action();
+ }
+ }
+ editorInfo.ace_inCallStackIfNecessary = inCallStackIfNecessary;
+
+ function dispose()
+ {
+ disposed = true;
+ if (idleWorkTimer) idleWorkTimer.never();
+ teardown();
+ }
+
+ function checkALines()
+ {
+ return; // disable for speed
+
+
+ function error()
+ {
+ throw new Error("checkALines");
+ }
+ if (rep.alines.length != rep.lines.length())
+ {
+ error();
+ }
+ for (var i = 0; i < rep.alines.length; i++)
+ {
+ var aline = rep.alines[i];
+ var lineText = rep.lines.atIndex(i).text + "\n";
+ var lineTextLength = lineText.length;
+ var opIter = Changeset.opIterator(aline);
+ var alineLength = 0;
+ while (opIter.hasNext())
+ {
+ var o = opIter.next();
+ alineLength += o.chars;
+ if (opIter.hasNext())
+ {
+ if (o.lines !== 0) error();
+ }
+ else
+ {
+ if (o.lines != 1) error();
+ }
+ }
+ if (alineLength != lineTextLength)
+ {
+ error();
+ }
+ }
+ }
+
+ function setWraps(newVal)
+ {
+ doesWrap = newVal;
+ var dwClass = "doesWrap";
+ setClassPresence(root, "doesWrap", doesWrap);
+ scheduler.setTimeout(function()
+ {
+ inCallStackIfNecessary("setWraps", function()
+ {
+ fastIncorp(7);
+ recreateDOM();
+ fixView();
+ });
+ }, 0);
+
+ // Chrome can't handle the truth.. If CSS rule white-space:pre-wrap
+ // is true then any paste event will insert two lines..
+ // Sadly this will mean you get a walking Caret in Chrome when clicking on a URL
+ // So this has to be set to pre-wrap ;(
+ // We need to file a bug w/ the Chromium team.
+ if(browser.chrome){
+ $("#innerdocbody").css({"white-space":"pre-wrap"});
+ }
+
+ }
+
+ function setStyled(newVal)
+ {
+ var oldVal = isStyled;
+ isStyled = !! newVal;
+
+ if (newVal != oldVal)
+ {
+ if (!newVal)
+ {
+ // clear styles
+ inCallStackIfNecessary("setStyled", function()
+ {
+ fastIncorp(12);
+ var clearStyles = [];
+ for (var k in STYLE_ATTRIBS)
+ {
+ clearStyles.push([k, '']);
+ }
+ performDocumentApplyAttributesToCharRange(0, rep.alltext.length, clearStyles);
+ });
+ }
+ }
+ }
+
+ function setTextFace(face)
+ {
+ textFace = face;
+ root.style.fontFamily = textFace;
+ lineMetricsDiv.style.fontFamily = textFace;
+ scheduler.setTimeout(function()
+ {
+ setUpTrackingCSS();
+ }, 0);
+ }
+
+ function setTextSize(size)
+ {
+ textSize = size;
+ root.style.fontSize = textSize + "px";
+ root.style.lineHeight = textLineHeight() + "px";
+ sideDiv.style.lineHeight = textLineHeight() + "px";
+ lineMetricsDiv.style.fontSize = textSize + "px";
+ scheduler.setTimeout(function()
+ {
+ setUpTrackingCSS();
+ }, 0);
+ }
+
+ function recreateDOM()
+ {
+ // precond: normalized
+ recolorLinesInRange(0, rep.alltext.length);
+ }
+
+ function setEditable(newVal)
+ {
+ isEditable = newVal;
+
+ // the following may fail, e.g. if iframe is hidden
+ if (!isEditable)
+ {
+ setDesignMode(false);
+ }
+ else
+ {
+ setDesignMode(true);
+ }
+ setClassPresence(root, "static", !isEditable);
+ }
+
+ function enforceEditability()
+ {
+ setEditable(isEditable);
+ }
+
+ function importText(text, undoable, dontProcess)
+ {
+ var lines;
+ if (dontProcess)
+ {
+ if (text.charAt(text.length - 1) != "\n")
+ {
+ throw new Error("new raw text must end with newline");
+ }
+ if (/[\r\t\xa0]/.exec(text))
+ {
+ throw new Error("new raw text must not contain CR, tab, or nbsp");
+ }
+ lines = text.substring(0, text.length - 1).split('\n');
+ }
+ else
+ {
+ lines = _.map(text.split('\n'), textify);
+ }
+ var newText = "\n";
+ if (lines.length > 0)
+ {
+ newText = lines.join('\n') + '\n';
+ }
+
+ inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function()
+ {
+ setDocText(newText);
+ });
+
+ if (dontProcess && rep.alltext != text)
+ {
+ throw new Error("mismatch error setting raw text in importText");
+ }
+ }
+
+ function importAText(atext, apoolJsonObj, undoable)
+ {
+ atext = Changeset.cloneAText(atext);
+ if (apoolJsonObj)
+ {
+ var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
+ atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
+ }
+ inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function()
+ {
+ setDocAText(atext);
});
}
- return null;
- }
- function getAuthorColorClassSelector(oneClassName)
- {
- return ".authorColors ." + oneClassName;
- }
-
- function setUpTrackingCSS()
- {
- if (dynamicCSS)
+ function setDocAText(atext)
{
- var backgroundHeight = lineMetricsDiv.offsetHeight;
- var lineHeight = textLineHeight();
- var extraBodding = 0;
- var extraTodding = 0;
- if (backgroundHeight < lineHeight)
+ fastIncorp(8);
+
+ var oldLen = rep.lines.totalWidth();
+ var numLines = rep.lines.length();
+ var upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
+ var lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
+ var assem = Changeset.smartOpAssembler();
+ var o = Changeset.newOp('-');
+ o.chars = upToLastLine;
+ o.lines = numLines - 1;
+ assem.append(o);
+ o.chars = lastLineLength;
+ o.lines = 0;
+ assem.append(o);
+ Changeset.appendATextToAssembler(atext, assem);
+ var newLen = oldLen + assem.getLengthChange();
+ var changeset = Changeset.checkRep(
+ Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
+ performDocumentApplyChangeset(changeset);
+
+ performSelectionChange([0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]);
+
+ idleWorkTimer.atMost(100);
+
+ if (rep.alltext != atext.text)
{
- extraBodding = Math.ceil((lineHeight - backgroundHeight) / 2);
- extraTodding = lineHeight - backgroundHeight - extraBodding;
+ dmesg(htmlPrettyEscape(rep.alltext));
+ dmesg(htmlPrettyEscape(atext.text));
+ throw new Error("mismatch error setting raw text in setDocAText");
}
- var spanStyle = dynamicCSS.selectorStyle("#innerdocbody span");
- spanStyle.paddingTop = extraTodding + "px";
- spanStyle.paddingBottom = extraBodding + "px";
- }
- }
-
- function fadeColor(colorCSS, fadeFrac)
- {
- var color = colorutils.css2triple(colorCSS);
- color = colorutils.blend(color, [1, 1, 1], fadeFrac);
- return colorutils.triple2css(color);
- }
-
- editorInfo.ace_getRep = function()
- {
- return rep;
- };
-
- editorInfo.ace_getAuthor = function()
- {
- return thisAuthor;
- }
-
- var currentCallStack = null;
-
- function inCallStack(type, action)
- {
- if (disposed) return;
-
- if (currentCallStack)
- {
- console.log("Can't enter callstack " + type + ", already in " + currentCallStack.type);
}
- var profiling = false;
-
- function profileRest()
+ function setDocText(text)
{
- profiling = true;
- console.profile();
+ setDocAText(Changeset.makeAText(text));
}
- function newEditEvent(eventType)
+ function getDocText()
{
+ var alltext = rep.alltext;
+ var len = alltext.length;
+ if (len > 0) len--; // final extra newline
+ return alltext.substring(0, len);
+ }
+
+ function exportText()
+ {
+ if (currentCallStack && !currentCallStack.domClean)
+ {
+ inCallStackIfNecessary("exportText", function()
+ {
+ fastIncorp(2);
+ });
+ }
+ return getDocText();
+ }
+
+ function editorChangedSize()
+ {
+ fixView();
+ }
+
+ function setOnKeyPress(handler)
+ {
+ outsideKeyPress = handler;
+ }
+
+ function setOnKeyDown(handler)
+ {
+ outsideKeyDown = handler;
+ }
+
+ function setNotifyDirty(handler)
+ {
+ outsideNotifyDirty = handler;
+ }
+
+ function getFormattedCode()
+ {
+ if (currentCallStack && !currentCallStack.domClean)
+ {
+ inCallStackIfNecessary("getFormattedCode", incorporateUserChanges);
+ }
+ var buf = [];
+ if (rep.lines.length() > 0)
+ {
+ // should be the case, even for empty file
+ var entry = rep.lines.atIndex(0);
+ while (entry)
+ {
+ var domInfo = entry.domInfo;
+ buf.push((domInfo && domInfo.getInnerHTML()) || domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || ' ' /*empty line*/ );
+ entry = rep.lines.next(entry);
+ }
+ }
+ return '
';
+ }
+
+ var CMDS = {
+ clearauthorship: function(prompt)
+ {
+ if ((!(rep.selStart && rep.selEnd)) || isCaret())
+ {
+ if (prompt)
+ {
+ prompt();
+ }
+ else
+ {
+ performDocumentApplyAttributesToCharRange(0, rep.alltext.length, [
+ ['author', '']
+ ]);
+ }
+ }
+ else
+ {
+ setAttributeOnSelection('author', '');
+ }
+ }
+ };
+
+ function execCommand(cmd)
+ {
+ cmd = cmd.toLowerCase();
+ var cmdArgs = Array.prototype.slice.call(arguments, 1);
+ if (CMDS[cmd])
+ {
+ inCallStackIfNecessary(cmd, function()
+ {
+ fastIncorp(9);
+ CMDS[cmd].apply(CMDS, cmdArgs);
+ });
+ }
+ }
+
+ function replaceRange(start, end, text)
+ {
+ inCallStackIfNecessary('replaceRange', function()
+ {
+ fastIncorp(9);
+ performDocumentReplaceRange(start, end, text);
+ });
+ }
+
+ editorInfo.ace_focus = focus;
+ editorInfo.ace_importText = importText;
+ editorInfo.ace_importAText = importAText;
+ editorInfo.ace_exportText = exportText;
+ editorInfo.ace_editorChangedSize = editorChangedSize;
+ editorInfo.ace_setOnKeyPress = setOnKeyPress;
+ editorInfo.ace_setOnKeyDown = setOnKeyDown;
+ editorInfo.ace_setNotifyDirty = setNotifyDirty;
+ editorInfo.ace_dispose = dispose;
+ editorInfo.ace_getFormattedCode = getFormattedCode;
+ 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()
+ {
+ return fn(editorInfo);
+ };
+
+ if (normalize !== undefined)
+ {
+ var wrapper1 = wrapper;
+ wrapper = function()
+ {
+ editorInfo.ace_fastIncorp(9);
+ wrapper1();
+ };
+ }
+
+ if (callStack !== undefined)
+ {
+ return editorInfo.ace_inCallStack(callStack, wrapper);
+ }
+ else
+ {
+ return wrapper();
+ }
+ };
+
+ // This methed exposes a setter for some ace properties
+ // @param key the name of the parameter
+ // @param value the value to set to
+ editorInfo.ace_setProperty = function(key, value)
+ {
+
+ // Convinience function returning a setter for a class on an element
+ var setClassPresenceNamed = function(element, cls){
+ return function(value){
+ setClassPresence(element, cls, !! value)
+ }
+ };
+
+ // These properties are exposed
+ var setters = {
+ wraps: setWraps,
+ showsauthorcolors: setClassPresenceNamed(root, "authorColors"),
+ showsuserselections: setClassPresenceNamed(root, "userSelections"),
+ showslinenumbers : function(value){
+ hasLineNumbers = !! value;
+ // disable line numbers on mobile devices
+ if (browser.mobile) hasLineNumbers = false;
+ setClassPresence(sideDiv, "sidedivhidden", !hasLineNumbers);
+ fixView();
+ },
+ grayedout: setClassPresenceNamed(window.document.body, "grayedout"),
+ dmesg: function(){ dmesg = window.dmesg = value; },
+ userauthor: function(value){
+ thisAuthor = String(value);
+ documentAttributeManager.author = thisAuthor;
+ },
+ styled: setStyled,
+ textface: setTextFace,
+ textsize: setTextSize,
+ rtlistrue: function(value) {
+ setClassPresence(root, "rtl", value)
+ setClassPresence(root, "ltr", !value)
+ document.documentElement.dir = value? 'rtl' : 'ltr'
+ }
+ };
+
+ var setter = setters[key.toLowerCase()];
+
+ // check if setter is present
+ if(setter !== undefined){
+ setter(value)
+ }
+ };
+
+ editorInfo.ace_setBaseText = function(txt)
+ {
+ changesetTracker.setBaseText(txt);
+ };
+ editorInfo.ace_setBaseAttributedText = function(atxt, apoolJsonObj)
+ {
+ setUpTrackingCSS();
+ changesetTracker.setBaseAttributedText(atxt, apoolJsonObj);
+ };
+ editorInfo.ace_applyChangesToBase = function(c, optAuthor, apoolJsonObj)
+ {
+ changesetTracker.applyChangesToBase(c, optAuthor, apoolJsonObj);
+ };
+ editorInfo.ace_prepareUserChangeset = function()
+ {
+ return changesetTracker.prepareUserChangeset();
+ };
+ editorInfo.ace_applyPreparedChangesetToBase = function()
+ {
+ changesetTracker.applyPreparedChangesetToBase();
+ };
+ editorInfo.ace_setUserChangeNotificationCallback = function(f)
+ {
+ changesetTracker.setUserChangeNotificationCallback(f);
+ };
+ editorInfo.ace_setAuthorInfo = function(author, info)
+ {
+ setAuthorInfo(author, info);
+ };
+ editorInfo.ace_setAuthorSelectionRange = function(author, start, end)
+ {
+ changesetTracker.setAuthorSelectionRange(author, start, end);
+ };
+
+ editorInfo.ace_getUnhandledErrors = function()
+ {
+ return caughtErrors.slice();
+ };
+
+ editorInfo.ace_getDocument = function()
+ {
+ return doc;
+ };
+
+ editorInfo.ace_getDebugProperty = function(prop)
+ {
+ if (prop == "debugger")
+ {
+ // obfuscate "eval" so as not to scare yuicompressor
+ window['ev' + 'al']("debugger");
+ }
+ else if (prop == "rep")
+ {
+ return rep;
+ }
+ else if (prop == "window")
+ {
+ return window;
+ }
+ else if (prop == "document")
+ {
+ return document;
+ }
+ return undefined;
+ };
+
+ function now()
+ {
+ return (new Date()).getTime();
+ }
+
+ function newTimeLimit(ms)
+ {
+ //console.debug("new time limit");
+ var startTime = now();
+ var lastElapsed = 0;
+ var exceededAlready = false;
+ var printedTrace = false;
+ var isTimeUp = function()
+ {
+ if (exceededAlready)
+ {
+ if ((!printedTrace))
+ { // && now() - startTime - ms > 300) {
+ //console.trace();
+ printedTrace = true;
+ }
+ return true;
+ }
+ var elapsed = now() - startTime;
+ if (elapsed > ms)
+ {
+ exceededAlready = true;
+ //console.debug("time limit hit, before was %d/%d", lastElapsed, ms);
+ //console.trace();
+ return true;
+ }
+ else
+ {
+ lastElapsed = elapsed;
+ return false;
+ }
+ };
+
+ isTimeUp.elapsed = function()
+ {
+ return now() - startTime;
+ };
+ return isTimeUp;
+ }
+
+
+ function makeIdleAction(func)
+ {
+ var scheduledTimeout = null;
+ var scheduledTime = 0;
+
+ function unschedule()
+ {
+ if (scheduledTimeout)
+ {
+ scheduler.clearTimeout(scheduledTimeout);
+ scheduledTimeout = null;
+ }
+ }
+
+ function reschedule(time)
+ {
+ unschedule();
+ scheduledTime = time;
+ var delay = time - now();
+ if (delay < 0) delay = 0;
+ scheduledTimeout = scheduler.setTimeout(callback, delay);
+ }
+
+ function callback()
+ {
+ scheduledTimeout = null;
+ // func may reschedule the action
+ func();
+ }
return {
- eventType: eventType,
- backset: null
+ atMost: function(ms)
+ {
+ var latestTime = now() + ms;
+ if ((!scheduledTimeout) || scheduledTime > latestTime)
+ {
+ reschedule(latestTime);
+ }
+ },
+ // atLeast(ms) will schedule the action if not scheduled yet.
+ // In other words, "infinity" is replaced by ms, even though
+ // it is technically larger.
+ atLeast: function(ms)
+ {
+ var earliestTime = now() + ms;
+ if ((!scheduledTimeout) || scheduledTime < earliestTime)
+ {
+ reschedule(earliestTime);
+ }
+ },
+ never: function()
+ {
+ unschedule();
+ }
};
}
- function submitOldEvent(evt)
+ function fastIncorp(n)
{
+ // normalize but don't do any lexing or anything
+ incorporateUserChanges(newTimeLimit(0));
+ }
+ editorInfo.ace_fastIncorp = fastIncorp;
+
+ var idleWorkTimer = makeIdleAction(function()
+ {
+
+ //if (! top.BEFORE) top.BEFORE = [];
+ //top.BEFORE.push(magicdom.root.dom.innerHTML);
+ //if (! isEditable) return; // and don't reschedule
+ if (inInternationalComposition)
+ {
+ // don't do idle input incorporation during international input composition
+ idleWorkTimer.atLeast(500);
+ return;
+ }
+
+ inCallStackIfNecessary("idleWorkTimer", function()
+ {
+
+ var isTimeUp = newTimeLimit(250);
+
+ //console.time("idlework");
+ var finishedImportantWork = false;
+ var finishedWork = false;
+
+ try
+ {
+
+ // isTimeUp() is a soft constraint for incorporateUserChanges,
+ // which always renormalizes the DOM, no matter how long it takes,
+ // but doesn't necessarily lex and highlight it
+ incorporateUserChanges(isTimeUp);
+
+ if (isTimeUp()) return;
+
+ updateLineNumbers(); // update line numbers if any time left
+ if (isTimeUp()) return;
+
+ var visibleRange = getVisibleCharRange();
+ var docRange = [0, rep.lines.totalWidth()];
+ //console.log("%o %o", docRange, visibleRange);
+ finishedImportantWork = true;
+ finishedWork = true;
+ }
+ finally
+ {
+ //console.timeEnd("idlework");
+ if (finishedWork)
+ {
+ idleWorkTimer.atMost(1000);
+ }
+ else if (finishedImportantWork)
+ {
+ // if we've finished highlighting the view area,
+ // more highlighting could be counter-productive,
+ // e.g. if the user just opened a triple-quote and will soon close it.
+ idleWorkTimer.atMost(500);
+ }
+ else
+ {
+ var timeToWait = Math.round(isTimeUp.elapsed() / 2);
+ if (timeToWait < 100) timeToWait = 100;
+ idleWorkTimer.atMost(timeToWait);
+ }
+ }
+ });
+
+ //if (! top.AFTER) top.AFTER = [];
+ //top.AFTER.push(magicdom.root.dom.innerHTML);
+ });
+
+ var _nextId = 1;
+
+ function uniqueId(n)
+ {
+ // not actually guaranteed to be unique, e.g. if user copy-pastes
+ // nodes with ids
+ var nid = n.id;
+ if (nid) return nid;
+ return (n.id = "magicdomid" + (_nextId++));
+ }
+
+
+ function recolorLinesInRange(startChar, endChar, isTimeUp, optModFunc)
+ {
+ if (endChar <= startChar) return;
+ if (startChar < 0 || startChar >= rep.lines.totalWidth()) return;
+ var lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary
+ var lineStart = rep.lines.offsetOfEntry(lineEntry);
+ var lineIndex = rep.lines.indexOfEntry(lineEntry);
+ var selectionNeedsResetting = false;
+ var firstLine = null;
+ var lastLine = null;
+ isTimeUp = (isTimeUp || noop);
+
+ // tokenFunc function; accesses current value of lineEntry and curDocChar,
+ // also mutates curDocChar
+ var curDocChar;
+ var tokenFunc = function(tokenText, tokenClass)
+ {
+ lineEntry.domInfo.appendSpan(tokenText, tokenClass);
+ };
+ if (optModFunc)
+ {
+ var f = tokenFunc;
+ tokenFunc = function(tokenText, tokenClass)
+ {
+ optModFunc(tokenText, tokenClass, f, curDocChar);
+ curDocChar += tokenText.length;
+ };
+ }
+
+ while (lineEntry && lineStart < endChar && !isTimeUp())
+ {
+ //var timer = newTimeLimit(200);
+ var lineEnd = lineStart + lineEntry.width;
+
+ curDocChar = lineStart;
+ lineEntry.domInfo.clearSpans();
+ getSpansForLine(lineEntry, tokenFunc, lineStart);
+ lineEntry.domInfo.finishUpdate();
+
+ markNodeClean(lineEntry.lineNode);
+
+ if (rep.selStart && rep.selStart[0] == lineIndex || rep.selEnd && rep.selEnd[0] == lineIndex)
+ {
+ selectionNeedsResetting = true;
+ }
+
+ //if (timer()) console.dirxml(lineEntry.lineNode.dom);
+ if (firstLine === null) firstLine = lineIndex;
+ lastLine = lineIndex;
+ lineStart = lineEnd;
+ lineEntry = rep.lines.next(lineEntry);
+ lineIndex++;
+ }
+ if (selectionNeedsResetting)
+ {
+ currentCallStack.selectionAffected = true;
+ }
+ //console.debug("Recolored line range %d-%d", firstLine, lastLine);
+ }
+
+ // like getSpansForRange, but for a line, and the func takes (text,class)
+ // instead of (width,class); excludes the trailing '\n' from
+ // consideration by func
+
+
+ function getSpansForLine(lineEntry, textAndClassFunc, lineEntryOffsetHint)
+ {
+ var lineEntryOffset = lineEntryOffsetHint;
+ if ((typeof lineEntryOffset) != "number")
+ {
+ lineEntryOffset = rep.lines.offsetOfEntry(lineEntry);
+ }
+ var text = lineEntry.text;
+ var width = lineEntry.width; // text.length+1
+ if (text.length === 0)
+ {
+ // allow getLineStyleFilter to set line-div styles
+ var func = linestylefilter.getLineStyleFilter(
+ 0, '', textAndClassFunc, rep.apool);
+ func('', '');
+ }
+ else
+ {
+ var offsetIntoLine = 0;
+ var filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser);
+ var lineNum = rep.lines.indexOfEntry(lineEntry);
+ var aline = rep.alines[lineNum];
+ filteredFunc = linestylefilter.getLineStyleFilter(
+ text.length, aline, filteredFunc, rep.apool);
+ filteredFunc(text, '');
+ }
+ }
+
+ var observedChanges;
+
+ function clearObservedChanges()
+ {
+ observedChanges = {
+ cleanNodesNearChanges: {}
+ };
+ }
+ clearObservedChanges();
+
+ function getCleanNodeByKey(key)
+ {
+ var p = PROFILER("getCleanNodeByKey", false);
+ p.extra = 0;
+ var n = doc.getElementById(key);
+ // copying and pasting can lead to duplicate ids
+ while (n && isNodeDirty(n))
+ {
+ p.extra++;
+ n.id = "";
+ n = doc.getElementById(key);
+ }
+ p.literal(p.extra, "extra");
+ p.end();
+ return n;
+ }
+
+ function observeChangesAroundNode(node)
+ {
+ // Around this top-level DOM node, look for changes to the document
+ // (from how it looks in our representation) and record them in a way
+ // that can be used to "normalize" the document (apply the changes to our
+ // representation, and put the DOM in a canonical form).
+ // top.console.log("observeChangesAroundNode(%o)", node);
+ var cleanNode;
+ var hasAdjacentDirtyness;
+ if (!isNodeDirty(node))
+ {
+ cleanNode = node;
+ var prevSib = cleanNode.previousSibling;
+ var nextSib = cleanNode.nextSibling;
+ hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) || (nextSib && isNodeDirty(nextSib)));
+ }
+ else
+ {
+ // node is dirty, look for clean node above
+ var upNode = node.previousSibling;
+ while (upNode && isNodeDirty(upNode))
+ {
+ upNode = upNode.previousSibling;
+ }
+ if (upNode)
+ {
+ cleanNode = upNode;
+ }
+ else
+ {
+ var downNode = node.nextSibling;
+ while (downNode && isNodeDirty(downNode))
+ {
+ downNode = downNode.nextSibling;
+ }
+ if (downNode)
+ {
+ cleanNode = downNode;
+ }
+ }
+ if (!cleanNode)
+ {
+ // Couldn't find any adjacent clean nodes!
+ // Since top and bottom of doc is dirty, the dirty area will be detected.
+ return;
+ }
+ hasAdjacentDirtyness = true;
+ }
+
+ if (hasAdjacentDirtyness)
+ {
+ // previous or next line is dirty
+ observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true;
+ }
+ else
+ {
+ // next and prev lines are clean (if they exist)
+ var lineKey = uniqueId(cleanNode);
+ var prevSib = cleanNode.previousSibling;
+ var nextSib = cleanNode.nextSibling;
+ var actualPrevKey = ((prevSib && uniqueId(prevSib)) || null);
+ var actualNextKey = ((nextSib && uniqueId(nextSib)) || null);
+ var repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey));
+ var repNextEntry = rep.lines.next(rep.lines.atKey(lineKey));
+ var repPrevKey = ((repPrevEntry && repPrevEntry.key) || null);
+ var repNextKey = ((repNextEntry && repNextEntry.key) || null);
+ if (actualPrevKey != repPrevKey || actualNextKey != repNextKey)
+ {
+ observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true;
+ }
+ }
+ }
+
+ function observeChangesAroundSelection()
+ {
+ if (currentCallStack.observedSelection) return;
+ currentCallStack.observedSelection = true;
+
+ var p = PROFILER("getSelection", false);
+ var selection = getSelection();
+ p.end();
+
+ function topLevel(n)
+ {
+ if ((!n) || n == root) return null;
+ while (n.parentNode != root)
+ {
+ n = n.parentNode;
+ }
+ return n;
+ }
+
+ if (selection)
+ {
+ var node1 = topLevel(selection.startPoint.node);
+ var node2 = topLevel(selection.endPoint.node);
+ if (node1) observeChangesAroundNode(node1);
+ if (node2 && node1 != node2)
+ {
+ observeChangesAroundNode(node2);
+ }
+ }
+ }
+
+ function observeSuspiciousNodes()
+ {
+ // inspired by Firefox bug #473255, where pasting formatted text
+ // causes the cursor to jump away, making the new HTML never found.
+ if (root.getElementsByTagName)
+ {
+ var nds = root.getElementsByTagName("style");
+ for (var i = 0; i < nds.length; i++)
+ {
+ var n = nds[i];
+ while (n.parentNode && n.parentNode != root)
+ {
+ n = n.parentNode;
+ }
+ if (n.parentNode == root)
+ {
+ observeChangesAroundNode(n);
+ }
+ }
+ }
+ }
+
+ function incorporateUserChanges(isTimeUp)
+ {
+
+ if (currentCallStack.domClean) return false;
+
+ currentCallStack.isUserChange = true;
+
+ isTimeUp = (isTimeUp ||
+ function()
+ {
+ return false;
+ });
+
+ if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false;
+
+ var p = PROFILER("incorp", false);
+
+ //if (doc.body.innerHTML.indexOf("AppJet") >= 0)
+ //dmesg(htmlPrettyEscape(doc.body.innerHTML));
+ //if (top.RECORD) top.RECORD.push(doc.body.innerHTML);
+ // returns true if dom changes were made
+ if (!root.firstChild)
+ {
+ root.innerHTML = "";
+ }
+
+ p.mark("obs");
+ observeChangesAroundSelection();
+ observeSuspiciousNodes();
+ p.mark("dirty");
+ var dirtyRanges = getDirtyRanges();
+ //console.log("dirtyRanges: "+toSource(dirtyRanges));
+ var dirtyRangesCheckOut = true;
+ var j = 0;
+ var a, b;
+ while (j < dirtyRanges.length)
+ {
+ a = dirtyRanges[j][0];
+ b = dirtyRanges[j][1];
+ if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key))))
+ {
+ dirtyRangesCheckOut = false;
+ break;
+ }
+ j++;
+ }
+ if (!dirtyRangesCheckOut)
+ {
+ var numBodyNodes = root.childNodes.length;
+ for (var k = 0; k < numBodyNodes; k++)
+ {
+ var bodyNode = root.childNodes.item(k);
+ if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id))))
+ {
+ observeChangesAroundNode(bodyNode);
+ }
+ }
+ dirtyRanges = getDirtyRanges();
+ }
+
+ clearObservedChanges();
+
+ p.mark("getsel");
+ var selection = getSelection();
+
+ //console.log(magicdom.root.dom.innerHTML);
+ //console.log("got selection: %o", selection);
+ var selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection
+ var i = 0;
+ var splicesToDo = [];
+ var netNumLinesChangeSoFar = 0;
+ var toDeleteAtEnd = [];
+ p.mark("ranges");
+ p.literal(dirtyRanges.length, "numdirt");
+ var domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]]
+ while (i < dirtyRanges.length)
+ {
+ var range = dirtyRanges[i];
+ a = range[0];
+ b = range[1];
+ var firstDirtyNode = (((a === 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling);
+ firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode);
+ var lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling);
+ lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
+ if (firstDirtyNode && lastDirtyNode)
+ {
+ var cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author);
+ cc.notifySelection(selection);
+ var dirtyNodes = [];
+ for (var n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode);
+ n = n.nextSibling)
+ {
+ if (browser.msie)
+ {
+ // try to undo IE's pesky and overzealous linkification
+ try
+ {
+ n.createTextRange().execCommand("unlink", false, null);
+ }
+ catch (e)
+ {}
+ }
+ cc.collectContent(n);
+ dirtyNodes.push(n);
+ }
+ cc.notifyNextNode(lastDirtyNode.nextSibling);
+ var lines = cc.getLines();
+ if ((lines.length <= 1 || lines[lines.length - 1] !== "") && lastDirtyNode.nextSibling)
+ {
+ // dirty region doesn't currently end a line, even taking the following node
+ // (or lack of node) into account, so include the following clean node.
+ // It could be SPAN or a DIV; basically this is any case where the contentCollector
+ // decides it isn't done.
+ // Note that this clean node might need to be there for the next dirty range.
+ //console.log("inclusive of "+lastDirtyNode.next().dom.tagName);
+ b++;
+ var cleanLine = lastDirtyNode.nextSibling;
+ cc.collectContent(cleanLine);
+ toDeleteAtEnd.push(cleanLine);
+ cc.notifyNextNode(cleanLine.nextSibling);
+ }
+
+ var ccData = cc.finish();
+ var ss = ccData.selStart;
+ var se = ccData.selEnd;
+ lines = ccData.lines;
+ var lineAttribs = ccData.lineAttribs;
+ var linesWrapped = ccData.linesWrapped;
+ var scrollToTheLeftNeeded = false;
+
+ if (linesWrapped > 0)
+ {
+ if(!browser.msie){
+ // chrome decides in it's infinite wisdom that its okay to put the browsers visisble window in the middle of the span
+ // an outcome of this is that the first chars of the string are no longer visible to the user.. Yay chrome..
+ // Move the browsers visible area to the left hand side of the span
+ // Firefox isn't quite so bad, but it's still pretty quirky.
+ var scrollToTheLeftNeeded = true;
+ }
+ // console.log("Editor warning: " + linesWrapped + " long line" + (linesWrapped == 1 ? " was" : "s were") + " hard-wrapped into " + ccData.numLinesAfter + " lines.");
+ }
+
+ if (ss[0] >= 0) selStart = [ss[0] + a + netNumLinesChangeSoFar, ss[1]];
+ if (se[0] >= 0) selEnd = [se[0] + a + netNumLinesChangeSoFar, se[1]];
+
+ var entries = [];
+ var nodeToAddAfter = lastDirtyNode;
+ var lineNodeInfos = new Array(lines.length);
+ for (var k = 0; k < lines.length; k++)
+ {
+ var lineString = lines[k];
+ var newEntry = createDomLineEntry(lineString);
+ entries.push(newEntry);
+ lineNodeInfos[k] = newEntry.domInfo;
+ }
+ //var fragment = magicdom.wrapDom(document.createDocumentFragment());
+ domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]);
+ _.each(dirtyNodes,function(n){
+ toDeleteAtEnd.push(n);
+ });
+ var spliceHints = {};
+ if (selStart) spliceHints.selStart = selStart;
+ if (selEnd) spliceHints.selEnd = selEnd;
+ splicesToDo.push([a + netNumLinesChangeSoFar, b - a, entries, lineAttribs, spliceHints]);
+ netNumLinesChangeSoFar += (lines.length - (b - a));
+ }
+ else if (b > a)
+ {
+ splicesToDo.push([a + netNumLinesChangeSoFar, b - a, [],
+ []
+ ]);
+ }
+ i++;
+ }
+
+ var domChanges = (splicesToDo.length > 0);
+
+ // update the representation
+ p.mark("splice");
+ _.each(splicesToDo, function(splice)
+ {
+ doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]);
+ });
+
+ //p.mark("relex");
+ //rep.lexer.lexCharRange(getVisibleCharRange(), function() { return false; });
+ //var isTimeUp = newTimeLimit(100);
+ // do DOM inserts
+ p.mark("insert");
+ _.each(domInsertsNeeded,function(ins)
+ {
+ insertDomLines(ins[0], ins[1], isTimeUp);
+ });
+
+ p.mark("del");
+ // delete old dom nodes
+ _.each(toDeleteAtEnd,function(n)
+ {
+ //var id = n.uniqueId();
+ // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf)
+ if(n.parentNode) n.parentNode.removeChild(n);
+
+ //dmesg(htmlPrettyEscape(htmlForRemovedChild(n)));
+ //console.log("removed: "+id);
+ });
+
+ if(scrollToTheLeftNeeded){ // needed to stop chrome from breaking the ui when long strings without spaces are pasted
+ $("#innerdocbody").scrollLeft(0);
+ }
+
+ p.mark("findsel");
+ // if the nodes that define the selection weren't encountered during
+ // content collection, figure out where those nodes are now.
+ if (selection && !selStart)
+ {
+ //if (domChanges) dmesg("selection not collected");
+ var selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', {
+ callstack: currentCallStack,
+ editorInfo: editorInfo,
+ rep: rep,
+ root:root,
+ point:selection.startPoint,
+ documentAttributeManager: documentAttributeManager
+ });
+ selStart = (selStartFromHook==null||selStartFromHook.length==0)?getLineAndCharForPoint(selection.startPoint):selStartFromHook;
+ }
+ if (selection && !selEnd)
+ {
+ var selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', {
+ callstack: currentCallStack,
+ editorInfo: editorInfo,
+ rep: rep,
+ root:root,
+ point:selection.endPoint,
+ documentAttributeManager: documentAttributeManager
+ });
+ selEnd = (selEndFromHook==null||selEndFromHook.length==0)?getLineAndCharForPoint(selection.endPoint):selEndFromHook;
+ }
+
+ // selection from content collection can, in various ways, extend past final
+ // BR in firefox DOM, so cap the line
+ var numLines = rep.lines.length();
+ if (selStart && selStart[0] >= numLines)
+ {
+ selStart[0] = numLines - 1;
+ selStart[1] = rep.lines.atIndex(selStart[0]).text.length;
+ }
+ if (selEnd && selEnd[0] >= numLines)
+ {
+ selEnd[0] = numLines - 1;
+ selEnd[1] = rep.lines.atIndex(selEnd[0]).text.length;
+ }
+
+ p.mark("repsel");
+ // update rep if we have a new selection
+ // NOTE: IE loses the selection when you click stuff in e.g. the
+ // editbar, so removing the selection when it's lost is not a good
+ // idea.
+ if (selection) repSelectionChange(selStart, selEnd, selection && selection.focusAtStart);
+ // update browser selection
+ p.mark("browsel");
+ if (selection && (domChanges || isCaret()))
+ {
+ // if no DOM changes (not this case), want to treat range selection delicately,
+ // e.g. in IE not lose which end of the selection is the focus/anchor;
+ // on the other hand, we may have just noticed a press of PageUp/PageDown
+ currentCallStack.selectionAffected = true;
+ }
+
+ currentCallStack.domClean = true;
+
+ p.mark("fixview");
+
+ fixView();
+
+ p.end("END");
+
+ return domChanges;
+ }
+
+ var STYLE_ATTRIBS = {
+ bold: true,
+ italic: true,
+ underline: true,
+ strikethrough: true,
+ list: true
+ };
+ var OTHER_INCORPED_ATTRIBS = {
+ insertorder: true,
+ author: true
+ };
+
+ function isStyleAttribute(aname)
+ {
+ return !!STYLE_ATTRIBS[aname];
+ }
+
+ function isIncorpedAttribute(aname)
+ {
+ return ( !! STYLE_ATTRIBS[aname]) || ( !! OTHER_INCORPED_ATTRIBS[aname]);
+ }
+
+ function insertDomLines(nodeToAddAfter, infoStructs, isTimeUp)
+ {
+ isTimeUp = (isTimeUp ||
+ function()
+ {
+ return false;
+ });
+
+ var lastEntry;
+ var lineStartOffset;
+ if (infoStructs.length < 1) return;
+ var startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node));
+ var endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node));
+ var charStart = rep.lines.offsetOfEntry(startEntry);
+ var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width;
+
+ //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp);
+ _.each(infoStructs, function(info)
+ {
+ var p2 = PROFILER("insertLine", false);
+ var node = info.node;
+ var key = uniqueId(node);
+ var entry;
+ p2.mark("findEntry");
+ if (lastEntry)
+ {
+ // optimization to avoid recalculation
+ var next = rep.lines.next(lastEntry);
+ if (next && next.key == key)
+ {
+ entry = next;
+ lineStartOffset += lastEntry.width;
+ }
+ }
+ if (!entry)
+ {
+ p2.literal(1, "nonopt");
+ entry = rep.lines.atKey(key);
+ lineStartOffset = rep.lines.offsetOfKey(key);
+ }
+ else p2.literal(0, "nonopt");
+ lastEntry = entry;
+ p2.mark("spans");
+ getSpansForLine(entry, function(tokenText, tokenClass)
+ {
+ info.appendSpan(tokenText, tokenClass);
+ }, lineStartOffset, isTimeUp());
+ //else if (entry.text.length > 0) {
+ //info.appendSpan(entry.text, 'dirty');
+ //}
+ p2.mark("addLine");
+ info.prepareForAdd();
+ entry.lineMarker = info.lineMarker;
+ if (!nodeToAddAfter)
+ {
+ root.insertBefore(node, root.firstChild);
+ }
+ else
+ {
+ root.insertBefore(node, nodeToAddAfter.nextSibling);
+ }
+ nodeToAddAfter = node;
+ info.notifyAdded();
+ p2.mark("markClean");
+ markNodeClean(node);
+ p2.end();
+ });
+ }
+
+ function isCaret()
+ {
+ return (rep.selStart && rep.selEnd && rep.selStart[0] == rep.selEnd[0] && rep.selStart[1] == rep.selEnd[1]);
+ }
+ editorInfo.ace_isCaret = isCaret;
+
+ // prereq: isCaret()
+
+
+ function caretLine()
+ {
+ 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
+ if (isCaret() && caretColumn() === 0 && caretLine() > 0)
+ {
+ var lineNum = caretLine();
+ var thisLine = rep.lines.atIndex(lineNum);
+ var prevLine = rep.lines.prev(thisLine);
+ var prevLineText = prevLine.text;
+ var theIndent = /^ *(?:)/.exec(prevLineText)[0];
+ if (/[\[\(\:\{]\s*$/.exec(prevLineText)) theIndent += THE_TAB;
+ var cs = Changeset.builder(rep.lines.totalWidth()).keep(
+ rep.lines.offsetOfIndex(lineNum), lineNum).insert(
+ theIndent, [
+ ['author', thisAuthor]
+ ], rep.apool).toString();
+ performDocumentApplyChangeset(cs);
+ performSelectionChange([lineNum, theIndent.length], [lineNum, theIndent.length]);
+ }
+ }
+
+ function getPointForLineAndChar(lineAndChar)
+ {
+ var line = lineAndChar[0];
+ var charsLeft = lineAndChar[1];
+ //console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key,
+ //getCleanNodeByKey(rep.lines.atIndex(line).key));
+ var lineEntry = rep.lines.atIndex(line);
+ charsLeft -= lineEntry.lineMarker;
+ if (charsLeft < 0)
+ {
+ charsLeft = 0;
+ }
+ var lineNode = lineEntry.lineNode;
+ var n = lineNode;
+ var after = false;
+ if (charsLeft === 0)
+ {
+ var index = 0;
+
+ if (browser.msie && parseInt(browser.version) >= 11) {
+ browser.msie = false; // Temp fix to resolve enter and backspace issues..
+ // Note that this makes MSIE behave like modern browsers..
+ }
+ if (browser.msie && line == (rep.lines.length() - 1) && lineNode.childNodes.length === 0)
+ {
+ // best to stay at end of last empty div in IE
+ index = 1;
+ }
+ return {
+ node: lineNode,
+ index: index,
+ maxIndex: 1
+ };
+ }
+ while (!(n == lineNode && after))
+ {
+ if (after)
+ {
+ if (n.nextSibling)
+ {
+ n = n.nextSibling;
+ after = false;
+ }
+ else n = n.parentNode;
+ }
+ else
+ {
+ if (isNodeText(n))
+ {
+ var len = n.nodeValue.length;
+ if (charsLeft <= len)
+ {
+ return {
+ node: n,
+ index: charsLeft,
+ maxIndex: len
+ };
+ }
+ charsLeft -= len;
+ after = true;
+ }
+ else
+ {
+ if (n.firstChild) n = n.firstChild;
+ else after = true;
+ }
+ }
+ }
+ return {
+ node: lineNode,
+ index: 1,
+ maxIndex: 1
+ };
+ }
+
+ function nodeText(n)
+ {
+ return n.innerText || n.textContent || n.nodeValue || '';
+ }
+
+ function getLineAndCharForPoint(point)
+ {
+ // Turn DOM node selection into [line,char] selection.
+ // This method has to work when the DOM is not pristine,
+ // assuming the point is not in a dirty node.
+ if (point.node == root)
+ {
+ if (point.index === 0)
+ {
+ return [0, 0];
+ }
+ else
+ {
+ var N = rep.lines.length();
+ var ln = rep.lines.atIndex(N - 1);
+ return [N - 1, ln.text.length];
+ }
+ }
+ else
+ {
+ var n = point.node;
+ var col = 0;
+ // if this part fails, it probably means the selection node
+ // was dirty, and we didn't see it when collecting dirty nodes.
+ if (isNodeText(n))
+ {
+ col = point.index;
+ }
+ else if (point.index > 0)
+ {
+ col = nodeText(n).length;
+ }
+ var parNode, prevSib;
+ while ((parNode = n.parentNode) != root)
+ {
+ if ((prevSib = n.previousSibling))
+ {
+ n = prevSib;
+ col += nodeText(n).length;
+ }
+ else
+ {
+ n = parNode;
+ }
+ }
+ if (n.id === "") console.debug("BAD");
+ if (n.firstChild && isBlockElement(n.firstChild))
+ {
+ col += 1; // lineMarker
+ }
+ var lineEntry = rep.lines.atKey(n.id);
+ var lineNum = rep.lines.indexOfEntry(lineEntry);
+ return [lineNum, col];
+ }
+ }
+ editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint;
+
+ function createDomLineEntry(lineString)
+ {
+ var info = doCreateDomLine(lineString.length > 0);
+ var newNode = info.node;
+ return {
+ key: uniqueId(newNode),
+ text: lineString,
+ lineNode: newNode,
+ domInfo: info,
+ lineMarker: 0
+ };
+ }
+
+ function canApplyChangesetToDocument(changes)
+ {
+ return Changeset.oldLen(changes) == rep.alltext.length;
+ }
+
+ function performDocumentApplyChangeset(changes, insertsAfterSelection)
+ {
+ doRepApplyChangeset(changes, insertsAfterSelection);
+
+ var requiredSelectionSetting = null;
if (rep.selStart && rep.selEnd)
{
var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
- evt.selStart = selStartChar;
- evt.selEnd = selEndChar;
- evt.selFocusAtStart = rep.selFocusAtStart;
+ var result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
+ requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
}
- if (undoModule.enabled)
- {
- var undoWorked = false;
- try
+
+ var linesMutatee = {
+ splice: function(start, numRemoved, newLinesVA)
{
- if (evt.eventType == "setup" || evt.eventType == "importText" || evt.eventType == "setBaseText")
+ var args = Array.prototype.slice.call(arguments, 2);
+ domAndRepSplice(start, numRemoved, _.map(args, function(s){ return s.slice(0, -1); }), null);
+ },
+ get: function(i)
+ {
+ return rep.lines.atIndex(i).text + '\n';
+ },
+ length: function()
+ {
+ return rep.lines.length();
+ },
+ slice_notused: function(start, end)
+ {
+ return _.map(rep.lines.slice(start, end), function(e)
{
- undoModule.clearHistory();
- }
- else if (evt.eventType == "nonundoable")
- {
- if (evt.changeset)
- {
- undoModule.reportExternalChange(evt.changeset);
- }
- }
- else
- {
- undoModule.reportEvent(evt);
- }
- undoWorked = true;
+ return e.text + '\n';
+ });
}
- finally
+ };
+
+ Changeset.mutateTextLines(changes, linesMutatee);
+
+ checkALines();
+
+ if (requiredSelectionSetting)
+ {
+ performSelectionChange(lineAndColumnFromChar(requiredSelectionSetting[0]), lineAndColumnFromChar(requiredSelectionSetting[1]), requiredSelectionSetting[2]);
+ }
+
+ function domAndRepSplice(startLine, deleteCount, newLineStrings, isTimeUp)
+ {
+ // dgreensp 3/2009: the spliced lines may be in the middle of a dirty region,
+ // so if no explicit time limit, don't spend a lot of time highlighting
+ isTimeUp = (isTimeUp || newTimeLimit(50));
+
+ var keysToDelete = [];
+ if (deleteCount > 0)
{
- if (!undoWorked)
+ var entryToDelete = rep.lines.atIndex(startLine);
+ for (var i = 0; i < deleteCount; i++)
{
- undoModule.enabled = false; // for safety
+ keysToDelete.push(entryToDelete.key);
+ entryToDelete = rep.lines.next(entryToDelete);
}
}
+
+ var lineEntries = _.map(newLineStrings, createDomLineEntry);
+
+ doRepLineSplice(startLine, deleteCount, lineEntries);
+
+ var nodeToAddAfter;
+ if (startLine > 0)
+ {
+ nodeToAddAfter = getCleanNodeByKey(rep.lines.atIndex(startLine - 1).key);
+ }
+ else nodeToAddAfter = null;
+
+ insertDomLines(nodeToAddAfter, _.map(lineEntries, function(entry)
+ {
+ return entry.domInfo;
+ }), isTimeUp);
+
+ _.each(keysToDelete, function(k)
+ {
+ var n = doc.getElementById(k);
+ n.parentNode.removeChild(n);
+ });
+
+ if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine + deleteCount) || (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount))
+ {
+ currentCallStack.selectionAffected = true;
+ }
}
}
- function startNewEvent(eventType, dontSubmitOld)
+ function checkChangesetLineInformationAgainstRep(changes)
{
- var oldEvent = currentCallStack.editEvent;
- if (!dontSubmitOld)
- {
- submitOldEvent(oldEvent);
- }
- currentCallStack.editEvent = newEditEvent(eventType);
- return oldEvent;
- }
-
- currentCallStack = {
- type: type,
- docTextChanged: false,
- selectionAffected: false,
- userChangedSelection: false,
- domClean: false,
- profileRest: profileRest,
- isUserChange: false,
- // is this a "user change" type of call-stack
- repChanged: false,
- editEvent: newEditEvent(type),
- startNewEvent: startNewEvent
- };
- var cleanExit = false;
- var result;
- try
- {
- result = action();
-
- hooks.callAll('aceEditEvent', {
- callstack: currentCallStack,
- editorInfo: editorInfo,
- rep: rep,
- documentAttributeManager: documentAttributeManager
- });
-
- //console.log("Just did action for: "+type);
- cleanExit = true;
- }
- /*
- catch (e)
- {
- caughtErrors.push(
- {
- error: e,
- time: +new Date()
- });
- dmesg(e.toString());
- throw e;
- }
-*/
- finally
- {
- var cs = currentCallStack;
- //console.log("Finished action for: "+type);
- if (cleanExit)
- {
- submitOldEvent(cs.editEvent);
- if (cs.domClean && cs.type != "setup")
- {
- // if (cs.isUserChange)
- // {
- // if (cs.repChanged) parenModule.notifyChange();
- // else parenModule.notifyTick();
- // }
- if (cs.selectionAffected)
- {
- updateBrowserSelectionFromRep();
- }
- if ((cs.docTextChanged || cs.userChangedSelection) && cs.type != "applyChangesToBase")
- {
- scrollSelectionIntoView();
- }
- if (cs.docTextChanged && cs.type.indexOf("importText") < 0)
- {
- outsideNotifyDirty();
- }
- }
- }
- else
- {
- // non-clean exit
- if (currentCallStack.type == "idleWorkTimer")
- {
- idleWorkTimer.atLeast(1000);
- }
- }
- currentCallStack = null;
- if (profiling) console.profileEnd();
- }
- return result;
- }
- editorInfo.ace_inCallStack = inCallStack;
-
- function inCallStackIfNecessary(type, action)
- {
- if (!currentCallStack)
- {
- inCallStack(type, action);
- }
- else
- {
- action();
- }
- }
- editorInfo.ace_inCallStackIfNecessary = inCallStackIfNecessary;
-
- function dispose()
- {
- disposed = true;
- if (idleWorkTimer) idleWorkTimer.never();
- teardown();
- }
-
- function checkALines()
- {
- return; // disable for speed
-
-
- function error()
- {
- throw new Error("checkALines");
- }
- if (rep.alines.length != rep.lines.length())
- {
- error();
- }
- for (var i = 0; i < rep.alines.length; i++)
- {
- var aline = rep.alines[i];
- var lineText = rep.lines.atIndex(i).text + "\n";
- var lineTextLength = lineText.length;
- var opIter = Changeset.opIterator(aline);
- var alineLength = 0;
+ return true; // disable for speed
+ var opIter = Changeset.opIterator(Changeset.unpack(changes).ops);
+ var curOffset = 0;
+ var curLine = 0;
+ var curCol = 0;
while (opIter.hasNext())
{
var o = opIter.next();
- alineLength += o.chars;
- if (opIter.hasNext())
+ if (o.opcode == '-' || o.opcode == '=')
{
- if (o.lines !== 0) error();
- }
- else
- {
- if (o.lines != 1) error();
- }
- }
- if (alineLength != lineTextLength)
- {
- error();
- }
- }
- }
-
- function setWraps(newVal)
- {
- doesWrap = newVal;
- var dwClass = "doesWrap";
- setClassPresence(root, "doesWrap", doesWrap);
- scheduler.setTimeout(function()
- {
- inCallStackIfNecessary("setWraps", function()
- {
- fastIncorp(7);
- recreateDOM();
- fixView();
- });
- }, 0);
-
- // Chrome can't handle the truth.. If CSS rule white-space:pre-wrap
- // is true then any paste event will insert two lines..
- // Sadly this will mean you get a walking Caret in Chrome when clicking on a URL
- // So this has to be set to pre-wrap ;(
- // We need to file a bug w/ the Chromium team.
- if(browser.chrome){
- $("#innerdocbody").css({"white-space":"pre-wrap"});
- }
-
- }
-
- function setStyled(newVal)
- {
- var oldVal = isStyled;
- isStyled = !! newVal;
-
- if (newVal != oldVal)
- {
- if (!newVal)
- {
- // clear styles
- inCallStackIfNecessary("setStyled", function()
- {
- fastIncorp(12);
- var clearStyles = [];
- for (var k in STYLE_ATTRIBS)
+ curOffset += o.chars;
+ if (o.lines)
{
- clearStyles.push([k, '']);
+ curLine += o.lines;
+ curCol = 0;
}
- performDocumentApplyAttributesToCharRange(0, rep.alltext.length, clearStyles);
- });
- }
- }
- }
-
- function setTextFace(face)
- {
- textFace = face;
- root.style.fontFamily = textFace;
- lineMetricsDiv.style.fontFamily = textFace;
- scheduler.setTimeout(function()
- {
- setUpTrackingCSS();
- }, 0);
- }
-
- function setTextSize(size)
- {
- textSize = size;
- root.style.fontSize = textSize + "px";
- root.style.lineHeight = textLineHeight() + "px";
- sideDiv.style.lineHeight = textLineHeight() + "px";
- lineMetricsDiv.style.fontSize = textSize + "px";
- scheduler.setTimeout(function()
- {
- setUpTrackingCSS();
- }, 0);
- }
-
- function recreateDOM()
- {
- // precond: normalized
- recolorLinesInRange(0, rep.alltext.length);
- }
-
- function setEditable(newVal)
- {
- isEditable = newVal;
-
- // the following may fail, e.g. if iframe is hidden
- if (!isEditable)
- {
- setDesignMode(false);
- }
- else
- {
- setDesignMode(true);
- }
- setClassPresence(root, "static", !isEditable);
- }
-
- function enforceEditability()
- {
- setEditable(isEditable);
- }
-
- function importText(text, undoable, dontProcess)
- {
- var lines;
- if (dontProcess)
- {
- if (text.charAt(text.length - 1) != "\n")
- {
- throw new Error("new raw text must end with newline");
- }
- if (/[\r\t\xa0]/.exec(text))
- {
- throw new Error("new raw text must not contain CR, tab, or nbsp");
- }
- lines = text.substring(0, text.length - 1).split('\n');
- }
- else
- {
- lines = _.map(text.split('\n'), textify);
- }
- var newText = "\n";
- if (lines.length > 0)
- {
- newText = lines.join('\n') + '\n';
- }
-
- inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function()
- {
- setDocText(newText);
- });
-
- if (dontProcess && rep.alltext != text)
- {
- throw new Error("mismatch error setting raw text in importText");
- }
- }
-
- function importAText(atext, apoolJsonObj, undoable)
- {
- atext = Changeset.cloneAText(atext);
- if (apoolJsonObj)
- {
- var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
- atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
- }
- inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function()
- {
- setDocAText(atext);
- });
- }
-
- function setDocAText(atext)
- {
- fastIncorp(8);
-
- var oldLen = rep.lines.totalWidth();
- var numLines = rep.lines.length();
- var upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
- var lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
- var assem = Changeset.smartOpAssembler();
- var o = Changeset.newOp('-');
- o.chars = upToLastLine;
- o.lines = numLines - 1;
- assem.append(o);
- o.chars = lastLineLength;
- o.lines = 0;
- assem.append(o);
- Changeset.appendATextToAssembler(atext, assem);
- var newLen = oldLen + assem.getLengthChange();
- var changeset = Changeset.checkRep(
- Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
- performDocumentApplyChangeset(changeset);
-
- performSelectionChange([0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]);
-
- idleWorkTimer.atMost(100);
-
- if (rep.alltext != atext.text)
- {
- dmesg(htmlPrettyEscape(rep.alltext));
- dmesg(htmlPrettyEscape(atext.text));
- throw new Error("mismatch error setting raw text in setDocAText");
- }
- }
-
- function setDocText(text)
- {
- setDocAText(Changeset.makeAText(text));
- }
-
- function getDocText()
- {
- var alltext = rep.alltext;
- var len = alltext.length;
- if (len > 0) len--; // final extra newline
- return alltext.substring(0, len);
- }
-
- function exportText()
- {
- if (currentCallStack && !currentCallStack.domClean)
- {
- inCallStackIfNecessary("exportText", function()
- {
- fastIncorp(2);
- });
- }
- return getDocText();
- }
-
- function editorChangedSize()
- {
- fixView();
- }
-
- function setOnKeyPress(handler)
- {
- outsideKeyPress = handler;
- }
-
- function setOnKeyDown(handler)
- {
- outsideKeyDown = handler;
- }
-
- function setNotifyDirty(handler)
- {
- outsideNotifyDirty = handler;
- }
-
- function getFormattedCode()
- {
- if (currentCallStack && !currentCallStack.domClean)
- {
- inCallStackIfNecessary("getFormattedCode", incorporateUserChanges);
- }
- var buf = [];
- if (rep.lines.length() > 0)
- {
- // should be the case, even for empty file
- var entry = rep.lines.atIndex(0);
- while (entry)
- {
- var domInfo = entry.domInfo;
- buf.push((domInfo && domInfo.getInnerHTML()) || domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || ' ' /*empty line*/ );
- entry = rep.lines.next(entry);
- }
- }
- return '';
- }
-
- var CMDS = {
- clearauthorship: function(prompt)
- {
- if ((!(rep.selStart && rep.selEnd)) || isCaret())
- {
- if (prompt)
- {
- prompt();
- }
- else
- {
- performDocumentApplyAttributesToCharRange(0, rep.alltext.length, [
- ['author', '']
- ]);
- }
- }
- else
- {
- setAttributeOnSelection('author', '');
- }
- }
- };
-
- function execCommand(cmd)
- {
- cmd = cmd.toLowerCase();
- var cmdArgs = Array.prototype.slice.call(arguments, 1);
- if (CMDS[cmd])
- {
- inCallStackIfNecessary(cmd, function()
- {
- fastIncorp(9);
- CMDS[cmd].apply(CMDS, cmdArgs);
- });
- }
- }
-
- function replaceRange(start, end, text)
- {
- inCallStackIfNecessary('replaceRange', function()
- {
- fastIncorp(9);
- performDocumentReplaceRange(start, end, text);
- });
- }
-
- editorInfo.ace_focus = focus;
- editorInfo.ace_importText = importText;
- editorInfo.ace_importAText = importAText;
- editorInfo.ace_exportText = exportText;
- editorInfo.ace_editorChangedSize = editorChangedSize;
- editorInfo.ace_setOnKeyPress = setOnKeyPress;
- editorInfo.ace_setOnKeyDown = setOnKeyDown;
- editorInfo.ace_setNotifyDirty = setNotifyDirty;
- editorInfo.ace_dispose = dispose;
- editorInfo.ace_getFormattedCode = getFormattedCode;
- 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()
- {
- return fn(editorInfo);
- };
-
- if (normalize !== undefined)
- {
- var wrapper1 = wrapper;
- wrapper = function()
- {
- editorInfo.ace_fastIncorp(9);
- wrapper1();
- };
- }
-
- if (callStack !== undefined)
- {
- return editorInfo.ace_inCallStack(callStack, wrapper);
- }
- else
- {
- return wrapper();
- }
- };
-
- // This methed exposes a setter for some ace properties
- // @param key the name of the parameter
- // @param value the value to set to
- editorInfo.ace_setProperty = function(key, value)
- {
-
- // Convinience function returning a setter for a class on an element
- var setClassPresenceNamed = function(element, cls){
- return function(value){
- setClassPresence(element, cls, !! value)
- }
- };
-
- // These properties are exposed
- var setters = {
- wraps: setWraps,
- showsauthorcolors: setClassPresenceNamed(root, "authorColors"),
- showsuserselections: setClassPresenceNamed(root, "userSelections"),
- showslinenumbers : function(value){
- hasLineNumbers = !! value;
- // disable line numbers on mobile devices
- if (browser.mobile) hasLineNumbers = false;
- setClassPresence(sideDiv, "sidedivhidden", !hasLineNumbers);
- fixView();
- },
- grayedout: setClassPresenceNamed(window.document.body, "grayedout"),
- dmesg: function(){ dmesg = window.dmesg = value; },
- userauthor: function(value){
- thisAuthor = String(value);
- documentAttributeManager.author = thisAuthor;
- },
- styled: setStyled,
- textface: setTextFace,
- textsize: setTextSize,
- rtlistrue: function(value) {
- setClassPresence(root, "rtl", value)
- setClassPresence(root, "ltr", !value)
- document.documentElement.dir = value? 'rtl' : 'ltr'
- }
- };
-
- var setter = setters[key.toLowerCase()];
-
- // check if setter is present
- if(setter !== undefined){
- setter(value)
- }
- };
-
- editorInfo.ace_setBaseText = function(txt)
- {
- changesetTracker.setBaseText(txt);
- };
- editorInfo.ace_setBaseAttributedText = function(atxt, apoolJsonObj)
- {
- setUpTrackingCSS();
- changesetTracker.setBaseAttributedText(atxt, apoolJsonObj);
- };
- editorInfo.ace_applyChangesToBase = function(c, optAuthor, apoolJsonObj)
- {
- changesetTracker.applyChangesToBase(c, optAuthor, apoolJsonObj);
- };
- editorInfo.ace_prepareUserChangeset = function()
- {
- return changesetTracker.prepareUserChangeset();
- };
- editorInfo.ace_applyPreparedChangesetToBase = function()
- {
- changesetTracker.applyPreparedChangesetToBase();
- };
- editorInfo.ace_setUserChangeNotificationCallback = function(f)
- {
- changesetTracker.setUserChangeNotificationCallback(f);
- };
- editorInfo.ace_setAuthorInfo = function(author, info)
- {
- setAuthorInfo(author, info);
- };
- editorInfo.ace_setAuthorSelectionRange = function(author, start, end)
- {
- changesetTracker.setAuthorSelectionRange(author, start, end);
- };
-
- editorInfo.ace_getUnhandledErrors = function()
- {
- return caughtErrors.slice();
- };
-
- editorInfo.ace_getDocument = function()
- {
- return doc;
- };
-
- editorInfo.ace_getDebugProperty = function(prop)
- {
- if (prop == "debugger")
- {
- // obfuscate "eval" so as not to scare yuicompressor
- window['ev' + 'al']("debugger");
- }
- else if (prop == "rep")
- {
- return rep;
- }
- else if (prop == "window")
- {
- return window;
- }
- else if (prop == "document")
- {
- return document;
- }
- return undefined;
- };
-
- function now()
- {
- return (new Date()).getTime();
- }
-
- function newTimeLimit(ms)
- {
- //console.debug("new time limit");
- var startTime = now();
- var lastElapsed = 0;
- var exceededAlready = false;
- var printedTrace = false;
- var isTimeUp = function()
- {
- if (exceededAlready)
- {
- if ((!printedTrace))
- { // && now() - startTime - ms > 300) {
- //console.trace();
- printedTrace = true;
+ else
+ {
+ curCol += o.chars;
}
- return true;
}
- var elapsed = now() - startTime;
- if (elapsed > ms)
+ var calcLine = rep.lines.indexOfOffset(curOffset);
+ var calcLineStart = rep.lines.offsetOfIndex(calcLine);
+ var calcCol = curOffset - calcLineStart;
+ if (calcCol != curCol || calcLine != curLine)
{
- exceededAlready = true;
- //console.debug("time limit hit, before was %d/%d", lastElapsed, ms);
- //console.trace();
- return true;
- }
- else
- {
- lastElapsed = elapsed;
return false;
}
- };
-
- isTimeUp.elapsed = function()
- {
- return now() - startTime;
- };
- return isTimeUp;
- }
-
-
- function makeIdleAction(func)
- {
- var scheduledTimeout = null;
- var scheduledTime = 0;
-
- function unschedule()
- {
- if (scheduledTimeout)
- {
- scheduler.clearTimeout(scheduledTimeout);
- scheduledTimeout = null;
}
+ return true;
}
- function reschedule(time)
+ function doRepApplyChangeset(changes, insertsAfterSelection)
{
- unschedule();
- scheduledTime = time;
- var delay = time - now();
- if (delay < 0) delay = 0;
- scheduledTimeout = scheduler.setTimeout(callback, delay);
- }
+ Changeset.checkRep(changes);
- function callback()
- {
- scheduledTimeout = null;
- // func may reschedule the action
- func();
- }
- return {
- atMost: function(ms)
+ if (Changeset.oldLen(changes) != rep.alltext.length) throw new Error("doRepApplyChangeset length mismatch: " + Changeset.oldLen(changes) + "/" + rep.alltext.length);
+
+ if (!checkChangesetLineInformationAgainstRep(changes))
{
- var latestTime = now() + ms;
- if ((!scheduledTimeout) || scheduledTime > latestTime)
- {
- reschedule(latestTime);
- }
- },
- // atLeast(ms) will schedule the action if not scheduled yet.
- // In other words, "infinity" is replaced by ms, even though
- // it is technically larger.
- atLeast: function(ms)
- {
- var earliestTime = now() + ms;
- if ((!scheduledTimeout) || scheduledTime < earliestTime)
- {
- reschedule(earliestTime);
- }
- },
- never: function()
- {
- unschedule();
+ throw new Error("doRepApplyChangeset line break mismatch");
}
- };
- }
- function fastIncorp(n)
- {
- // normalize but don't do any lexing or anything
- incorporateUserChanges(newTimeLimit(0));
- }
- editorInfo.ace_fastIncorp = fastIncorp;
-
- var idleWorkTimer = makeIdleAction(function()
- {
-
- //if (! top.BEFORE) top.BEFORE = [];
- //top.BEFORE.push(magicdom.root.dom.innerHTML);
- //if (! isEditable) return; // and don't reschedule
- if (inInternationalComposition)
- {
- // don't do idle input incorporation during international input composition
- idleWorkTimer.atLeast(500);
- return;
- }
-
- inCallStackIfNecessary("idleWorkTimer", function()
- {
-
- var isTimeUp = newTimeLimit(250);
-
- //console.time("idlework");
- var finishedImportantWork = false;
- var finishedWork = false;
-
- try
+ (function doRecordUndoInformation(changes)
{
-
- // isTimeUp() is a soft constraint for incorporateUserChanges,
- // which always renormalizes the DOM, no matter how long it takes,
- // but doesn't necessarily lex and highlight it
- incorporateUserChanges(isTimeUp);
-
- if (isTimeUp()) return;
-
- updateLineNumbers(); // update line numbers if any time left
- if (isTimeUp()) return;
-
- var visibleRange = getVisibleCharRange();
- var docRange = [0, rep.lines.totalWidth()];
- //console.log("%o %o", docRange, visibleRange);
- finishedImportantWork = true;
- finishedWork = true;
- }
- finally
- {
- //console.timeEnd("idlework");
- if (finishedWork)
+ var editEvent = currentCallStack.editEvent;
+ if (editEvent.eventType == "nonundoable")
{
- idleWorkTimer.atMost(1000);
- }
- else if (finishedImportantWork)
- {
- // if we've finished highlighting the view area,
- // more highlighting could be counter-productive,
- // e.g. if the user just opened a triple-quote and will soon close it.
- idleWorkTimer.atMost(500);
+ if (!editEvent.changeset)
+ {
+ editEvent.changeset = changes;
+ }
+ else
+ {
+ editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
+ }
}
else
{
- var timeToWait = Math.round(isTimeUp.elapsed() / 2);
- if (timeToWait < 100) timeToWait = 100;
- idleWorkTimer.atMost(timeToWait);
+ var inverseChangeset = Changeset.inverse(changes, {
+ get: function(i)
+ {
+ return rep.lines.atIndex(i).text + '\n';
+ },
+ length: function()
+ {
+ return rep.lines.length();
+ }
+ }, rep.alines, rep.apool);
+
+ if (!editEvent.backset)
+ {
+ editEvent.backset = inverseChangeset;
+ }
+ else
+ {
+ editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool);
+ }
}
- }
- });
+ })(changes);
- //if (! top.AFTER) top.AFTER = [];
- //top.AFTER.push(magicdom.root.dom.innerHTML);
- });
+ //rep.alltext = Changeset.applyToText(changes, rep.alltext);
+ Changeset.mutateAttributionLines(changes, rep.alines, rep.apool);
- var _nextId = 1;
-
- function uniqueId(n)
- {
- // not actually guaranteed to be unique, e.g. if user copy-pastes
- // nodes with ids
- var nid = n.id;
- if (nid) return nid;
- return (n.id = "magicdomid" + (_nextId++));
- }
-
-
- function recolorLinesInRange(startChar, endChar, isTimeUp, optModFunc)
- {
- if (endChar <= startChar) return;
- if (startChar < 0 || startChar >= rep.lines.totalWidth()) return;
- var lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary
- var lineStart = rep.lines.offsetOfEntry(lineEntry);
- var lineIndex = rep.lines.indexOfEntry(lineEntry);
- var selectionNeedsResetting = false;
- var firstLine = null;
- var lastLine = null;
- isTimeUp = (isTimeUp || noop);
-
- // tokenFunc function; accesses current value of lineEntry and curDocChar,
- // also mutates curDocChar
- var curDocChar;
- var tokenFunc = function(tokenText, tokenClass)
+ if (changesetTracker.isTracking())
{
- lineEntry.domInfo.appendSpan(tokenText, tokenClass);
- };
- if (optModFunc)
- {
- var f = tokenFunc;
- tokenFunc = function(tokenText, tokenClass)
- {
- optModFunc(tokenText, tokenClass, f, curDocChar);
- curDocChar += tokenText.length;
- };
- }
-
- while (lineEntry && lineStart < endChar && !isTimeUp())
- {
- //var timer = newTimeLimit(200);
- var lineEnd = lineStart + lineEntry.width;
-
- curDocChar = lineStart;
- lineEntry.domInfo.clearSpans();
- getSpansForLine(lineEntry, tokenFunc, lineStart);
- lineEntry.domInfo.finishUpdate();
-
- markNodeClean(lineEntry.lineNode);
-
- if (rep.selStart && rep.selStart[0] == lineIndex || rep.selEnd && rep.selEnd[0] == lineIndex)
- {
- selectionNeedsResetting = true;
+ changesetTracker.composeUserChangeset(changes);
}
- //if (timer()) console.dirxml(lineEntry.lineNode.dom);
- if (firstLine === null) firstLine = lineIndex;
- lastLine = lineIndex;
- lineStart = lineEnd;
- lineEntry = rep.lines.next(lineEntry);
- lineIndex++;
}
- if (selectionNeedsResetting)
- {
- currentCallStack.selectionAffected = true;
- }
- //console.debug("Recolored line range %d-%d", firstLine, lastLine);
- }
- // like getSpansForRange, but for a line, and the func takes (text,class)
- // instead of (width,class); excludes the trailing '\n' from
- // consideration by func
-
-
- function getSpansForLine(lineEntry, textAndClassFunc, lineEntryOffsetHint)
- {
- var lineEntryOffset = lineEntryOffsetHint;
- if ((typeof lineEntryOffset) != "number")
+ /*
+ Converts the position of a char (index in String) into a [row, col] tuple
+ */
+ function lineAndColumnFromChar(x)
{
- lineEntryOffset = rep.lines.offsetOfEntry(lineEntry);
- }
- var text = lineEntry.text;
- var width = lineEntry.width; // text.length+1
- if (text.length === 0)
- {
- // allow getLineStyleFilter to set line-div styles
- var func = linestylefilter.getLineStyleFilter(
- 0, '', textAndClassFunc, rep.apool);
- func('', '');
- }
- else
- {
- var offsetIntoLine = 0;
- var filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser);
+ var lineEntry = rep.lines.atOffset(x);
+ var lineStart = rep.lines.offsetOfEntry(lineEntry);
var lineNum = rep.lines.indexOfEntry(lineEntry);
- var aline = rep.alines[lineNum];
- filteredFunc = linestylefilter.getLineStyleFilter(
- text.length, aline, filteredFunc, rep.apool);
- filteredFunc(text, '');
+ return [lineNum, x - lineStart];
}
- }
- var observedChanges;
-
- function clearObservedChanges()
- {
- observedChanges = {
- cleanNodesNearChanges: {}
- };
- }
- clearObservedChanges();
-
- function getCleanNodeByKey(key)
- {
- var p = PROFILER("getCleanNodeByKey", false);
- p.extra = 0;
- var n = doc.getElementById(key);
- // copying and pasting can lead to duplicate ids
- while (n && isNodeDirty(n))
+ function performDocumentReplaceCharRange(startChar, endChar, newText)
{
- p.extra++;
- n.id = "";
- n = doc.getElementById(key);
- }
- p.literal(p.extra, "extra");
- p.end();
- return n;
- }
-
- function observeChangesAroundNode(node)
- {
- // Around this top-level DOM node, look for changes to the document
- // (from how it looks in our representation) and record them in a way
- // that can be used to "normalize" the document (apply the changes to our
- // representation, and put the DOM in a canonical form).
- // top.console.log("observeChangesAroundNode(%o)", node);
- var cleanNode;
- var hasAdjacentDirtyness;
- if (!isNodeDirty(node))
- {
- cleanNode = node;
- var prevSib = cleanNode.previousSibling;
- var nextSib = cleanNode.nextSibling;
- hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) || (nextSib && isNodeDirty(nextSib)));
- }
- else
- {
- // node is dirty, look for clean node above
- var upNode = node.previousSibling;
- while (upNode && isNodeDirty(upNode))
+ if (startChar == endChar && newText.length === 0)
{
- upNode = upNode.previousSibling;
- }
- if (upNode)
- {
- cleanNode = upNode;
- }
- else
- {
- var downNode = node.nextSibling;
- while (downNode && isNodeDirty(downNode))
- {
- downNode = downNode.nextSibling;
- }
- if (downNode)
- {
- cleanNode = downNode;
- }
- }
- if (!cleanNode)
- {
- // Couldn't find any adjacent clean nodes!
- // Since top and bottom of doc is dirty, the dirty area will be detected.
return;
}
- hasAdjacentDirtyness = true;
- }
-
- if (hasAdjacentDirtyness)
- {
- // previous or next line is dirty
- observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true;
- }
- else
- {
- // next and prev lines are clean (if they exist)
- var lineKey = uniqueId(cleanNode);
- var prevSib = cleanNode.previousSibling;
- var nextSib = cleanNode.nextSibling;
- var actualPrevKey = ((prevSib && uniqueId(prevSib)) || null);
- var actualNextKey = ((nextSib && uniqueId(nextSib)) || null);
- var repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey));
- var repNextEntry = rep.lines.next(rep.lines.atKey(lineKey));
- var repPrevKey = ((repPrevEntry && repPrevEntry.key) || null);
- var repNextKey = ((repNextEntry && repNextEntry.key) || null);
- if (actualPrevKey != repPrevKey || actualNextKey != repNextKey)
+ // Requires that the replacement preserve the property that the
+ // internal document text ends in a newline. Given this, we
+ // rewrite the splice so that it doesn't touch the very last
+ // char of the document.
+ if (endChar == rep.alltext.length)
{
- observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true;
- }
- }
- }
-
- function observeChangesAroundSelection()
- {
- if (currentCallStack.observedSelection) return;
- currentCallStack.observedSelection = true;
-
- var p = PROFILER("getSelection", false);
- var selection = getSelection();
- p.end();
-
- function topLevel(n)
- {
- if ((!n) || n == root) return null;
- while (n.parentNode != root)
- {
- n = n.parentNode;
- }
- return n;
- }
-
- if (selection)
- {
- var node1 = topLevel(selection.startPoint.node);
- var node2 = topLevel(selection.endPoint.node);
- if (node1) observeChangesAroundNode(node1);
- if (node2 && node1 != node2)
- {
- observeChangesAroundNode(node2);
- }
- }
- }
-
- function observeSuspiciousNodes()
- {
- // inspired by Firefox bug #473255, where pasting formatted text
- // causes the cursor to jump away, making the new HTML never found.
- if (root.getElementsByTagName)
- {
- var nds = root.getElementsByTagName("style");
- for (var i = 0; i < nds.length; i++)
- {
- var n = nds[i];
- while (n.parentNode && n.parentNode != root)
+ if (startChar == endChar)
{
- n = n.parentNode;
+ // an insert at end
+ startChar--;
+ endChar--;
+ newText = '\n' + newText.substring(0, newText.length - 1);
}
- if (n.parentNode == root)
+ else if (newText.length === 0)
{
- observeChangesAroundNode(n);
+ // a delete at end
+ startChar--;
+ endChar--;
+ }
+ else
+ {
+ // a replace at end
+ endChar--;
+ newText = newText.substring(0, newText.length - 1);
}
}
- }
- }
-
- function incorporateUserChanges(isTimeUp)
- {
-
- if (currentCallStack.domClean) return false;
-
- currentCallStack.isUserChange = true;
-
- isTimeUp = (isTimeUp ||
- function()
- {
- return false;
- });
-
- if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false;
-
- var p = PROFILER("incorp", false);
-
- //if (doc.body.innerHTML.indexOf("AppJet") >= 0)
- //dmesg(htmlPrettyEscape(doc.body.innerHTML));
- //if (top.RECORD) top.RECORD.push(doc.body.innerHTML);
- // returns true if dom changes were made
- if (!root.firstChild)
- {
- root.innerHTML = "";
+ performDocumentReplaceRange(lineAndColumnFromChar(startChar), lineAndColumnFromChar(endChar), newText);
}
- p.mark("obs");
- observeChangesAroundSelection();
- observeSuspiciousNodes();
- p.mark("dirty");
- var dirtyRanges = getDirtyRanges();
- //console.log("dirtyRanges: "+toSource(dirtyRanges));
- var dirtyRangesCheckOut = true;
- var j = 0;
- var a, b;
- while (j < dirtyRanges.length)
+ function performDocumentReplaceRange(start, end, newText)
{
- a = dirtyRanges[j][0];
- b = dirtyRanges[j][1];
- if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key))))
+ if (start === undefined) start = rep.selStart;
+ if (end === undefined) end = rep.selEnd;
+
+ //dmesg(String([start.toSource(),end.toSource(),newText.toSource()]));
+ // start[0]: <--- start[1] --->CCCCCCCCCCC\n
+ // CCCCCCCCCCCCCCCCCCCC\n
+ // CCCC\n
+ // end[0]: -------\n
+ var builder = Changeset.builder(rep.lines.totalWidth());
+ ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
+ ChangesetUtils.buildRemoveRange(rep, builder, start, end);
+ builder.insert(newText, [
+ ['author', thisAuthor]
+ ], rep.apool);
+ var cs = builder.toString();
+
+ performDocumentApplyChangeset(cs);
+ }
+
+ function performDocumentApplyAttributesToCharRange(start, end, attribs)
+ {
+ end = Math.min(end, rep.alltext.length - 1);
+ documentAttributeManager.setAttributesOnRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs);
+ }
+ editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange;
+
+
+ function setAttributeOnSelection(attributeName, attributeValue)
+ {
+ if (!(rep.selStart && rep.selEnd)) return;
+
+ documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
+ [attributeName, attributeValue]
+ ]);
+ }
+ editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection;
+
+
+ function getAttributeOnSelection(attributeName){
+ if (!(rep.selStart && rep.selEnd)) return
+
+ var withIt = Changeset.makeAttribsString('+', [
+ [attributeName, 'true']
+ ], rep.apool);
+ var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)");
+ function hasIt(attribs)
{
- dirtyRangesCheckOut = false;
- break;
+ return withItRegex.test(attribs);
}
- j++;
- }
- if (!dirtyRangesCheckOut)
- {
- var numBodyNodes = root.childNodes.length;
- for (var k = 0; k < numBodyNodes; k++)
- {
- var bodyNode = root.childNodes.item(k);
- if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id))))
- {
- observeChangesAroundNode(bodyNode);
+
+ return rangeHasAttrib(rep.selStart, rep.selEnd)
+
+ function rangeHasAttrib(selStart, selEnd) {
+ // if range is collapsed -> no attribs in range
+ if(selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false
+
+ if(selStart[0] != selEnd[0]) { // -> More than one line selected
+ var hasAttrib = true
+
+ // from selStart to the end of the first line
+ hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length])
+
+ // for all lines in between
+ for(var n=selStart[0]+1; n < selEnd[0]; n++) {
+ hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length])
+ }
+
+ // for the last, potentially partial, line
+ hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]])
+
+ return hasAttrib
}
- }
- dirtyRanges = getDirtyRanges();
- }
- clearObservedChanges();
+ // Logic tells us we now have a range on a single line
- p.mark("getsel");
- var selection = getSelection();
+ var lineNum = selStart[0]
+ , start = selStart[1]
+ , end = selEnd[1]
+ , hasAttrib = true
- //console.log(magicdom.root.dom.innerHTML);
- //console.log("got selection: %o", selection);
- var selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection
- var i = 0;
- var splicesToDo = [];
- var netNumLinesChangeSoFar = 0;
- var toDeleteAtEnd = [];
- p.mark("ranges");
- p.literal(dirtyRanges.length, "numdirt");
- var domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]]
- while (i < dirtyRanges.length)
- {
- var range = dirtyRanges[i];
- a = range[0];
- b = range[1];
- var firstDirtyNode = (((a === 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling);
- firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode);
- var lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling);
- lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
- if (firstDirtyNode && lastDirtyNode)
- {
- var cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author);
- cc.notifySelection(selection);
- var dirtyNodes = [];
- for (var n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode);
- n = n.nextSibling)
- {
- if (browser.msie)
- {
- // try to undo IE's pesky and overzealous linkification
- try
- {
- n.createTextRange().execCommand("unlink", false, null);
+ // Iterate over attribs on this line
+
+ var opIter = Changeset.opIterator(rep.alines[lineNum])
+ , indexIntoLine = 0
+
+ while (opIter.hasNext()) {
+ var op = opIter.next();
+ var opStartInLine = indexIntoLine;
+ var opEndInLine = opStartInLine + op.chars;
+ if (!hasIt(op.attribs)) {
+ // does op overlap selection?
+ if (!(opEndInLine <= start || opStartInLine >= end)) {
+ hasAttrib = false; // since it's overlapping but hasn't got the attrib -> range hasn't got it
+ break;
}
- catch (e)
- {}
}
- cc.collectContent(n);
- dirtyNodes.push(n);
- }
- cc.notifyNextNode(lastDirtyNode.nextSibling);
- var lines = cc.getLines();
- if ((lines.length <= 1 || lines[lines.length - 1] !== "") && lastDirtyNode.nextSibling)
- {
- // dirty region doesn't currently end a line, even taking the following node
- // (or lack of node) into account, so include the following clean node.
- // It could be SPAN or a DIV; basically this is any case where the contentCollector
- // decides it isn't done.
- // Note that this clean node might need to be there for the next dirty range.
- //console.log("inclusive of "+lastDirtyNode.next().dom.tagName);
- b++;
- var cleanLine = lastDirtyNode.nextSibling;
- cc.collectContent(cleanLine);
- toDeleteAtEnd.push(cleanLine);
- cc.notifyNextNode(cleanLine.nextSibling);
+ indexIntoLine = opEndInLine;
}
- var ccData = cc.finish();
- var ss = ccData.selStart;
- var se = ccData.selEnd;
- lines = ccData.lines;
- var lineAttribs = ccData.lineAttribs;
- var linesWrapped = ccData.linesWrapped;
- var scrollToTheLeftNeeded = false;
-
- if (linesWrapped > 0)
- {
- if(!browser.msie){
- // chrome decides in it's infinite wisdom that its okay to put the browsers visisble window in the middle of the span
- // an outcome of this is that the first chars of the string are no longer visible to the user.. Yay chrome..
- // Move the browsers visible area to the left hand side of the span
- // Firefox isn't quite so bad, but it's still pretty quirky.
- var scrollToTheLeftNeeded = true;
- }
- // console.log("Editor warning: " + linesWrapped + " long line" + (linesWrapped == 1 ? " was" : "s were") + " hard-wrapped into " + ccData.numLinesAfter + " lines.");
- }
-
- if (ss[0] >= 0) selStart = [ss[0] + a + netNumLinesChangeSoFar, ss[1]];
- if (se[0] >= 0) selEnd = [se[0] + a + netNumLinesChangeSoFar, se[1]];
-
- var entries = [];
- var nodeToAddAfter = lastDirtyNode;
- var lineNodeInfos = new Array(lines.length);
- for (var k = 0; k < lines.length; k++)
- {
- var lineString = lines[k];
- var newEntry = createDomLineEntry(lineString);
- entries.push(newEntry);
- lineNodeInfos[k] = newEntry.domInfo;
- }
- //var fragment = magicdom.wrapDom(document.createDocumentFragment());
- domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]);
- _.each(dirtyNodes,function(n){
- toDeleteAtEnd.push(n);
- });
- var spliceHints = {};
- if (selStart) spliceHints.selStart = selStart;
- if (selEnd) spliceHints.selEnd = selEnd;
- splicesToDo.push([a + netNumLinesChangeSoFar, b - a, entries, lineAttribs, spliceHints]);
- netNumLinesChangeSoFar += (lines.length - (b - a));
+ return hasAttrib
}
- else if (b > a)
+ }
+
+ editorInfo.ace_getAttributeOnSelection = getAttributeOnSelection;
+
+ function toggleAttributeOnSelection(attributeName)
+ {
+ if (!(rep.selStart && rep.selEnd)) return;
+
+ var selectionAllHasIt = true;
+ var withIt = Changeset.makeAttribsString('+', [
+ [attributeName, 'true']
+ ], rep.apool);
+ var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)");
+
+ function hasIt(attribs)
{
- splicesToDo.push([a + netNumLinesChangeSoFar, b - a, [],
- []
+ return withItRegex.test(attribs);
+ }
+
+ var selStartLine = rep.selStart[0];
+ var selEndLine = rep.selEnd[0];
+ for (var n = selStartLine; n <= selEndLine; n++)
+ {
+ var opIter = Changeset.opIterator(rep.alines[n]);
+ var indexIntoLine = 0;
+ var selectionStartInLine = 0;
+ var selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline
+ if (n == selStartLine)
+ {
+ selectionStartInLine = rep.selStart[1];
+ }
+ if (n == selEndLine)
+ {
+ selectionEndInLine = rep.selEnd[1];
+ }
+ while (opIter.hasNext())
+ {
+ var op = opIter.next();
+ var opStartInLine = indexIntoLine;
+ var opEndInLine = opStartInLine + op.chars;
+ if (!hasIt(op.attribs))
+ {
+ // does op overlap selection?
+ if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine))
+ {
+ selectionAllHasIt = false;
+ break;
+ }
+ }
+ indexIntoLine = opEndInLine;
+ }
+ if (!selectionAllHasIt)
+ {
+ break;
+ }
+ }
+
+ if (selectionAllHasIt)
+ {
+ documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
+ [attributeName, '']
]);
}
- i++;
- }
-
- var domChanges = (splicesToDo.length > 0);
-
- // update the representation
- p.mark("splice");
- _.each(splicesToDo, function(splice)
- {
- doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]);
- });
-
- //p.mark("relex");
- //rep.lexer.lexCharRange(getVisibleCharRange(), function() { return false; });
- //var isTimeUp = newTimeLimit(100);
- // do DOM inserts
- p.mark("insert");
- _.each(domInsertsNeeded,function(ins)
- {
- insertDomLines(ins[0], ins[1], isTimeUp);
- });
-
- p.mark("del");
- // delete old dom nodes
- _.each(toDeleteAtEnd,function(n)
- {
- //var id = n.uniqueId();
- // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf)
- if(n.parentNode) n.parentNode.removeChild(n);
-
- //dmesg(htmlPrettyEscape(htmlForRemovedChild(n)));
- //console.log("removed: "+id);
- });
-
- if(scrollToTheLeftNeeded){ // needed to stop chrome from breaking the ui when long strings without spaces are pasted
- $("#innerdocbody").scrollLeft(0);
- }
-
- p.mark("findsel");
- // if the nodes that define the selection weren't encountered during
- // content collection, figure out where those nodes are now.
- if (selection && !selStart)
- {
- //if (domChanges) dmesg("selection not collected");
- var selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', {
- callstack: currentCallStack,
- editorInfo: editorInfo,
- rep: rep,
- root:root,
- point:selection.startPoint,
- documentAttributeManager: documentAttributeManager
- });
- selStart = (selStartFromHook==null||selStartFromHook.length==0)?getLineAndCharForPoint(selection.startPoint):selStartFromHook;
- }
- if (selection && !selEnd)
- {
- var selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', {
- callstack: currentCallStack,
- editorInfo: editorInfo,
- rep: rep,
- root:root,
- point:selection.endPoint,
- documentAttributeManager: documentAttributeManager
- });
- selEnd = (selEndFromHook==null||selEndFromHook.length==0)?getLineAndCharForPoint(selection.endPoint):selEndFromHook;
- }
-
- // selection from content collection can, in various ways, extend past final
- // BR in firefox DOM, so cap the line
- var numLines = rep.lines.length();
- if (selStart && selStart[0] >= numLines)
- {
- selStart[0] = numLines - 1;
- selStart[1] = rep.lines.atIndex(selStart[0]).text.length;
- }
- if (selEnd && selEnd[0] >= numLines)
- {
- selEnd[0] = numLines - 1;
- selEnd[1] = rep.lines.atIndex(selEnd[0]).text.length;
- }
-
- p.mark("repsel");
- // update rep if we have a new selection
- // NOTE: IE loses the selection when you click stuff in e.g. the
- // editbar, so removing the selection when it's lost is not a good
- // idea.
- if (selection) repSelectionChange(selStart, selEnd, selection && selection.focusAtStart);
- // update browser selection
- p.mark("browsel");
- if (selection && (domChanges || isCaret()))
- {
- // if no DOM changes (not this case), want to treat range selection delicately,
- // e.g. in IE not lose which end of the selection is the focus/anchor;
- // on the other hand, we may have just noticed a press of PageUp/PageDown
- currentCallStack.selectionAffected = true;
- }
-
- currentCallStack.domClean = true;
-
- p.mark("fixview");
-
- fixView();
-
- p.end("END");
-
- return domChanges;
- }
-
- var STYLE_ATTRIBS = {
- bold: true,
- italic: true,
- underline: true,
- strikethrough: true,
- list: true
- };
- var OTHER_INCORPED_ATTRIBS = {
- insertorder: true,
- author: true
- };
-
- function isStyleAttribute(aname)
- {
- return !!STYLE_ATTRIBS[aname];
- }
-
- function isIncorpedAttribute(aname)
- {
- return ( !! STYLE_ATTRIBS[aname]) || ( !! OTHER_INCORPED_ATTRIBS[aname]);
- }
-
- function insertDomLines(nodeToAddAfter, infoStructs, isTimeUp)
- {
- isTimeUp = (isTimeUp ||
- function()
- {
- return false;
- });
-
- var lastEntry;
- var lineStartOffset;
- if (infoStructs.length < 1) return;
- var startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node));
- var endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node));
- var charStart = rep.lines.offsetOfEntry(startEntry);
- var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width;
-
- //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp);
- _.each(infoStructs, function(info)
- {
- var p2 = PROFILER("insertLine", false);
- var node = info.node;
- var key = uniqueId(node);
- var entry;
- p2.mark("findEntry");
- if (lastEntry)
- {
- // optimization to avoid recalculation
- var next = rep.lines.next(lastEntry);
- if (next && next.key == key)
- {
- entry = next;
- lineStartOffset += lastEntry.width;
- }
- }
- if (!entry)
- {
- p2.literal(1, "nonopt");
- entry = rep.lines.atKey(key);
- lineStartOffset = rep.lines.offsetOfKey(key);
- }
- else p2.literal(0, "nonopt");
- lastEntry = entry;
- p2.mark("spans");
- getSpansForLine(entry, function(tokenText, tokenClass)
- {
- info.appendSpan(tokenText, tokenClass);
- }, lineStartOffset, isTimeUp());
- //else if (entry.text.length > 0) {
- //info.appendSpan(entry.text, 'dirty');
- //}
- p2.mark("addLine");
- info.prepareForAdd();
- entry.lineMarker = info.lineMarker;
- if (!nodeToAddAfter)
- {
- root.insertBefore(node, root.firstChild);
- }
else
{
- root.insertBefore(node, nodeToAddAfter.nextSibling);
+ documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
+ [attributeName, 'true']
+ ]);
}
- nodeToAddAfter = node;
- info.notifyAdded();
- p2.mark("markClean");
- markNodeClean(node);
- p2.end();
- });
- }
-
- function isCaret()
- {
- return (rep.selStart && rep.selEnd && rep.selStart[0] == rep.selEnd[0] && rep.selStart[1] == rep.selEnd[1]);
- }
- editorInfo.ace_isCaret = isCaret;
-
- // prereq: isCaret()
-
-
- function caretLine()
- {
- 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
- if (isCaret() && caretColumn() === 0 && caretLine() > 0)
- {
- var lineNum = caretLine();
- var thisLine = rep.lines.atIndex(lineNum);
- var prevLine = rep.lines.prev(thisLine);
- var prevLineText = prevLine.text;
- var theIndent = /^ *(?:)/.exec(prevLineText)[0];
- if (/[\[\(\:\{]\s*$/.exec(prevLineText)) theIndent += THE_TAB;
- var cs = Changeset.builder(rep.lines.totalWidth()).keep(
- rep.lines.offsetOfIndex(lineNum), lineNum).insert(
- theIndent, [
- ['author', thisAuthor]
- ], rep.apool).toString();
- performDocumentApplyChangeset(cs);
- performSelectionChange([lineNum, theIndent.length], [lineNum, theIndent.length]);
}
- }
+ editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection;
- function getPointForLineAndChar(lineAndChar)
- {
- var line = lineAndChar[0];
- var charsLeft = lineAndChar[1];
- //console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key,
- //getCleanNodeByKey(rep.lines.atIndex(line).key));
- var lineEntry = rep.lines.atIndex(line);
- charsLeft -= lineEntry.lineMarker;
- if (charsLeft < 0)
+ function performDocumentReplaceSelection(newText)
{
- charsLeft = 0;
+ if (!(rep.selStart && rep.selEnd)) return;
+ performDocumentReplaceRange(rep.selStart, rep.selEnd, newText);
}
- var lineNode = lineEntry.lineNode;
- var n = lineNode;
- var after = false;
- if (charsLeft === 0)
- {
- var index = 0;
- if (browser.msie && parseInt(browser.version) >= 11) {
- browser.msie = false; // Temp fix to resolve enter and backspace issues..
- // Note that this makes MSIE behave like modern browsers..
- }
- if (browser.msie && line == (rep.lines.length() - 1) && lineNode.childNodes.length === 0)
+ // Change the abstract representation of the document to have a different set of lines.
+ // Must be called after rep.alltext is set.
+
+
+ function doRepLineSplice(startLine, deleteCount, newLineEntries)
+ {
+
+ _.each(newLineEntries, function(entry)
{
- // best to stay at end of last empty div in IE
- index = 1;
+ entry.width = entry.text.length + 1;
+ });
+
+ var startOldChar = rep.lines.offsetOfIndex(startLine);
+ var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount);
+
+ var oldRegionStart = rep.lines.offsetOfIndex(startLine);
+ var oldRegionEnd = rep.lines.offsetOfIndex(startLine + deleteCount);
+ rep.lines.splice(startLine, deleteCount, newLineEntries);
+ currentCallStack.docTextChanged = true;
+ currentCallStack.repChanged = true;
+ var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length);
+
+ var newText = _.map(newLineEntries, function(e)
+ {
+ return e.text + '\n';
+ }).join('');
+
+ rep.alltext = rep.alltext.substring(0, startOldChar) + newText + rep.alltext.substring(endOldChar, rep.alltext.length);
+
+ //var newTotalLength = rep.alltext.length;
+ //rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart,
+ //newRegionEnd - oldRegionStart);
+ }
+
+ function doIncorpLineSplice(startLine, deleteCount, newLineEntries, lineAttribs, hints)
+ {
+
+ var startOldChar = rep.lines.offsetOfIndex(startLine);
+ var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount);
+
+ var oldRegionStart = rep.lines.offsetOfIndex(startLine);
+
+ var selStartHintChar, selEndHintChar;
+ if (hints && hints.selStart)
+ {
+ selStartHintChar = rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - oldRegionStart;
}
- return {
- node: lineNode,
- index: index,
- maxIndex: 1
+ if (hints && hints.selEnd)
+ {
+ selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart;
+ }
+
+ var newText = _.map(newLineEntries, function(e)
+ {
+ return e.text + '\n';
+ }).join('');
+ var oldText = rep.alltext.substring(startOldChar, endOldChar);
+ var oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join('');
+ var newAttribs = lineAttribs.join('|1+1') + '|1+1'; // not valid in a changeset
+ var analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar);
+ var commonStart = analysis[0];
+ var commonEnd = analysis[1];
+ var shortOldText = oldText.substring(commonStart, oldText.length - commonEnd);
+ var shortNewText = newText.substring(commonStart, newText.length - commonEnd);
+ var spliceStart = startOldChar + commonStart;
+ var spliceEnd = endOldChar - commonEnd;
+ var shiftFinalNewlineToBeforeNewText = false;
+
+ // adjust the splice to not involve the final newline of the document;
+ // be very defensive
+ if (shortOldText.charAt(shortOldText.length - 1) == '\n' && shortNewText.charAt(shortNewText.length - 1) == '\n')
+ {
+ // replacing text that ends in newline with text that also ends in newline
+ // (still, after analysis, somehow)
+ shortOldText = shortOldText.slice(0, -1);
+ shortNewText = shortNewText.slice(0, -1);
+ spliceEnd--;
+ commonEnd++;
+ }
+ if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0)
+ {
+ // inserting after final newline, bad
+ spliceStart--;
+ spliceEnd--;
+ shortNewText = '\n' + shortNewText.slice(0, -1);
+ shiftFinalNewlineToBeforeNewText = true;
+ }
+ if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0)
+ {
+ // deletion at end of rep.alltext
+ if (rep.alltext.charAt(spliceStart - 1) == '\n')
+ {
+ // (if not then what the heck? it will definitely lead
+ // to a rep.alltext without a final newline)
+ spliceStart--;
+ spliceEnd--;
+ }
+ }
+
+ if (!(shortOldText.length === 0 && shortNewText.length === 0))
+ {
+ var oldDocText = rep.alltext;
+ var oldLen = oldDocText.length;
+
+ var spliceStartLine = rep.lines.indexOfOffset(spliceStart);
+ var spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
+
+ var startBuilder = function()
+ {
+ var builder = Changeset.builder(oldLen);
+ builder.keep(spliceStartLineStart, spliceStartLine);
+ builder.keep(spliceStart - spliceStartLineStart);
+ return builder;
+ };
+
+ var eachAttribRun = function(attribs, func /*(startInNewText, endInNewText, attribs)*/ )
+ {
+ var attribsIter = Changeset.opIterator(attribs);
+ var textIndex = 0;
+ var newTextStart = commonStart;
+ var newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
+ while (attribsIter.hasNext())
+ {
+ var op = attribsIter.next();
+ var nextIndex = textIndex + op.chars;
+ if (!(nextIndex <= newTextStart || textIndex >= newTextEnd))
+ {
+ func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
+ }
+ textIndex = nextIndex;
+ }
+ };
+
+ var justApplyStyles = (shortNewText == shortOldText);
+ var theChangeset;
+
+ if (justApplyStyles)
+ {
+ // create changeset that clears the incorporated styles on
+ // the existing text. we compose this with the
+ // changeset the applies the styles found in the DOM.
+ // This allows us to incorporate, e.g., Safari's native "unbold".
+ var incorpedAttribClearer = cachedStrFunc(function(oldAtts)
+ {
+ return Changeset.mapAttribNumbers(oldAtts, function(n)
+ {
+ var k = rep.apool.getAttribKey(n);
+ if (isStyleAttribute(k))
+ {
+ return rep.apool.putAttrib([k, '']);
+ }
+ return false;
+ });
+ });
+
+ var builder1 = startBuilder();
+ if (shiftFinalNewlineToBeforeNewText)
+ {
+ builder1.keep(1, 1);
+ }
+ eachAttribRun(oldAttribs, function(start, end, attribs)
+ {
+ builder1.keepText(newText.substring(start, end), incorpedAttribClearer(attribs));
+ });
+ var clearer = builder1.toString();
+
+ var builder2 = startBuilder();
+ if (shiftFinalNewlineToBeforeNewText)
+ {
+ builder2.keep(1, 1);
+ }
+ eachAttribRun(newAttribs, function(start, end, attribs)
+ {
+ builder2.keepText(newText.substring(start, end), attribs);
+ });
+ var styler = builder2.toString();
+
+ theChangeset = Changeset.compose(clearer, styler, rep.apool);
+ }
+ else
+ {
+ var builder = startBuilder();
+
+ var spliceEndLine = rep.lines.indexOfOffset(spliceEnd);
+ var spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine);
+ if (spliceEndLineStart > spliceStart)
+ {
+ builder.remove(spliceEndLineStart - spliceStart, spliceEndLine - spliceStartLine);
+ builder.remove(spliceEnd - spliceEndLineStart);
+ }
+ else
+ {
+ builder.remove(spliceEnd - spliceStart);
+ }
+
+ var isNewTextMultiauthor = false;
+ var authorAtt = Changeset.makeAttribsString('+', (thisAuthor ? [
+ ['author', thisAuthor]
+ ] : []), rep.apool);
+ var authorizer = cachedStrFunc(function(oldAtts)
+ {
+ if (isNewTextMultiauthor)
+ {
+ // prefer colors from DOM
+ return Changeset.composeAttributes(authorAtt, oldAtts, true, rep.apool);
+ }
+ else
+ {
+ // use this author's color
+ return Changeset.composeAttributes(oldAtts, authorAtt, true, rep.apool);
+ }
+ });
+
+ var foundDomAuthor = '';
+ eachAttribRun(newAttribs, function(start, end, attribs)
+ {
+ var a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool);
+ if (a && a != foundDomAuthor)
+ {
+ if (!foundDomAuthor)
+ {
+ foundDomAuthor = a;
+ }
+ else
+ {
+ isNewTextMultiauthor = true; // multiple authors in DOM!
+ }
+ }
+ });
+
+ if (shiftFinalNewlineToBeforeNewText)
+ {
+ builder.insert('\n', authorizer(''));
+ }
+
+ eachAttribRun(newAttribs, function(start, end, attribs)
+ {
+ builder.insert(newText.substring(start, end), authorizer(attribs));
+ });
+ theChangeset = builder.toString();
+ }
+
+ //dmesg(htmlPrettyEscape(theChangeset));
+ doRepApplyChangeset(theChangeset);
+ }
+
+ // do this no matter what, because we need to get the right
+ // line keys into the rep.
+ doRepLineSplice(startLine, deleteCount, newLineEntries);
+
+ checkALines();
+ }
+
+ function cachedStrFunc(func)
+ {
+ var cache = {};
+ return function(s)
+ {
+ if (!cache[s])
+ {
+ cache[s] = func(s);
+ }
+ return cache[s];
};
}
- while (!(n == lineNode && after))
+
+ function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint)
{
- if (after)
+ function incorpedAttribFilter(anum)
{
- if (n.nextSibling)
- {
- n = n.nextSibling;
- after = false;
- }
- else n = n.parentNode;
+ return isStyleAttribute(rep.apool.getAttribKey(anum));
}
- else
+
+ function attribRuns(attribs)
{
- if (isNodeText(n))
+ var lengs = [];
+ var atts = [];
+ var iter = Changeset.opIterator(attribs);
+ while (iter.hasNext())
{
- var len = n.nodeValue.length;
- if (charsLeft <= len)
+ var op = iter.next();
+ lengs.push(op.chars);
+ atts.push(op.attribs);
+ }
+ return [lengs, atts];
+ }
+
+ function attribIterator(runs, backward)
+ {
+ var lengs = runs[0];
+ var atts = runs[1];
+ var i = (backward ? lengs.length - 1 : 0);
+ var j = 0;
+ return function next()
+ {
+ while (j >= lengs[i])
{
- return {
- node: n,
- index: charsLeft,
- maxIndex: len
- };
+ if (backward) i--;
+ else i++;
+ j = 0;
}
- charsLeft -= len;
- after = true;
+ var a = atts[i];
+ j++;
+ return a;
+ };
+ }
+
+ var oldLen = oldText.length;
+ var newLen = newText.length;
+ var minLen = Math.min(oldLen, newLen);
+
+ var oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter));
+ var newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter));
+
+ var commonStart = 0;
+ var oldStartIter = attribIterator(oldARuns, false);
+ var newStartIter = attribIterator(newARuns, false);
+ while (commonStart < minLen)
+ {
+ if (oldText.charAt(commonStart) == newText.charAt(commonStart) && oldStartIter() == newStartIter())
+ {
+ commonStart++;
+ }
+ else break;
+ }
+
+ var commonEnd = 0;
+ var oldEndIter = attribIterator(oldARuns, true);
+ var newEndIter = attribIterator(newARuns, true);
+ while (commonEnd < minLen)
+ {
+ if (commonEnd === 0)
+ {
+ // assume newline in common
+ oldEndIter();
+ newEndIter();
+ commonEnd++;
+ }
+ else if (oldText.charAt(oldLen - 1 - commonEnd) == newText.charAt(newLen - 1 - commonEnd) && oldEndIter() == newEndIter())
+ {
+ commonEnd++;
+ }
+ else break;
+ }
+
+ var hintedCommonEnd = -1;
+ if ((typeof optSelEndHint) == "number")
+ {
+ hintedCommonEnd = newLen - optSelEndHint;
+ }
+
+
+ if (commonStart + commonEnd > oldLen)
+ {
+ // ambiguous insertion
+ var minCommonEnd = oldLen - commonStart;
+ var maxCommonEnd = commonEnd;
+ if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd)
+ {
+ commonEnd = hintedCommonEnd;
}
else
{
- if (n.firstChild) n = n.firstChild;
- else after = true;
+ commonEnd = minCommonEnd;
}
+ commonStart = oldLen - commonEnd;
}
- }
- return {
- node: lineNode,
- index: 1,
- maxIndex: 1
- };
- }
-
- function nodeText(n)
- {
- return n.innerText || n.textContent || n.nodeValue || '';
- }
-
- function getLineAndCharForPoint(point)
- {
- // Turn DOM node selection into [line,char] selection.
- // This method has to work when the DOM is not pristine,
- // assuming the point is not in a dirty node.
- if (point.node == root)
- {
- if (point.index === 0)
+ if (commonStart + commonEnd > newLen)
{
- return [0, 0];
- }
- else
- {
- var N = rep.lines.length();
- var ln = rep.lines.atIndex(N - 1);
- return [N - 1, ln.text.length];
- }
- }
- else
- {
- var n = point.node;
- var col = 0;
- // if this part fails, it probably means the selection node
- // was dirty, and we didn't see it when collecting dirty nodes.
- if (isNodeText(n))
- {
- col = point.index;
- }
- else if (point.index > 0)
- {
- col = nodeText(n).length;
- }
- var parNode, prevSib;
- while ((parNode = n.parentNode) != root)
- {
- if ((prevSib = n.previousSibling))
+ // ambiguous deletion
+ var minCommonEnd = newLen - commonStart;
+ var maxCommonEnd = commonEnd;
+ if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd)
{
- n = prevSib;
- col += nodeText(n).length;
+ commonEnd = hintedCommonEnd;
}
else
{
- n = parNode;
+ commonEnd = minCommonEnd;
}
+ commonStart = newLen - commonEnd;
}
- if (n.id === "") console.debug("BAD");
- if (n.firstChild && isBlockElement(n.firstChild))
- {
- col += 1; // lineMarker
- }
- var lineEntry = rep.lines.atKey(n.id);
- var lineNum = rep.lines.indexOfEntry(lineEntry);
- return [lineNum, col];
- }
- }
- editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint;
- function createDomLineEntry(lineString)
- {
- var info = doCreateDomLine(lineString.length > 0);
- var newNode = info.node;
- return {
- key: uniqueId(newNode),
- text: lineString,
- lineNode: newNode,
- domInfo: info,
- lineMarker: 0
- };
- }
-
- function canApplyChangesetToDocument(changes)
- {
- return Changeset.oldLen(changes) == rep.alltext.length;
- }
-
- function performDocumentApplyChangeset(changes, insertsAfterSelection)
- {
- doRepApplyChangeset(changes, insertsAfterSelection);
-
- var requiredSelectionSetting = null;
- if (rep.selStart && rep.selEnd)
- {
- var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
- var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
- var result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
- requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
+ return [commonStart, commonEnd];
}
- var linesMutatee = {
- splice: function(start, numRemoved, newLinesVA)
- {
- var args = Array.prototype.slice.call(arguments, 2);
- domAndRepSplice(start, numRemoved, _.map(args, function(s){ return s.slice(0, -1); }), null);
- },
- get: function(i)
- {
- return rep.lines.atIndex(i).text + '\n';
- },
- length: function()
- {
- return rep.lines.length();
- },
- slice_notused: function(start, end)
- {
- return _.map(rep.lines.slice(start, end), function(e)
- {
- return e.text + '\n';
- });
- }
- };
-
- Changeset.mutateTextLines(changes, linesMutatee);
-
- checkALines();
-
- if (requiredSelectionSetting)
+ function equalLineAndChars(a, b)
{
- performSelectionChange(lineAndColumnFromChar(requiredSelectionSetting[0]), lineAndColumnFromChar(requiredSelectionSetting[1]), requiredSelectionSetting[2]);
+ if (!a) return !b;
+ if (!b) return !a;
+ return (a[0] == b[0] && a[1] == b[1]);
}
- function domAndRepSplice(startLine, deleteCount, newLineStrings, isTimeUp)
+ function performSelectionChange(selectStart, selectEnd, focusAtStart)
{
- // dgreensp 3/2009: the spliced lines may be in the middle of a dirty region,
- // so if no explicit time limit, don't spend a lot of time highlighting
- isTimeUp = (isTimeUp || newTimeLimit(50));
-
- var keysToDelete = [];
- if (deleteCount > 0)
- {
- var entryToDelete = rep.lines.atIndex(startLine);
- for (var i = 0; i < deleteCount; i++)
- {
- keysToDelete.push(entryToDelete.key);
- entryToDelete = rep.lines.next(entryToDelete);
- }
- }
-
- var lineEntries = _.map(newLineStrings, createDomLineEntry);
-
- doRepLineSplice(startLine, deleteCount, lineEntries);
-
- var nodeToAddAfter;
- if (startLine > 0)
- {
- nodeToAddAfter = getCleanNodeByKey(rep.lines.atIndex(startLine - 1).key);
- }
- else nodeToAddAfter = null;
-
- insertDomLines(nodeToAddAfter, _.map(lineEntries, function(entry)
- {
- return entry.domInfo;
- }), isTimeUp);
-
- _.each(keysToDelete, function(k)
- {
- var n = doc.getElementById(k);
- n.parentNode.removeChild(n);
- });
-
- if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine + deleteCount) || (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount))
+ if (repSelectionChange(selectStart, selectEnd, focusAtStart))
{
currentCallStack.selectionAffected = true;
}
}
- }
+ editorInfo.ace_performSelectionChange = performSelectionChange;
- function checkChangesetLineInformationAgainstRep(changes)
- {
- return true; // disable for speed
- var opIter = Changeset.opIterator(Changeset.unpack(changes).ops);
- var curOffset = 0;
- var curLine = 0;
- var curCol = 0;
- while (opIter.hasNext())
+ // Change the abstract representation of the document to have a different selection.
+ // Should not rely on the line representation. Should not affect the DOM.
+
+
+ function repSelectionChange(selectStart, selectEnd, focusAtStart)
{
- var o = opIter.next();
- if (o.opcode == '-' || o.opcode == '=')
+ focusAtStart = !! focusAtStart;
+
+ var newSelFocusAtStart = (focusAtStart && ((!selectStart) || (!selectEnd) || (selectStart[0] != selectEnd[0]) || (selectStart[1] != selectEnd[1])));
+
+ if ((!equalLineAndChars(rep.selStart, selectStart)) || (!equalLineAndChars(rep.selEnd, selectEnd)) || (rep.selFocusAtStart != newSelFocusAtStart))
{
- curOffset += o.chars;
- if (o.lines)
- {
- curLine += o.lines;
- curCol = 0;
- }
- else
- {
- curCol += o.chars;
- }
+ rep.selStart = selectStart;
+ rep.selEnd = selectEnd;
+ rep.selFocusAtStart = newSelFocusAtStart;
+ currentCallStack.repChanged = true;
+
+ return true;
+ //console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd,
+ //String(!!rep.selFocusAtStart));
}
- var calcLine = rep.lines.indexOfOffset(curOffset);
- var calcLineStart = rep.lines.offsetOfIndex(calcLine);
- var calcCol = curOffset - calcLineStart;
- if (calcCol != curCol || calcLine != curLine)
- {
- return false;
- }
- }
- return true;
- }
-
- function doRepApplyChangeset(changes, insertsAfterSelection)
- {
- Changeset.checkRep(changes);
-
- if (Changeset.oldLen(changes) != rep.alltext.length) throw new Error("doRepApplyChangeset length mismatch: " + Changeset.oldLen(changes) + "/" + rep.alltext.length);
-
- if (!checkChangesetLineInformationAgainstRep(changes))
- {
- throw new Error("doRepApplyChangeset line break mismatch");
+ return false;
+ //console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart);
}
- (function doRecordUndoInformation(changes)
+ function doCreateDomLine(nonEmpty)
{
- var editEvent = currentCallStack.editEvent;
- if (editEvent.eventType == "nonundoable")
+ if (browser.msie && (!nonEmpty))
{
- if (!editEvent.changeset)
+ var result = {
+ node: null,
+ appendSpan: noop,
+ prepareForAdd: noop,
+ notifyAdded: noop,
+ clearSpans: noop,
+ finishUpdate: noop,
+ lineMarker: 0
+ };
+
+ var lineElem = doc.createElement("div");
+ result.node = lineElem;
+
+ result.notifyAdded = function()
{
- editEvent.changeset = changes;
- }
- else
+ // magic -- settng an empty div's innerHTML to the empty string
+ // keeps it from collapsing. Apparently innerHTML must be set *after*
+ // adding the node to the DOM.
+ // Such a div is what IE 6 creates naturally when you make a blank line
+ // in a document of divs. However, when copy-and-pasted the div will
+ // contain a space, so we note its emptiness with a property.
+ lineElem.innerHTML = " "; // Frist we set a value that isnt blank
+ // a primitive-valued property survives copy-and-paste
+ setAssoc(lineElem, "shouldBeEmpty", true);
+ // an object property doesn't
+ setAssoc(lineElem, "unpasted", {});
+ lineElem.innerHTML = ""; // Then we make it blank.. New line and no space = Awesome :)
+ };
+ var lineClass = 'ace-line';
+ result.appendSpan = function(txt, cls)
{
- editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
- }
+ if ((!txt) && cls)
+ {
+ // gain a whole-line style (currently to show insertion point in CSS)
+ lineClass = domline.addToLineClass(lineClass, cls);
+ }
+ // otherwise, ignore appendSpan, this is an empty line
+ };
+ result.clearSpans = function()
+ {
+ lineClass = ''; // non-null to cause update
+ };
+
+ var writeClass = function()
+ {
+ if (lineClass !== null) lineElem.className = lineClass;
+ };
+
+ result.prepareForAdd = writeClass;
+ result.finishUpdate = writeClass;
+ result.getInnerHTML = function()
+ {
+ return "";
+ };
+ return result;
}
else
{
- var inverseChangeset = Changeset.inverse(changes, {
- get: function(i)
- {
- return rep.lines.atIndex(i).text + '\n';
- },
- length: function()
- {
- return rep.lines.length();
- }
- }, rep.alines, rep.apool);
-
- if (!editEvent.backset)
- {
- editEvent.backset = inverseChangeset;
- }
- else
- {
- editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool);
- }
- }
- })(changes);
-
- //rep.alltext = Changeset.applyToText(changes, rep.alltext);
- Changeset.mutateAttributionLines(changes, rep.alines, rep.apool);
-
- if (changesetTracker.isTracking())
- {
- changesetTracker.composeUserChangeset(changes);
- }
-
- }
-
- /*
- Converts the position of a char (index in String) into a [row, col] tuple
- */
- function lineAndColumnFromChar(x)
- {
- var lineEntry = rep.lines.atOffset(x);
- var lineStart = rep.lines.offsetOfEntry(lineEntry);
- var lineNum = rep.lines.indexOfEntry(lineEntry);
- return [lineNum, x - lineStart];
- }
-
- function performDocumentReplaceCharRange(startChar, endChar, newText)
- {
- if (startChar == endChar && newText.length === 0)
- {
- return;
- }
- // Requires that the replacement preserve the property that the
- // internal document text ends in a newline. Given this, we
- // rewrite the splice so that it doesn't touch the very last
- // char of the document.
- if (endChar == rep.alltext.length)
- {
- if (startChar == endChar)
- {
- // an insert at end
- startChar--;
- endChar--;
- newText = '\n' + newText.substring(0, newText.length - 1);
- }
- else if (newText.length === 0)
- {
- // a delete at end
- startChar--;
- endChar--;
- }
- else
- {
- // a replace at end
- endChar--;
- newText = newText.substring(0, newText.length - 1);
- }
- }
- performDocumentReplaceRange(lineAndColumnFromChar(startChar), lineAndColumnFromChar(endChar), newText);
- }
-
- function performDocumentReplaceRange(start, end, newText)
- {
- if (start === undefined) start = rep.selStart;
- if (end === undefined) end = rep.selEnd;
-
- //dmesg(String([start.toSource(),end.toSource(),newText.toSource()]));
- // start[0]: <--- start[1] --->CCCCCCCCCCC\n
- // CCCCCCCCCCCCCCCCCCCC\n
- // CCCC\n
- // end[0]: -------\n
- var builder = Changeset.builder(rep.lines.totalWidth());
- ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
- ChangesetUtils.buildRemoveRange(rep, builder, start, end);
- builder.insert(newText, [
- ['author', thisAuthor]
- ], rep.apool);
- var cs = builder.toString();
-
- performDocumentApplyChangeset(cs);
- }
-
- function performDocumentApplyAttributesToCharRange(start, end, attribs)
- {
- end = Math.min(end, rep.alltext.length - 1);
- documentAttributeManager.setAttributesOnRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs);
- }
- editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange;
-
-
- function setAttributeOnSelection(attributeName, attributeValue)
- {
- if (!(rep.selStart && rep.selEnd)) return;
-
- documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
- [attributeName, attributeValue]
- ]);
- }
- editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection;
-
-
- function getAttributeOnSelection(attributeName){
- if (!(rep.selStart && rep.selEnd)) return
-
- var withIt = Changeset.makeAttribsString('+', [
- [attributeName, 'true']
- ], rep.apool);
- var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)");
- function hasIt(attribs)
- {
- return withItRegex.test(attribs);
- }
-
- return rangeHasAttrib(rep.selStart, rep.selEnd)
-
- function rangeHasAttrib(selStart, selEnd) {
- // if range is collapsed -> no attribs in range
- if(selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false
-
- if(selStart[0] != selEnd[0]) { // -> More than one line selected
- var hasAttrib = true
-
- // from selStart to the end of the first line
- hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length])
-
- // for all lines in between
- for(var n=selStart[0]+1; n < selEnd[0]; n++) {
- hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length])
- }
-
- // for the last, potentially partial, line
- hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]])
-
- return hasAttrib
- }
-
- // Logic tells us we now have a range on a single line
-
- var lineNum = selStart[0]
- , start = selStart[1]
- , end = selEnd[1]
- , hasAttrib = true
-
- // Iterate over attribs on this line
-
- var opIter = Changeset.opIterator(rep.alines[lineNum])
- , indexIntoLine = 0
-
- while (opIter.hasNext()) {
- var op = opIter.next();
- var opStartInLine = indexIntoLine;
- var opEndInLine = opStartInLine + op.chars;
- if (!hasIt(op.attribs)) {
- // does op overlap selection?
- if (!(opEndInLine <= start || opStartInLine >= end)) {
- hasAttrib = false; // since it's overlapping but hasn't got the attrib -> range hasn't got it
- break;
- }
- }
- indexIntoLine = opEndInLine;
- }
-
- return hasAttrib
- }
- }
-
- editorInfo.ace_getAttributeOnSelection = getAttributeOnSelection;
-
- function toggleAttributeOnSelection(attributeName)
- {
- if (!(rep.selStart && rep.selEnd)) return;
-
- var selectionAllHasIt = true;
- var withIt = Changeset.makeAttribsString('+', [
- [attributeName, 'true']
- ], rep.apool);
- var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)");
-
- function hasIt(attribs)
- {
- return withItRegex.test(attribs);
- }
-
- var selStartLine = rep.selStart[0];
- var selEndLine = rep.selEnd[0];
- for (var n = selStartLine; n <= selEndLine; n++)
- {
- var opIter = Changeset.opIterator(rep.alines[n]);
- var indexIntoLine = 0;
- var selectionStartInLine = 0;
- var selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline
- if (n == selStartLine)
- {
- selectionStartInLine = rep.selStart[1];
- }
- if (n == selEndLine)
- {
- selectionEndInLine = rep.selEnd[1];
- }
- while (opIter.hasNext())
- {
- var op = opIter.next();
- var opStartInLine = indexIntoLine;
- var opEndInLine = opStartInLine + op.chars;
- if (!hasIt(op.attribs))
- {
- // does op overlap selection?
- if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine))
- {
- selectionAllHasIt = false;
- break;
- }
- }
- indexIntoLine = opEndInLine;
- }
- if (!selectionAllHasIt)
- {
- break;
+ return domline.createDomLine(nonEmpty, doesWrap, browser, doc);
}
}
- if (selectionAllHasIt)
+ function textify(str)
{
- documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
- [attributeName, '']
- ]);
+ return str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' ');
}
- else
- {
- documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
- [attributeName, 'true']
- ]);
- }
- }
- editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection;
- function performDocumentReplaceSelection(newText)
- {
- if (!(rep.selStart && rep.selEnd)) return;
- performDocumentReplaceRange(rep.selStart, rep.selEnd, newText);
- }
+ var _blockElems = {
+ "div": 1,
+ "p": 1,
+ "pre": 1,
+ "li": 1,
+ "ol": 1,
+ "ul": 1
+ };
- // Change the abstract representation of the document to have a different set of lines.
- // Must be called after rep.alltext is set.
-
-
- function doRepLineSplice(startLine, deleteCount, newLineEntries)
- {
-
- _.each(newLineEntries, function(entry)
- {
- entry.width = entry.text.length + 1;
+ _.each(hooks.callAll('aceRegisterBlockElements'), function(element){
+ _blockElems[element] = 1;
});
- var startOldChar = rep.lines.offsetOfIndex(startLine);
- var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount);
-
- var oldRegionStart = rep.lines.offsetOfIndex(startLine);
- var oldRegionEnd = rep.lines.offsetOfIndex(startLine + deleteCount);
- rep.lines.splice(startLine, deleteCount, newLineEntries);
- currentCallStack.docTextChanged = true;
- currentCallStack.repChanged = true;
- var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length);
-
- var newText = _.map(newLineEntries, function(e)
+ function isBlockElement(n)
{
- return e.text + '\n';
- }).join('');
-
- rep.alltext = rep.alltext.substring(0, startOldChar) + newText + rep.alltext.substring(endOldChar, rep.alltext.length);
-
- //var newTotalLength = rep.alltext.length;
- //rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart,
- //newRegionEnd - oldRegionStart);
- }
-
- function doIncorpLineSplice(startLine, deleteCount, newLineEntries, lineAttribs, hints)
- {
-
- var startOldChar = rep.lines.offsetOfIndex(startLine);
- var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount);
-
- var oldRegionStart = rep.lines.offsetOfIndex(startLine);
-
- var selStartHintChar, selEndHintChar;
- if (hints && hints.selStart)
- {
- selStartHintChar = rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - oldRegionStart;
- }
- if (hints && hints.selEnd)
- {
- selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart;
+ return !!_blockElems[(n.tagName || "").toLowerCase()];
}
- var newText = _.map(newLineEntries, function(e)
+ function getDirtyRanges()
{
- return e.text + '\n';
- }).join('');
- var oldText = rep.alltext.substring(startOldChar, endOldChar);
- var oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join('');
- var newAttribs = lineAttribs.join('|1+1') + '|1+1'; // not valid in a changeset
- var analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar);
- var commonStart = analysis[0];
- var commonEnd = analysis[1];
- var shortOldText = oldText.substring(commonStart, oldText.length - commonEnd);
- var shortNewText = newText.substring(commonStart, newText.length - commonEnd);
- var spliceStart = startOldChar + commonStart;
- var spliceEnd = endOldChar - commonEnd;
- var shiftFinalNewlineToBeforeNewText = false;
+ // based on observedChanges, return a list of ranges of original lines
+ // that need to be removed or replaced with new user content to incorporate
+ // the user's changes into the line representation. ranges may be zero-length,
+ // indicating inserted content. for example, [0,0] means content was inserted
+ // at the top of the document, while [3,4] means line 3 was deleted, modified,
+ // or replaced with one or more new lines of content. ranges do not touch.
+ var p = PROFILER("getDirtyRanges", false);
+ p.forIndices = 0;
+ p.consecutives = 0;
+ p.corrections = 0;
- // adjust the splice to not involve the final newline of the document;
- // be very defensive
- if (shortOldText.charAt(shortOldText.length - 1) == '\n' && shortNewText.charAt(shortNewText.length - 1) == '\n')
- {
- // replacing text that ends in newline with text that also ends in newline
- // (still, after analysis, somehow)
- shortOldText = shortOldText.slice(0, -1);
- shortNewText = shortNewText.slice(0, -1);
- spliceEnd--;
- commonEnd++;
- }
- if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0)
- {
- // inserting after final newline, bad
- spliceStart--;
- spliceEnd--;
- shortNewText = '\n' + shortNewText.slice(0, -1);
- shiftFinalNewlineToBeforeNewText = true;
- }
- if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0)
- {
- // deletion at end of rep.alltext
- if (rep.alltext.charAt(spliceStart - 1) == '\n')
+ var cleanNodeForIndexCache = {};
+ var N = rep.lines.length(); // old number of lines
+
+
+ function cleanNodeForIndex(i)
{
- // (if not then what the heck? it will definitely lead
- // to a rep.alltext without a final newline)
- spliceStart--;
- spliceEnd--;
- }
- }
-
- if (!(shortOldText.length === 0 && shortNewText.length === 0))
- {
- var oldDocText = rep.alltext;
- var oldLen = oldDocText.length;
-
- var spliceStartLine = rep.lines.indexOfOffset(spliceStart);
- var spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
-
- var startBuilder = function()
- {
- var builder = Changeset.builder(oldLen);
- builder.keep(spliceStartLineStart, spliceStartLine);
- builder.keep(spliceStart - spliceStartLineStart);
- return builder;
- };
-
- var eachAttribRun = function(attribs, func /*(startInNewText, endInNewText, attribs)*/ )
- {
- var attribsIter = Changeset.opIterator(attribs);
- var textIndex = 0;
- var newTextStart = commonStart;
- var newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
- while (attribsIter.hasNext())
+ // if line (i) in the un-updated line representation maps to a clean node
+ // in the document, return that node.
+ // if (i) is out of bounds, return true. else return false.
+ if (cleanNodeForIndexCache[i] === undefined)
{
- var op = attribsIter.next();
- var nextIndex = textIndex + op.chars;
- if (!(nextIndex <= newTextStart || textIndex >= newTextEnd))
+ p.forIndices++;
+ var result;
+ if (i < 0 || i >= N)
{
- func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
- }
- textIndex = nextIndex;
- }
- };
-
- var justApplyStyles = (shortNewText == shortOldText);
- var theChangeset;
-
- if (justApplyStyles)
- {
- // create changeset that clears the incorporated styles on
- // the existing text. we compose this with the
- // changeset the applies the styles found in the DOM.
- // This allows us to incorporate, e.g., Safari's native "unbold".
- var incorpedAttribClearer = cachedStrFunc(function(oldAtts)
- {
- return Changeset.mapAttribNumbers(oldAtts, function(n)
- {
- var k = rep.apool.getAttribKey(n);
- if (isStyleAttribute(k))
- {
- return rep.apool.putAttrib([k, '']);
- }
- return false;
- });
- });
-
- var builder1 = startBuilder();
- if (shiftFinalNewlineToBeforeNewText)
- {
- builder1.keep(1, 1);
- }
- eachAttribRun(oldAttribs, function(start, end, attribs)
- {
- builder1.keepText(newText.substring(start, end), incorpedAttribClearer(attribs));
- });
- var clearer = builder1.toString();
-
- var builder2 = startBuilder();
- if (shiftFinalNewlineToBeforeNewText)
- {
- builder2.keep(1, 1);
- }
- eachAttribRun(newAttribs, function(start, end, attribs)
- {
- builder2.keepText(newText.substring(start, end), attribs);
- });
- var styler = builder2.toString();
-
- theChangeset = Changeset.compose(clearer, styler, rep.apool);
- }
- else
- {
- var builder = startBuilder();
-
- var spliceEndLine = rep.lines.indexOfOffset(spliceEnd);
- var spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine);
- if (spliceEndLineStart > spliceStart)
- {
- builder.remove(spliceEndLineStart - spliceStart, spliceEndLine - spliceStartLine);
- builder.remove(spliceEnd - spliceEndLineStart);
- }
- else
- {
- builder.remove(spliceEnd - spliceStart);
- }
-
- var isNewTextMultiauthor = false;
- var authorAtt = Changeset.makeAttribsString('+', (thisAuthor ? [
- ['author', thisAuthor]
- ] : []), rep.apool);
- var authorizer = cachedStrFunc(function(oldAtts)
- {
- if (isNewTextMultiauthor)
- {
- // prefer colors from DOM
- return Changeset.composeAttributes(authorAtt, oldAtts, true, rep.apool);
+ result = true; // truthy, but no actual node
}
else
{
- // use this author's color
- return Changeset.composeAttributes(oldAtts, authorAtt, true, rep.apool);
+ var key = rep.lines.atIndex(i).key;
+ result = (getCleanNodeByKey(key) || false);
}
- });
+ cleanNodeForIndexCache[i] = result;
+ }
+ return cleanNodeForIndexCache[i];
+ }
+ var isConsecutiveCache = {};
- var foundDomAuthor = '';
- eachAttribRun(newAttribs, function(start, end, attribs)
+ function isConsecutive(i)
+ {
+ if (isConsecutiveCache[i] === undefined)
{
- var a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool);
- if (a && a != foundDomAuthor)
+ p.consecutives++;
+ isConsecutiveCache[i] = (function()
{
- if (!foundDomAuthor)
- {
- foundDomAuthor = a;
- }
- else
- {
- isNewTextMultiauthor = true; // multiple authors in DOM!
- }
- }
+ // returns whether line (i) and line (i-1), assumed to be map to clean DOM nodes,
+ // or document boundaries, are consecutive in the changed DOM
+ var a = cleanNodeForIndex(i - 1);
+ var b = cleanNodeForIndex(i);
+ if ((!a) || (!b)) return false; // violates precondition
+ if ((a === true) && (b === true)) return !root.firstChild;
+ if ((a === true) && b.previousSibling) return false;
+ if ((b === true) && a.nextSibling) return false;
+ if ((a === true) || (b === true)) return true;
+ return a.nextSibling == b;
+ })();
+ }
+ return isConsecutiveCache[i];
+ }
+
+ function isClean(i)
+ {
+ // returns whether line (i) in the un-updated representation maps to a clean node,
+ // or is outside the bounds of the document
+ return !!cleanNodeForIndex(i);
+ }
+ // list of pairs, each representing a range of lines that is clean and consecutive
+ // in the changed DOM. lines (-1) and (N) are always clean, but may or may not
+ // be consecutive with lines in the document. pairs are in sorted order.
+ var cleanRanges = [
+ [-1, N + 1]
+ ];
+
+ function rangeForLine(i)
+ {
+ // returns index of cleanRange containing i, or -1 if none
+ var answer = -1;
+ _.each(cleanRanges ,function(r, idx)
+ {
+ if (i >= r[1]) return false; // keep looking
+ if (i < r[0]) return true; // not found, stop looking
+ answer = idx;
+ return true; // found, stop looking
});
-
- if (shiftFinalNewlineToBeforeNewText)
- {
- builder.insert('\n', authorizer(''));
- }
-
- eachAttribRun(newAttribs, function(start, end, attribs)
- {
- builder.insert(newText.substring(start, end), authorizer(attribs));
- });
- theChangeset = builder.toString();
+ return answer;
}
- //dmesg(htmlPrettyEscape(theChangeset));
- doRepApplyChangeset(theChangeset);
- }
-
- // do this no matter what, because we need to get the right
- // line keys into the rep.
- doRepLineSplice(startLine, deleteCount, newLineEntries);
-
- checkALines();
- }
-
- function cachedStrFunc(func)
- {
- var cache = {};
- return function(s)
- {
- if (!cache[s])
+ function removeLineFromRange(rng, line)
{
- cache[s] = func(s);
- }
- return cache[s];
- };
- }
-
- function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint)
- {
- function incorpedAttribFilter(anum)
- {
- return isStyleAttribute(rep.apool.getAttribKey(anum));
- }
-
- function attribRuns(attribs)
- {
- var lengs = [];
- var atts = [];
- var iter = Changeset.opIterator(attribs);
- while (iter.hasNext())
- {
- var op = iter.next();
- lengs.push(op.chars);
- atts.push(op.attribs);
- }
- return [lengs, atts];
- }
-
- function attribIterator(runs, backward)
- {
- var lengs = runs[0];
- var atts = runs[1];
- var i = (backward ? lengs.length - 1 : 0);
- var j = 0;
- return function next()
- {
- while (j >= lengs[i])
- {
- if (backward) i--;
- else i++;
- j = 0;
- }
- var a = atts[i];
- j++;
- return a;
- };
- }
-
- var oldLen = oldText.length;
- var newLen = newText.length;
- var minLen = Math.min(oldLen, newLen);
-
- var oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter));
- var newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter));
-
- var commonStart = 0;
- var oldStartIter = attribIterator(oldARuns, false);
- var newStartIter = attribIterator(newARuns, false);
- while (commonStart < minLen)
- {
- if (oldText.charAt(commonStart) == newText.charAt(commonStart) && oldStartIter() == newStartIter())
- {
- commonStart++;
- }
- else break;
- }
-
- var commonEnd = 0;
- var oldEndIter = attribIterator(oldARuns, true);
- var newEndIter = attribIterator(newARuns, true);
- while (commonEnd < minLen)
- {
- if (commonEnd === 0)
- {
- // assume newline in common
- oldEndIter();
- newEndIter();
- commonEnd++;
- }
- else if (oldText.charAt(oldLen - 1 - commonEnd) == newText.charAt(newLen - 1 - commonEnd) && oldEndIter() == newEndIter())
- {
- commonEnd++;
- }
- else break;
- }
-
- var hintedCommonEnd = -1;
- if ((typeof optSelEndHint) == "number")
- {
- hintedCommonEnd = newLen - optSelEndHint;
- }
-
-
- if (commonStart + commonEnd > oldLen)
- {
- // ambiguous insertion
- var minCommonEnd = oldLen - commonStart;
- var maxCommonEnd = commonEnd;
- if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd)
- {
- commonEnd = hintedCommonEnd;
- }
- else
- {
- commonEnd = minCommonEnd;
- }
- commonStart = oldLen - commonEnd;
- }
- if (commonStart + commonEnd > newLen)
- {
- // ambiguous deletion
- var minCommonEnd = newLen - commonStart;
- var maxCommonEnd = commonEnd;
- if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd)
- {
- commonEnd = hintedCommonEnd;
- }
- else
- {
- commonEnd = minCommonEnd;
- }
- commonStart = newLen - commonEnd;
- }
-
- return [commonStart, commonEnd];
- }
-
- function equalLineAndChars(a, b)
- {
- if (!a) return !b;
- if (!b) return !a;
- return (a[0] == b[0] && a[1] == b[1]);
- }
-
- function performSelectionChange(selectStart, selectEnd, focusAtStart)
- {
- if (repSelectionChange(selectStart, selectEnd, focusAtStart))
- {
- currentCallStack.selectionAffected = true;
- }
- }
- editorInfo.ace_performSelectionChange = performSelectionChange;
-
- // Change the abstract representation of the document to have a different selection.
- // Should not rely on the line representation. Should not affect the DOM.
-
-
- function repSelectionChange(selectStart, selectEnd, focusAtStart)
- {
- focusAtStart = !! focusAtStart;
-
- var newSelFocusAtStart = (focusAtStart && ((!selectStart) || (!selectEnd) || (selectStart[0] != selectEnd[0]) || (selectStart[1] != selectEnd[1])));
-
- if ((!equalLineAndChars(rep.selStart, selectStart)) || (!equalLineAndChars(rep.selEnd, selectEnd)) || (rep.selFocusAtStart != newSelFocusAtStart))
- {
- rep.selStart = selectStart;
- rep.selEnd = selectEnd;
- rep.selFocusAtStart = newSelFocusAtStart;
- currentCallStack.repChanged = true;
-
- return true;
- //console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd,
- //String(!!rep.selFocusAtStart));
- }
- return false;
- //console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart);
- }
-
- function doCreateDomLine(nonEmpty)
- {
- if (browser.msie && (!nonEmpty))
- {
- var result = {
- node: null,
- appendSpan: noop,
- prepareForAdd: noop,
- notifyAdded: noop,
- clearSpans: noop,
- finishUpdate: noop,
- lineMarker: 0
- };
-
- var lineElem = doc.createElement("div");
- result.node = lineElem;
-
- result.notifyAdded = function()
- {
- // magic -- settng an empty div's innerHTML to the empty string
- // keeps it from collapsing. Apparently innerHTML must be set *after*
- // adding the node to the DOM.
- // Such a div is what IE 6 creates naturally when you make a blank line
- // in a document of divs. However, when copy-and-pasted the div will
- // contain a space, so we note its emptiness with a property.
- lineElem.innerHTML = " "; // Frist we set a value that isnt blank
- // a primitive-valued property survives copy-and-paste
- setAssoc(lineElem, "shouldBeEmpty", true);
- // an object property doesn't
- setAssoc(lineElem, "unpasted", {});
- lineElem.innerHTML = ""; // Then we make it blank.. New line and no space = Awesome :)
- };
- var lineClass = 'ace-line';
- result.appendSpan = function(txt, cls)
- {
- if ((!txt) && cls)
- {
- // gain a whole-line style (currently to show insertion point in CSS)
- lineClass = domline.addToLineClass(lineClass, cls);
- }
- // otherwise, ignore appendSpan, this is an empty line
- };
- result.clearSpans = function()
- {
- lineClass = ''; // non-null to cause update
- };
-
- var writeClass = function()
- {
- if (lineClass !== null) lineElem.className = lineClass;
- };
-
- result.prepareForAdd = writeClass;
- result.finishUpdate = writeClass;
- result.getInnerHTML = function()
- {
- return "";
- };
- return result;
- }
- else
- {
- return domline.createDomLine(nonEmpty, doesWrap, browser, doc);
- }
- }
-
- function textify(str)
- {
- return str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' ');
- }
-
- var _blockElems = {
- "div": 1,
- "p": 1,
- "pre": 1,
- "li": 1,
- "ol": 1,
- "ul": 1
- };
-
- _.each(hooks.callAll('aceRegisterBlockElements'), function(element){
- _blockElems[element] = 1;
- });
-
- function isBlockElement(n)
- {
- return !!_blockElems[(n.tagName || "").toLowerCase()];
- }
-
- function getDirtyRanges()
- {
- // based on observedChanges, return a list of ranges of original lines
- // that need to be removed or replaced with new user content to incorporate
- // the user's changes into the line representation. ranges may be zero-length,
- // indicating inserted content. for example, [0,0] means content was inserted
- // at the top of the document, while [3,4] means line 3 was deleted, modified,
- // or replaced with one or more new lines of content. ranges do not touch.
- var p = PROFILER("getDirtyRanges", false);
- p.forIndices = 0;
- p.consecutives = 0;
- p.corrections = 0;
-
- var cleanNodeForIndexCache = {};
- var N = rep.lines.length(); // old number of lines
-
-
- function cleanNodeForIndex(i)
- {
- // if line (i) in the un-updated line representation maps to a clean node
- // in the document, return that node.
- // if (i) is out of bounds, return true. else return false.
- if (cleanNodeForIndexCache[i] === undefined)
- {
- p.forIndices++;
- var result;
- if (i < 0 || i >= N)
- {
- result = true; // truthy, but no actual node
- }
- else
- {
- var key = rep.lines.atIndex(i).key;
- result = (getCleanNodeByKey(key) || false);
- }
- cleanNodeForIndexCache[i] = result;
- }
- return cleanNodeForIndexCache[i];
- }
- var isConsecutiveCache = {};
-
- function isConsecutive(i)
- {
- if (isConsecutiveCache[i] === undefined)
- {
- p.consecutives++;
- isConsecutiveCache[i] = (function()
- {
- // returns whether line (i) and line (i-1), assumed to be map to clean DOM nodes,
- // or document boundaries, are consecutive in the changed DOM
- var a = cleanNodeForIndex(i - 1);
- var b = cleanNodeForIndex(i);
- if ((!a) || (!b)) return false; // violates precondition
- if ((a === true) && (b === true)) return !root.firstChild;
- if ((a === true) && b.previousSibling) return false;
- if ((b === true) && a.nextSibling) return false;
- if ((a === true) || (b === true)) return true;
- return a.nextSibling == b;
- })();
- }
- return isConsecutiveCache[i];
- }
-
- function isClean(i)
- {
- // returns whether line (i) in the un-updated representation maps to a clean node,
- // or is outside the bounds of the document
- return !!cleanNodeForIndex(i);
- }
- // list of pairs, each representing a range of lines that is clean and consecutive
- // in the changed DOM. lines (-1) and (N) are always clean, but may or may not
- // be consecutive with lines in the document. pairs are in sorted order.
- var cleanRanges = [
- [-1, N + 1]
- ];
-
- function rangeForLine(i)
- {
- // returns index of cleanRange containing i, or -1 if none
- var answer = -1;
- _.each(cleanRanges ,function(r, idx)
- {
- if (i >= r[1]) return false; // keep looking
- if (i < r[0]) return true; // not found, stop looking
- answer = idx;
- return true; // found, stop looking
- });
- return answer;
- }
-
- function removeLineFromRange(rng, line)
- {
- // rng is index into cleanRanges, line is line number
- // precond: line is in rng
- var a = cleanRanges[rng][0];
- var b = cleanRanges[rng][1];
- if ((a + 1) == b) cleanRanges.splice(rng, 1);
- else if (line == a) cleanRanges[rng][0]++;
- else if (line == (b - 1)) cleanRanges[rng][1]--;
- else cleanRanges.splice(rng, 1, [a, line], [line + 1, b]);
- }
-
- function splitRange(rng, pt)
- {
- // precond: pt splits cleanRanges[rng] into two non-empty ranges
- var a = cleanRanges[rng][0];
- var b = cleanRanges[rng][1];
- cleanRanges.splice(rng, 1, [a, pt], [pt, b]);
- }
- var correctedLines = {};
-
- function correctlyAssignLine(line)
- {
- if (correctedLines[line]) return true;
- p.corrections++;
- correctedLines[line] = true;
- // "line" is an index of a line in the un-updated rep.
- // returns whether line was already correctly assigned (i.e. correctly
- // clean or dirty, according to cleanRanges, and if clean, correctly
- // attached or not attached (i.e. in the same range as) the prev and next lines).
- //console.log("correctly assigning: %d", line);
- var rng = rangeForLine(line);
- var lineClean = isClean(line);
- if (rng < 0)
- {
- if (lineClean)
- {
- console.debug("somehow lost clean line");
- }
- return true;
- }
- if (!lineClean)
- {
- // a clean-range includes this dirty line, fix it
- removeLineFromRange(rng, line);
- return false;
- }
- else
- {
- // line is clean, but could be wrongly connected to a clean line
- // above or below
+ // rng is index into cleanRanges, line is line number
+ // precond: line is in rng
var a = cleanRanges[rng][0];
var b = cleanRanges[rng][1];
- var didSomething = false;
- // we'll leave non-clean adjacent nodes in the clean range for the caller to
- // detect and deal with. we deal with whether the range should be split
- // just above or just below this line.
- if (a < line && isClean(line - 1) && !isConsecutive(line))
- {
- splitRange(rng, line);
- didSomething = true;
- }
- if (b > (line + 1) && isClean(line + 1) && !isConsecutive(line + 1))
- {
- splitRange(rng, line + 1);
- didSomething = true;
- }
- return !didSomething;
+ if ((a + 1) == b) cleanRanges.splice(rng, 1);
+ else if (line == a) cleanRanges[rng][0]++;
+ else if (line == (b - 1)) cleanRanges[rng][1]--;
+ else cleanRanges.splice(rng, 1, [a, line], [line + 1, b]);
}
- }
- function detectChangesAroundLine(line, reqInARow)
- {
- // make sure cleanRanges is correct about line number "line" and the surrounding
- // lines; only stops checking at end of document or after no changes need
- // making for several consecutive lines. note that iteration is over old lines,
- // so this operation takes time proportional to the number of old lines
- // that are changed or missing, not the number of new lines inserted.
- var correctInARow = 0;
- var currentIndex = line;
- while (correctInARow < reqInARow && currentIndex >= 0)
+ function splitRange(rng, pt)
{
- if (correctlyAssignLine(currentIndex))
+ // precond: pt splits cleanRanges[rng] into two non-empty ranges
+ var a = cleanRanges[rng][0];
+ var b = cleanRanges[rng][1];
+ cleanRanges.splice(rng, 1, [a, pt], [pt, b]);
+ }
+ var correctedLines = {};
+
+ function correctlyAssignLine(line)
+ {
+ if (correctedLines[line]) return true;
+ p.corrections++;
+ correctedLines[line] = true;
+ // "line" is an index of a line in the un-updated rep.
+ // returns whether line was already correctly assigned (i.e. correctly
+ // clean or dirty, according to cleanRanges, and if clean, correctly
+ // attached or not attached (i.e. in the same range as) the prev and next lines).
+ //console.log("correctly assigning: %d", line);
+ var rng = rangeForLine(line);
+ var lineClean = isClean(line);
+ if (rng < 0)
{
- correctInARow++;
+ if (lineClean)
+ {
+ console.debug("somehow lost clean line");
+ }
+ return true;
}
- else correctInARow = 0;
- currentIndex--;
- }
- correctInARow = 0;
- currentIndex = line;
- while (correctInARow < reqInARow && currentIndex < N)
- {
- if (correctlyAssignLine(currentIndex))
+ if (!lineClean)
{
- correctInARow++;
- }
- else correctInARow = 0;
- currentIndex++;
- }
- }
-
- if (N === 0)
- {
- p.cancel();
- if (!isConsecutive(0))
- {
- splitRange(0, 0);
- }
- }
- else
- {
- p.mark("topbot");
- detectChangesAroundLine(0, 1);
- detectChangesAroundLine(N - 1, 1);
-
- p.mark("obs");
- //console.log("observedChanges: "+toSource(observedChanges));
- for (var k in observedChanges.cleanNodesNearChanges)
- {
- var key = k.substring(1);
- if (rep.lines.containsKey(key))
- {
- var line = rep.lines.indexOfKey(key);
- detectChangesAroundLine(line, 2);
- }
- }
- p.mark("stats&calc");
- p.literal(p.forIndices, "byidx");
- p.literal(p.consecutives, "cons");
- p.literal(p.corrections, "corr");
- }
-
- var dirtyRanges = [];
- for (var r = 0; r < cleanRanges.length - 1; r++)
- {
- dirtyRanges.push([cleanRanges[r][1], cleanRanges[r + 1][0]]);
- }
-
- p.end();
-
- return dirtyRanges;
- }
-
- function markNodeClean(n)
- {
- // clean nodes have knownHTML that matches their innerHTML
- var dirtiness = {};
- dirtiness.nodeId = uniqueId(n);
- dirtiness.knownHTML = n.innerHTML;
- if (browser.msie)
- {
- // adding a space to an "empty" div in IE designMode doesn't
- // change the innerHTML of the div's parent; also, other
- // browsers don't support innerText
- dirtiness.knownText = n.innerText;
- }
- setAssoc(n, "dirtiness", dirtiness);
- }
-
- function isNodeDirty(n)
- {
- var p = PROFILER("cleanCheck", false);
- if (n.parentNode != root) return true;
- var data = getAssoc(n, "dirtiness");
- if (!data) return true;
- if (n.id !== data.nodeId) return true;
- if (browser.msie)
- {
- if (n.innerText !== data.knownText) return true;
- }
- if (n.innerHTML !== data.knownHTML) return true;
- p.end();
- return false;
- }
-
- function getLineEntryTopBottom(entry, destObj)
- {
- var dom = entry.lineNode;
- var top = dom.offsetTop;
- var height = dom.offsetHeight;
- var obj = (destObj || {});
- obj.top = top;
- obj.bottom = (top + height);
- return obj;
- }
-
- function getViewPortTopBottom()
- {
- var theTop = getScrollY();
- var doc = window.document;
- var height = doc.documentElement.clientHeight;
- return {
- top: theTop,
- bottom: (theTop + height)
- };
- }
-
- function getVisibleLineRange()
- {
- var viewport = getViewPortTopBottom();
- //console.log("viewport top/bottom: %o", viewport);
- var obj = {};
- var start = rep.lines.search(function(e)
- {
- return getLineEntryTopBottom(e, obj).bottom > viewport.top;
- });
- var end = rep.lines.search(function(e)
- {
- return getLineEntryTopBottom(e, obj).top >= viewport.bottom;
- });
- if (end < start) end = start; // unlikely
- //console.log(start+","+end);
- return [start, end];
- }
-
- function getVisibleCharRange()
- {
- var lineRange = getVisibleLineRange();
- return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
- }
-
- function handleCut(evt)
- {
- inCallStackIfNecessary("handleCut", function()
- {
- doDeleteKey(evt);
- });
- return true;
- }
-
- function handleClick(evt)
- {
- inCallStackIfNecessary("handleClick", function()
- {
- idleWorkTimer.atMost(200);
- });
-
- function isLink(n)
- {
- return (n.tagName || '').toLowerCase() == "a" && n.href;
- }
-
- // only want to catch left-click
- if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3))
- {
- // find A tag with HREF
- var n = evt.target;
- while (n && n.parentNode && !isLink(n))
- {
- n = n.parentNode;
- }
- if (n && isLink(n))
- {
- try
- {
- var newWindow = window.open(n.href, '_blank');
- newWindow.focus();
- }
- catch (e)
- {
- // absorb "user canceled" error in IE for certain prompts
- }
- evt.preventDefault();
- }
- }
- }
-
- function doReturnKey()
- {
- if (!(rep.selStart && rep.selEnd))
- {
- return;
- }
-
- var lineNum = rep.selStart[0];
- var listType = getLineListType(lineNum);
-
- if (listType)
- {
- var text = rep.lines.atIndex(lineNum).text;
- listType = /([a-z]+)([0-9]+)/.exec(listType);
- var type = listType[1];
- var level = Number(listType[2]);
-
- //detect empty list item; exclude indentation
- if(text === '*' && type !== "indent")
- {
- //if not already on the highest level
- if(level > 1)
- {
- setLineListType(lineNum, type+(level-1));//automatically decrease the level
+ // a clean-range includes this dirty line, fix it
+ removeLineFromRange(rng, line);
+ return false;
}
else
{
- setLineListType(lineNum, '');//remove the list
- renumberList(lineNum + 1);//trigger renumbering of list that may be right after
+ // line is clean, but could be wrongly connected to a clean line
+ // above or below
+ var a = cleanRanges[rng][0];
+ var b = cleanRanges[rng][1];
+ var didSomething = false;
+ // we'll leave non-clean adjacent nodes in the clean range for the caller to
+ // detect and deal with. we deal with whether the range should be split
+ // just above or just below this line.
+ if (a < line && isClean(line - 1) && !isConsecutive(line))
+ {
+ splitRange(rng, line);
+ didSomething = true;
+ }
+ if (b > (line + 1) && isClean(line + 1) && !isConsecutive(line + 1))
+ {
+ splitRange(rng, line + 1);
+ didSomething = true;
+ }
+ return !didSomething;
}
}
- else if (lineNum + 1 < rep.lines.length())
- {
- performDocumentReplaceSelection('\n');
- setLineListType(lineNum + 1, type+level);
- }
- }
- else
- {
- performDocumentReplaceSelection('\n');
- handleReturnIndentation();
- }
- }
- function doIndentOutdent(isOut)
- {
- if (!((rep.selStart && rep.selEnd) ||
- ((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) && rep.selEnd[1] > 1)) &&
- (isOut != true)
- )
+ function detectChangesAroundLine(line, reqInARow)
+ {
+ // make sure cleanRanges is correct about line number "line" and the surrounding
+ // lines; only stops checking at end of document or after no changes need
+ // making for several consecutive lines. note that iteration is over old lines,
+ // so this operation takes time proportional to the number of old lines
+ // that are changed or missing, not the number of new lines inserted.
+ var correctInARow = 0;
+ var currentIndex = line;
+ while (correctInARow < reqInARow && currentIndex >= 0)
+ {
+ if (correctlyAssignLine(currentIndex))
+ {
+ correctInARow++;
+ }
+ else correctInARow = 0;
+ currentIndex--;
+ }
+ correctInARow = 0;
+ currentIndex = line;
+ while (correctInARow < reqInARow && currentIndex < N)
+ {
+ if (correctlyAssignLine(currentIndex))
+ {
+ correctInARow++;
+ }
+ else correctInARow = 0;
+ currentIndex++;
+ }
+ }
+
+ if (N === 0)
+ {
+ p.cancel();
+ if (!isConsecutive(0))
+ {
+ splitRange(0, 0);
+ }
+ }
+ else
+ {
+ p.mark("topbot");
+ detectChangesAroundLine(0, 1);
+ detectChangesAroundLine(N - 1, 1);
+
+ p.mark("obs");
+ //console.log("observedChanges: "+toSource(observedChanges));
+ for (var k in observedChanges.cleanNodesNearChanges)
+ {
+ var key = k.substring(1);
+ if (rep.lines.containsKey(key))
+ {
+ var line = rep.lines.indexOfKey(key);
+ detectChangesAroundLine(line, 2);
+ }
+ }
+ p.mark("stats&calc");
+ p.literal(p.forIndices, "byidx");
+ p.literal(p.consecutives, "cons");
+ p.literal(p.corrections, "corr");
+ }
+
+ var dirtyRanges = [];
+ for (var r = 0; r < cleanRanges.length - 1; r++)
+ {
+ dirtyRanges.push([cleanRanges[r][1], cleanRanges[r + 1][0]]);
+ }
+
+ p.end();
+
+ return dirtyRanges;
+ }
+
+ function markNodeClean(n)
{
+ // clean nodes have knownHTML that matches their innerHTML
+ var dirtiness = {};
+ dirtiness.nodeId = uniqueId(n);
+ dirtiness.knownHTML = n.innerHTML;
+ if (browser.msie)
+ {
+ // adding a space to an "empty" div in IE designMode doesn't
+ // change the innerHTML of the div's parent; also, other
+ // browsers don't support innerText
+ dirtiness.knownText = n.innerText;
+ }
+ setAssoc(n, "dirtiness", dirtiness);
+ }
+
+ function isNodeDirty(n)
+ {
+ var p = PROFILER("cleanCheck", false);
+ if (n.parentNode != root) return true;
+ var data = getAssoc(n, "dirtiness");
+ if (!data) return true;
+ if (n.id !== data.nodeId) return true;
+ if (browser.msie)
+ {
+ if (n.innerText !== data.knownText) return true;
+ }
+ if (n.innerHTML !== data.knownHTML) return true;
+ p.end();
return false;
}
- var firstLine, lastLine;
- firstLine = rep.selStart[0];
- lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0));
- var mods = [];
- for (var n = firstLine; n <= lastLine; n++)
+ function getLineEntryTopBottom(entry, destObj)
{
- var listType = getLineListType(n);
- var t = 'indent';
- var level = 0;
+ var dom = entry.lineNode;
+ var top = dom.offsetTop;
+ var height = dom.offsetHeight;
+ var obj = (destObj || {});
+ obj.top = top;
+ obj.bottom = (top + height);
+ return obj;
+ }
+
+ function getViewPortTopBottom()
+ {
+ var theTop = getScrollY();
+ var doc = window.document;
+ var height = doc.documentElement.clientHeight;
+ return {
+ top: theTop,
+ bottom: (theTop + height)
+ };
+ }
+
+ function getVisibleLineRange()
+ {
+ var viewport = getViewPortTopBottom();
+ //console.log("viewport top/bottom: %o", viewport);
+ var obj = {};
+ var start = rep.lines.search(function(e)
+ {
+ return getLineEntryTopBottom(e, obj).bottom > viewport.top;
+ });
+ var end = rep.lines.search(function(e)
+ {
+ return getLineEntryTopBottom(e, obj).top >= viewport.bottom;
+ });
+ if (end < start) end = start; // unlikely
+ //console.log(start+","+end);
+ return [start, end];
+ }
+
+ function getVisibleCharRange()
+ {
+ var lineRange = getVisibleLineRange();
+ return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
+ }
+
+ function handleCut(evt)
+ {
+ inCallStackIfNecessary("handleCut", function()
+ {
+ doDeleteKey(evt);
+ });
+ return true;
+ }
+
+ function handleClick(evt)
+ {
+ inCallStackIfNecessary("handleClick", function()
+ {
+ idleWorkTimer.atMost(200);
+ });
+
+ function isLink(n)
+ {
+ return (n.tagName || '').toLowerCase() == "a" && n.href;
+ }
+
+ // only want to catch left-click
+ if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3))
+ {
+ // find A tag with HREF
+ var n = evt.target;
+ while (n && n.parentNode && !isLink(n))
+ {
+ n = n.parentNode;
+ }
+ if (n && isLink(n))
+ {
+ try
+ {
+ var newWindow = window.open(n.href, '_blank');
+ newWindow.focus();
+ }
+ catch (e)
+ {
+ // absorb "user canceled" error in IE for certain prompts
+ }
+ evt.preventDefault();
+ }
+ }
+ }
+
+ function doReturnKey()
+ {
+ if (!(rep.selStart && rep.selEnd))
+ {
+ return;
+ }
+
+ var lineNum = rep.selStart[0];
+ var listType = getLineListType(lineNum);
+
if (listType)
{
+ var text = rep.lines.atIndex(lineNum).text;
listType = /([a-z]+)([0-9]+)/.exec(listType);
+ var type = listType[1];
+ var level = Number(listType[2]);
+
+ //detect empty list item; exclude indentation
+ if(text === '*' && type !== "indent")
+ {
+ //if not already on the highest level
+ if(level > 1)
+ {
+ setLineListType(lineNum, type+(level-1));//automatically decrease the level
+ }
+ else
+ {
+ setLineListType(lineNum, '');//remove the list
+ renumberList(lineNum + 1);//trigger renumbering of list that may be right after
+ }
+ }
+ else if (lineNum + 1 < rep.lines.length())
+ {
+ performDocumentReplaceSelection('\n');
+ setLineListType(lineNum + 1, type+level);
+ }
+ }
+ else
+ {
+ performDocumentReplaceSelection('\n');
+ handleReturnIndentation();
+ }
+ }
+
+ function doIndentOutdent(isOut)
+ {
+ if (!((rep.selStart && rep.selEnd) ||
+ ((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) && rep.selEnd[1] > 1)) &&
+ (isOut != true)
+ )
+ {
+ return false;
+ }
+
+ var firstLine, lastLine;
+ firstLine = rep.selStart[0];
+ lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0));
+ var mods = [];
+ for (var n = firstLine; n <= lastLine; n++)
+ {
+ var listType = getLineListType(n);
+ var t = 'indent';
+ var level = 0;
if (listType)
{
- t = listType[1];
- level = Number(listType[2]);
+ listType = /([a-z]+)([0-9]+)/.exec(listType);
+ if (listType)
+ {
+ t = listType[1];
+ level = Number(listType[2]);
+ }
}
- }
- var newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1)));
- if (level != newLevel)
- {
- mods.push([n, (newLevel > 0) ? t + newLevel : '']);
- }
- }
-
- _.each(mods, function(mod){
- setLineListType(mod[0], mod[1]);
- });
- return true;
- }
- editorInfo.ace_doIndentOutdent = doIndentOutdent;
-
- function doTabKey(shiftDown)
- {
- if (!doIndentOutdent(shiftDown))
- {
- performDocumentReplaceSelection(THE_TAB);
- }
- }
-
- function doDeleteKey(optEvt)
- {
- var evt = optEvt || {};
- var handled = false;
- if (rep.selStart)
- {
- if (isCaret())
- {
- var lineNum = caretLine();
- var col = caretColumn();
- var lineEntry = rep.lines.atIndex(lineNum);
- var lineText = lineEntry.text;
- var lineMarker = lineEntry.lineMarker;
- if (/^ +$/.exec(lineText.substring(lineMarker, col)))
+ var newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1)));
+ if (level != newLevel)
{
- var col2 = col - lineMarker;
- var tabSize = THE_TAB.length;
- var toDelete = ((col2 - 1) % tabSize) + 1;
- performDocumentReplaceRange([lineNum, col - toDelete], [lineNum, col], '');
- //scrollSelectionIntoView();
- handled = true;
+ mods.push([n, (newLevel > 0) ? t + newLevel : '']);
}
}
- if (!handled)
+
+ _.each(mods, function(mod){
+ setLineListType(mod[0], mod[1]);
+ });
+ return true;
+ }
+ editorInfo.ace_doIndentOutdent = doIndentOutdent;
+
+ function doTabKey(shiftDown)
+ {
+ if (!doIndentOutdent(shiftDown))
+ {
+ performDocumentReplaceSelection(THE_TAB);
+ }
+ }
+
+ function doDeleteKey(optEvt)
+ {
+ var evt = optEvt || {};
+ var handled = false;
+ if (rep.selStart)
{
if (isCaret())
{
- var theLine = caretLine();
- var lineEntry = rep.lines.atIndex(theLine);
- if (caretColumn() <= lineEntry.lineMarker)
+ var lineNum = caretLine();
+ var col = caretColumn();
+ var lineEntry = rep.lines.atIndex(lineNum);
+ var lineText = lineEntry.text;
+ var lineMarker = lineEntry.lineMarker;
+ if (/^ +$/.exec(lineText.substring(lineMarker, col)))
{
- // delete at beginning of line
- var action = 'delete_newline';
- var prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : '');
- var thisLineListType = getLineListType(theLine);
- var prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1));
- var prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker);
-
- var thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine);
-
- if (thisLineListType)
+ var col2 = col - lineMarker;
+ var tabSize = THE_TAB.length;
+ var toDelete = ((col2 - 1) % tabSize) + 1;
+ performDocumentReplaceRange([lineNum, col - toDelete], [lineNum, col], '');
+ //scrollSelectionIntoView();
+ handled = true;
+ }
+ }
+ if (!handled)
+ {
+ if (isCaret())
+ {
+ var theLine = caretLine();
+ var lineEntry = rep.lines.atIndex(theLine);
+ if (caretColumn() <= lineEntry.lineMarker)
{
- // this line is a list
- if (prevLineBlank && !prevLineListType)
+ // delete at beginning of line
+ var action = 'delete_newline';
+ var prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : '');
+ var thisLineListType = getLineListType(theLine);
+ var prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1));
+ var prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker);
+
+ var thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine);
+
+ if (thisLineListType)
{
- // previous line is blank, remove it
+ // this line is a list
+ if (prevLineBlank && !prevLineListType)
+ {
+ // previous line is blank, remove it
+ performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], '');
+ }
+ else
+ {
+ // delistify
+ performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], '');
+ }
+ }else if (thisLineHasMarker && prevLineEntry){
+ // If the line has any attributes assigned, remove them by removing the marker '*'
+ performDocumentReplaceRange([theLine -1 , prevLineEntry.text.length], [theLine, lineEntry.lineMarker], '');
+ }
+ else if (theLine > 0)
+ {
+ // remove newline
performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], '');
}
- else
- {
- // delistify
- performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], '');
- }
- }else if (thisLineHasMarker && prevLineEntry){
- // If the line has any attributes assigned, remove them by removing the marker '*'
- performDocumentReplaceRange([theLine -1 , prevLineEntry.text.length], [theLine, lineEntry.lineMarker], '');
}
- else if (theLine > 0)
+ else
{
- // remove newline
- performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], '');
- }
- }
- else
- {
- var docChar = caretDocChar();
- if (docChar > 0)
- {
- if (evt.metaKey || evt.ctrlKey || evt.altKey)
+ var docChar = caretDocChar();
+ if (docChar > 0)
{
- // delete as many unicode "letters or digits" in a row as possible;
- // always delete one char, delete further even if that first char
- // isn't actually a word char.
- var deleteBackTo = docChar - 1;
- while (deleteBackTo > lineEntry.lineMarker && isWordChar(rep.alltext.charAt(deleteBackTo - 1)))
+ if (evt.metaKey || evt.ctrlKey || evt.altKey)
{
- deleteBackTo--;
- }
- performDocumentReplaceCharRange(deleteBackTo, docChar, '');
- }
- else
- {
- // normal delete
- performDocumentReplaceCharRange(docChar - 1, docChar, '');
- }
- }
- }
- }
- else
- {
- performDocumentReplaceSelection('');
- }
- }
- }
- //if the list has been removed, it is necessary to renumber
- //starting from the *next* line because the list may have been
- //separated. If it returns null, it means that the list was not cut, try
- //from the current one.
- var line = caretLine();
- if(line != -1 && renumberList(line+1) === null)
- {
- renumberList(line);
- }
- }
-
- // set of "letter or digit" chars is based on section 20.5.16 of the original Java Language Spec
- var REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
- var REGEX_SPACE = /\s/;
-
- function isWordChar(c)
- {
- return !!REGEX_WORDCHAR.exec(c);
- }
- editorInfo.ace_isWordChar = isWordChar;
-
- function isSpaceChar(c)
- {
- return !!REGEX_SPACE.exec(c);
- }
-
- function moveByWordInLine(lineText, initialIndex, forwardNotBack)
- {
- var i = initialIndex;
-
- function nextChar()
- {
- if (forwardNotBack) return lineText.charAt(i);
- else return lineText.charAt(i - 1);
- }
-
- function advance()
- {
- if (forwardNotBack) i++;
- else i--;
- }
-
- function isDone()
- {
- if (forwardNotBack) return i >= lineText.length;
- else return i <= 0;
- }
-
- // On Mac and Linux, move right moves to end of word and move left moves to start;
- // on Windows, always move to start of word.
- // On Windows, Firefox and IE disagree on whether to stop for punctuation (FF says no).
- if (browser.msie && forwardNotBack)
- {
- while ((!isDone()) && isWordChar(nextChar()))
- {
- advance();
- }
- while ((!isDone()) && !isWordChar(nextChar()))
- {
- advance();
- }
- }
- else
- {
- while ((!isDone()) && !isWordChar(nextChar()))
- {
- advance();
- }
- while ((!isDone()) && isWordChar(nextChar()))
- {
- advance();
- }
- }
-
- return i;
- }
-
- function handleKeyEvent(evt)
- {
- // if (DEBUG && window.DONT_INCORP) return;
- if (!isEditable) return;
- var type = evt.type;
- var charCode = evt.charCode;
- var keyCode = evt.keyCode;
- var which = evt.which;
- var altKey = evt.altKey;
- var shiftKey = evt.shiftKey;
-
- // prevent ESC key
- if (keyCode == 27)
- {
- evt.preventDefault();
- return;
- }
- // Is caret potentially hidden by the chat button?
- var myselection = document.getSelection(); // get the current caret selection
- var caretOffsetTop = myselection.focusNode.parentNode.offsetTop | myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214
-
- if(myselection.focusNode.wholeText){ // Is there any content? If not lineHeight will report wrong..
- var lineHeight = myselection.focusNode.parentNode.offsetHeight; // line height of populated links
- }else{
- var lineHeight = myselection.focusNode.offsetHeight; // line height of blank lines
- }
- var heightOfChatIcon = $('#chaticon').height(); // height of the chat icon button
- lineHeight = (lineHeight *2) + heightOfChatIcon;
- var viewport = getViewPortTopBottom();
- var viewportHeight = viewport.bottom - viewport.top - lineHeight;
- var relCaretOffsetTop = caretOffsetTop - viewport.top; // relative Caret Offset Top to viewport
- if (viewportHeight < relCaretOffsetTop){
- $("#chaticon").css("opacity",".3"); // make chaticon opacity low when user types near it
- }else{
- $("#chaticon").css("opacity","1"); // make chaticon opacity back to full (so fully visible)
- }
-
- //dmesg("keyevent type: "+type+", which: "+which);
- // Don't take action based on modifier keys going up and down.
- // Modifier keys do not generate "keypress" events.
- // 224 is the command-key under Mac Firefox.
- // 91 is the Windows key in IE; it is ASCII for open-bracket but isn't the keycode for that key
- // 20 is capslock in IE.
- var isModKey = ((!charCode) && ((type == "keyup") || (type == "keydown")) && (keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 20 || keyCode == 224 || keyCode == 91));
- if (isModKey) return;
-
- // If the key is a keypress and the browser is opera and the key is enter, do nothign at all as this fires twice.
- if (keyCode == 13 && browser.opera && (type == "keypress")){
- return; // This stops double enters in Opera but double Tabs still show on single tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice
- }
- var specialHandled = false;
- var isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress"));
- var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress"));
- var stopped = false;
-
- inCallStackIfNecessary("handleKeyEvent", function()
- {
- if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ ))
- {
- // in IE, special keys don't send keypress, the keydown does the action
- if (!outsideKeyPress(evt))
- {
- evt.preventDefault();
- stopped = true;
- }
- }
- else if (type == "keydown")
- {
- outsideKeyDown(evt);
- }
- if (!stopped)
- {
- 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) && altKey && isTypeForSpecialKey && keyCode == 120){
- // Alt F9 focuses on the File Menu and/or editbar.
- // Note that while most editors use Alt F10 this is not desirable
- // As ubuntu cannot use Alt F10....
- // Focus on the editbar. -- TODO: Move Focus back to previous state (we know it so we can use it)
- var firstEditbarElement = $('#editbar').children("ul").first().children().first().children().first().children().first();
- $(this).blur();
- firstEditbarElement.focus();
- evt.preventDefault();
- }
- if ((!specialHandled) && altKey && keyCode == 67){
- // Alt c focuses on the Chat window
- $(this).blur();
- window.chat.show();
- window.chat.focus();
- evt.preventDefault();
- }
- if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === "keydown"){
- // Control-Shift-2 shows a gritter popup showing a line author
- var lineNumber = rep.selEnd[0];
- var alineAttrs = rep.alines[lineNumber];
- var apool = rep.apool;
-
- // TODO: support selection ranges
- // TODO: Still work when authorship colors have been cleared
- // TODO: i18n
- // TODO: There appears to be a race condition or so.
-
- var author = null;
- if (alineAttrs) {
- var authors = [];
- var authorNames = [];
- var opIter = Changeset.opIterator(alineAttrs);
-
- while (opIter.hasNext()){
- var op = opIter.next();
- authorId = Changeset.opAttributeValue(op, 'author', apool);
-
- // Only push unique authors and ones with values
- if(authors.indexOf(authorId) === -1 && authorId !== ""){
- authors.push(authorId);
- }
-
- }
-
- }
-
- // No author information is available IE on a new pad.
- if(authors.length === 0){
- var authorString = "No author information is available";
- }
- else{
- // Known authors info, both current and historical
- var padAuthors = window.pad.userList();
- var authorObj = {};
- authors.forEach(function(authorId){
- padAuthors.forEach(function(padAuthor){
- // If the person doing the lookup is the author..
- if(padAuthor.userId === authorId){
- if(window.clientVars.userId === authorId){
- authorObj = {
- name: "Me"
- }
- }else{
- authorObj = padAuthor;
+ // delete as many unicode "letters or digits" in a row as possible;
+ // always delete one char, delete further even if that first char
+ // isn't actually a word char.
+ var deleteBackTo = docChar - 1;
+ while (deleteBackTo > lineEntry.lineMarker && isWordChar(rep.alltext.charAt(deleteBackTo - 1)))
+ {
+ deleteBackTo--;
}
+ performDocumentReplaceCharRange(deleteBackTo, docChar, '');
+ }
+ else
+ {
+ // normal delete
+ performDocumentReplaceCharRange(docChar - 1, docChar, '');
}
- });
- if(!authorObj){
- author = "Unknown";
- return;
}
- author = authorObj.name;
- if(!author) author = "Unknown";
- authorNames.push(author);
- })
- }
- if(authors.length === 1){
- var authorString = "The author of this line is " + authorNames;
- }
- if(authors.length > 1){
- var authorString = "The authors of this line are " + authorNames.join(" & ");
- }
-
- $.gritter.add({
- // (string | mandatory) the heading of the notification
- title: 'Line Authors',
- // (string | mandatory) the text inside the notification
- text: authorString,
- // (bool | optional) if you want it to fade out on its own or just sit there
- sticky: false,
- // (int | optional) the time you want it to be alive for before fading out
- time: '4000'
- });
- }
- 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.
- // --
- // we do deletes completely customly now:
- // - allows consistent (and better) meta-delete behavior
- // - normalizing and then allowing default behavior confused IE
- // - probably eliminates a few minor quirks
- fastIncorp(3);
- evt.preventDefault();
- doDeleteKey(evt);
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForSpecialKey && keyCode == 13)
- {
- // return key, handle specially;
- // note that in mozilla we need to do an incorporation for proper return behavior anyway.
- fastIncorp(4);
- evt.preventDefault();
- doReturnKey();
- //scrollSelectionIntoView();
- scheduler.setTimeout(function()
- {
- window.scrollBy(-100, 0);
- }, 0);
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "s" && (evt.metaKey || evt.ctrlKey) && !evt.altKey) /* Do a saved revision on ctrl S */
- {
- evt.preventDefault();
- var originalBackground = $('#revisionlink').css("background")
- $('#revisionlink').css({"background":"lightyellow"});
- scheduler.setTimeout(function(){
- $('#revisionlink').css({"background":originalBackground});
- }, 1000);
- window.pad.collabClient.sendMessage({"type":"SAVE_REVISION"}); /* The parent.parent part of this is BAD and I feel bad.. It may break something */
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && !(evt.metaKey || evt.ctrlKey))
- {
- // tab
- fastIncorp(5);
- evt.preventDefault();
- doTabKey(evt.shiftKey);
- //scrollSelectionIntoView();
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "z" && (evt.metaKey || evt.ctrlKey) && !evt.altKey)
- {
- // cmd-Z (undo)
- fastIncorp(6);
- evt.preventDefault();
- if (evt.shiftKey)
- {
- doUndoRedo("redo");
+ }
}
else
{
- doUndoRedo("undo");
+ performDocumentReplaceSelection('');
}
- specialHandled = true;
}
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "y" && (evt.metaKey || evt.ctrlKey))
- {
- // cmd-Y (redo)
- fastIncorp(10);
- evt.preventDefault();
- doUndoRedo("redo");
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "b" && (evt.metaKey || evt.ctrlKey))
- {
- // cmd-B (bold)
- fastIncorp(13);
- evt.preventDefault();
- toggleAttributeOnSelection('bold');
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "i" && (evt.metaKey || evt.ctrlKey))
- {
- // cmd-I (italic)
- fastIncorp(14);
- evt.preventDefault();
- toggleAttributeOnSelection('italic');
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "u" && (evt.metaKey || evt.ctrlKey))
- {
- // cmd-U (underline)
- fastIncorp(15);
- evt.preventDefault();
- toggleAttributeOnSelection('underline');
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "5" && (evt.metaKey || evt.ctrlKey))
- {
- // cmd-5 (strikethrough)
- fastIncorp(13);
- evt.preventDefault();
- toggleAttributeOnSelection('strikethrough');
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "l" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey)
- {
- // cmd-shift-L (unorderedlist)
- fastIncorp(9);
- evt.preventDefault();
- doInsertUnorderedList()
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "n" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey)
- {
- // cmd-shift-N (orderedlist)
- fastIncorp(9);
- evt.preventDefault();
- doInsertOrderedList()
- specialHandled = true;
- }
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "c" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey) {
- // cmd-shift-C (clearauthorship)
- fastIncorp(9);
- evt.preventDefault();
- CMDS.clearauthorship();
- }
- if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "h" && (evt.ctrlKey))
- {
- // cmd-H (backspace)
- fastIncorp(20);
- evt.preventDefault();
- doDeleteKey();
- specialHandled = true;
- }
- if((evt.which == 36 && evt.ctrlKey == true)){ setScrollY(0); } // Control Home send to Y = 0
- if((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey){
+ }
+ //if the list has been removed, it is necessary to renumber
+ //starting from the *next* line because the list may have been
+ //separated. If it returns null, it means that the list was not cut, try
+ //from the current one.
+ var line = caretLine();
+ if(line != -1 && renumberList(line+1) === null)
+ {
+ renumberList(line);
+ }
+ }
- evt.preventDefault(); // This is required, browsers will try to do normal default behavior on page up / down and the default behavior SUCKS
+ // set of "letter or digit" chars is based on section 20.5.16 of the original Java Language Spec
+ var REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
+ var REGEX_SPACE = /\s/;
- var oldVisibleLineRange = getVisibleLineRange();
- var topOffset = rep.selStart[0] - oldVisibleLineRange[0];
- if(topOffset < 0 ){
- topOffset = 0;
+ function isWordChar(c)
+ {
+ return !!REGEX_WORDCHAR.exec(c);
+ }
+ editorInfo.ace_isWordChar = isWordChar;
+
+ function isSpaceChar(c)
+ {
+ return !!REGEX_SPACE.exec(c);
+ }
+
+ function moveByWordInLine(lineText, initialIndex, forwardNotBack)
+ {
+ var i = initialIndex;
+
+ function nextChar()
+ {
+ if (forwardNotBack) return lineText.charAt(i);
+ else return lineText.charAt(i - 1);
+ }
+
+ function advance()
+ {
+ if (forwardNotBack) i++;
+ else i--;
+ }
+
+ function isDone()
+ {
+ if (forwardNotBack) return i >= lineText.length;
+ else return i <= 0;
+ }
+
+ // On Mac and Linux, move right moves to end of word and move left moves to start;
+ // on Windows, always move to start of word.
+ // On Windows, Firefox and IE disagree on whether to stop for punctuation (FF says no).
+ if (browser.msie && forwardNotBack)
+ {
+ while ((!isDone()) && isWordChar(nextChar()))
+ {
+ advance();
+ }
+ while ((!isDone()) && !isWordChar(nextChar()))
+ {
+ advance();
+ }
+ }
+ else
+ {
+ while ((!isDone()) && !isWordChar(nextChar()))
+ {
+ advance();
+ }
+ while ((!isDone()) && isWordChar(nextChar()))
+ {
+ advance();
+ }
+ }
+
+ return i;
+ }
+
+ function handleKeyEvent(evt)
+ {
+ // if (DEBUG && window.DONT_INCORP) return;
+ if (!isEditable) return;
+ var type = evt.type;
+ var charCode = evt.charCode;
+ var keyCode = evt.keyCode;
+ var which = evt.which;
+ var altKey = evt.altKey;
+ var shiftKey = evt.shiftKey;
+
+ // prevent ESC key
+ if (keyCode == 27)
+ {
+ evt.preventDefault();
+ return;
+ }
+ // Is caret potentially hidden by the chat button?
+ var myselection = document.getSelection(); // get the current caret selection
+ var caretOffsetTop = myselection.focusNode.parentNode.offsetTop | myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214
+
+ if(myselection.focusNode.wholeText){ // Is there any content? If not lineHeight will report wrong..
+ var lineHeight = myselection.focusNode.parentNode.offsetHeight; // line height of populated links
+ }else{
+ var lineHeight = myselection.focusNode.offsetHeight; // line height of blank lines
+ }
+ var heightOfChatIcon = $('#chaticon').height(); // height of the chat icon button
+ lineHeight = (lineHeight *2) + heightOfChatIcon;
+ var viewport = getViewPortTopBottom();
+ var viewportHeight = viewport.bottom - viewport.top - lineHeight;
+ var relCaretOffsetTop = caretOffsetTop - viewport.top; // relative Caret Offset Top to viewport
+ if (viewportHeight < relCaretOffsetTop){
+ $("#chaticon").css("opacity",".3"); // make chaticon opacity low when user types near it
+ }else{
+ $("#chaticon").css("opacity","1"); // make chaticon opacity back to full (so fully visible)
+ }
+
+ //dmesg("keyevent type: "+type+", which: "+which);
+ // Don't take action based on modifier keys going up and down.
+ // Modifier keys do not generate "keypress" events.
+ // 224 is the command-key under Mac Firefox.
+ // 91 is the Windows key in IE; it is ASCII for open-bracket but isn't the keycode for that key
+ // 20 is capslock in IE.
+ var isModKey = ((!charCode) && ((type == "keyup") || (type == "keydown")) && (keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 20 || keyCode == 224 || keyCode == 91));
+ if (isModKey) return;
+
+ // If the key is a keypress and the browser is opera and the key is enter, do nothign at all as this fires twice.
+ if (keyCode == 13 && browser.opera && (type == "keypress")){
+ return; // This stops double enters in Opera but double Tabs still show on single tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice
+ }
+ var specialHandled = false;
+ var isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress"));
+ var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress"));
+ var stopped = false;
+
+ inCallStackIfNecessary("handleKeyEvent", function()
+ {
+ if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ ))
+ {
+ // in IE, special keys don't send keypress, the keydown does the action
+ if (!outsideKeyPress(evt))
+ {
+ evt.preventDefault();
+ stopped = true;
}
+ }
+ else if (type == "keydown")
+ {
+ outsideKeyDown(evt);
+ }
+ if (!stopped)
+ {
+ 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) && altKey && isTypeForSpecialKey && keyCode == 120){
+ // Alt F9 focuses on the File Menu and/or editbar.
+ // Note that while most editors use Alt F10 this is not desirable
+ // As ubuntu cannot use Alt F10....
+ // Focus on the editbar. -- TODO: Move Focus back to previous state (we know it so we can use it)
+ var firstEditbarElement = $('#editbar').children("ul").first().children().first().children().first().children().first();
+ $(this).blur();
+ firstEditbarElement.focus();
+ evt.preventDefault();
+ }
+ if ((!specialHandled) && altKey && keyCode == 67){
+ // Alt c focuses on the Chat window
+ $(this).blur();
+ window.chat.show();
+ window.chat.focus();
+ evt.preventDefault();
+ }
+ if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === "keydown"){
+ // Control-Shift-2 shows a gritter popup showing a line author
+ var lineNumber = rep.selEnd[0];
+ var alineAttrs = rep.alines[lineNumber];
+ var apool = rep.apool;
- var isPageDown = evt.which === 34;
- var isPageUp = evt.which === 33;
+ // TODO: support selection ranges
+ // TODO: Still work when authorship colors have been cleared
+ // TODO: i18n
+ // TODO: There appears to be a race condition or so.
- scheduler.setTimeout(function(){
- var newVisibleLineRange = getVisibleLineRange(); // the visible lines IE 1,10
- var linesCount = rep.lines.length(); // total count of lines in pad IE 10
- var numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now?
+ var author = null;
+ if (alineAttrs) {
+ var authors = [];
+ var authorNames = [];
+ var opIter = Changeset.opIterator(alineAttrs);
- if(isPageUp){
- rep.selEnd[0] = rep.selEnd[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page)
- rep.selStart[0] = rep.selStart[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page)
- }
+ while (opIter.hasNext()){
+ var op = opIter.next();
+ authorId = Changeset.opAttributeValue(op, 'author', apool);
+
+ // Only push unique authors and ones with values
+ if(authors.indexOf(authorId) === -1 && authorId !== ""){
+ authors.push(authorId);
+ }
- if(isPageDown){ // if we hit page down
- if(rep.selEnd[0] >= oldVisibleLineRange[0]){ // If the new viewpoint position is actually further than where we are right now
- rep.selStart[0] = oldVisibleLineRange[1] -1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content
- rep.selEnd[0] = oldVisibleLineRange[1] -1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content
}
+
}
- //ensure min and max
- if(rep.selEnd[0] < 0){
- rep.selEnd[0] = 0;
+ // No author information is available IE on a new pad.
+ if(authors.length === 0){
+ var authorString = "No author information is available";
}
- if(rep.selStart[0] < 0){
- rep.selStart[0] = 0;
+ else{
+ // Known authors info, both current and historical
+ var padAuthors = window.pad.userList();
+ var authorObj = {};
+ authors.forEach(function(authorId){
+ padAuthors.forEach(function(padAuthor){
+ // If the person doing the lookup is the author..
+ if(padAuthor.userId === authorId){
+ if(window.clientVars.userId === authorId){
+ authorObj = {
+ name: "Me"
+ }
+ }else{
+ authorObj = padAuthor;
+ }
+ }
+ });
+ if(!authorObj){
+ author = "Unknown";
+ return;
+ }
+ author = authorObj.name;
+ if(!author) author = "Unknown";
+ authorNames.push(author);
+ })
}
- if(rep.selEnd[0] >= linesCount){
- rep.selEnd[0] = linesCount-1;
+ if(authors.length === 1){
+ var authorString = "The author of this line is " + authorNames;
}
- updateBrowserSelectionFromRep();
+ if(authors.length > 1){
+ var authorString = "The authors of this line are " + authorNames.join(" & ");
+ }
+
+ $.gritter.add({
+ // (string | mandatory) the heading of the notification
+ title: 'Line Authors',
+ // (string | mandatory) the text inside the notification
+ text: authorString,
+ // (bool | optional) if you want it to fade out on its own or just sit there
+ sticky: false,
+ // (int | optional) the time you want it to be alive for before fading out
+ time: '4000'
+ });
+ }
+ 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.
+ // --
+ // we do deletes completely customly now:
+ // - allows consistent (and better) meta-delete behavior
+ // - normalizing and then allowing default behavior confused IE
+ // - probably eliminates a few minor quirks
+ fastIncorp(3);
+ evt.preventDefault();
+ doDeleteKey(evt);
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForSpecialKey && keyCode == 13)
+ {
+ // return key, handle specially;
+ // note that in mozilla we need to do an incorporation for proper return behavior anyway.
+ fastIncorp(4);
+ evt.preventDefault();
+ doReturnKey();
+ //scrollSelectionIntoView();
+ scheduler.setTimeout(function()
+ {
+ window.scrollBy(-100, 0);
+ }, 0);
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "s" && (evt.metaKey || evt.ctrlKey) && !evt.altKey) /* Do a saved revision on ctrl S */
+ {
+ evt.preventDefault();
+ var originalBackground = $('#revisionlink').css("background")
+ $('#revisionlink').css({"background":"lightyellow"});
+ scheduler.setTimeout(function(){
+ $('#revisionlink').css({"background":originalBackground});
+ }, 1000);
+ window.pad.collabClient.sendMessage({"type":"SAVE_REVISION"}); /* The parent.parent part of this is BAD and I feel bad.. It may break something */
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && !(evt.metaKey || evt.ctrlKey))
+ {
+ // tab
+ fastIncorp(5);
+ evt.preventDefault();
+ doTabKey(evt.shiftKey);
+ //scrollSelectionIntoView();
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "z" && (evt.metaKey || evt.ctrlKey) && !evt.altKey)
+ {
+ // cmd-Z (undo)
+ fastIncorp(6);
+ evt.preventDefault();
+ if (evt.shiftKey)
+ {
+ doUndoRedo("redo");
+ }
+ else
+ {
+ doUndoRedo("undo");
+ }
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "y" && (evt.metaKey || evt.ctrlKey))
+ {
+ // cmd-Y (redo)
+ fastIncorp(10);
+ evt.preventDefault();
+ doUndoRedo("redo");
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "b" && (evt.metaKey || evt.ctrlKey))
+ {
+ // cmd-B (bold)
+ fastIncorp(13);
+ evt.preventDefault();
+ toggleAttributeOnSelection('bold');
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "i" && (evt.metaKey || evt.ctrlKey))
+ {
+ // cmd-I (italic)
+ fastIncorp(14);
+ evt.preventDefault();
+ toggleAttributeOnSelection('italic');
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "u" && (evt.metaKey || evt.ctrlKey))
+ {
+ // cmd-U (underline)
+ fastIncorp(15);
+ evt.preventDefault();
+ toggleAttributeOnSelection('underline');
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "5" && (evt.metaKey || evt.ctrlKey))
+ {
+ // cmd-5 (strikethrough)
+ fastIncorp(13);
+ evt.preventDefault();
+ toggleAttributeOnSelection('strikethrough');
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "l" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey)
+ {
+ // cmd-shift-L (unorderedlist)
+ fastIncorp(9);
+ evt.preventDefault();
+ doInsertUnorderedList()
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "n" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey)
+ {
+ // cmd-shift-N (orderedlist)
+ fastIncorp(9);
+ evt.preventDefault();
+ doInsertOrderedList()
+ specialHandled = true;
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "c" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey) {
+ // cmd-shift-C (clearauthorship)
+ fastIncorp(9);
+ evt.preventDefault();
+ CMDS.clearauthorship();
+ }
+ if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "h" && (evt.ctrlKey))
+ {
+ // cmd-H (backspace)
+ fastIncorp(20);
+ evt.preventDefault();
+ doDeleteKey();
+ specialHandled = true;
+ }
+ if((evt.which == 36 && evt.ctrlKey == true)){ setScrollY(0); } // Control Home send to Y = 0
+ if((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey){
+
+ evt.preventDefault(); // This is required, browsers will try to do normal default behavior on page up / down and the default behavior SUCKS
+
+ var oldVisibleLineRange = getVisibleLineRange();
+ var topOffset = rep.selStart[0] - oldVisibleLineRange[0];
+ if(topOffset < 0 ){
+ topOffset = 0;
+ }
+
+ var isPageDown = evt.which === 34;
+ var isPageUp = evt.which === 33;
+
+ scheduler.setTimeout(function(){
+ var newVisibleLineRange = getVisibleLineRange(); // the visible lines IE 1,10
+ var linesCount = rep.lines.length(); // total count of lines in pad IE 10
+ var numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now?
+
+ if(isPageUp){
+ rep.selEnd[0] = rep.selEnd[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page)
+ rep.selStart[0] = rep.selStart[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page)
+ }
+
+ if(isPageDown){ // if we hit page down
+ if(rep.selEnd[0] >= oldVisibleLineRange[0]){ // If the new viewpoint position is actually further than where we are right now
+ rep.selStart[0] = oldVisibleLineRange[1] -1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content
+ rep.selEnd[0] = oldVisibleLineRange[1] -1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content
+ }
+ }
+
+ //ensure min and max
+ if(rep.selEnd[0] < 0){
+ rep.selEnd[0] = 0;
+ }
+ if(rep.selStart[0] < 0){
+ rep.selStart[0] = 0;
+ }
+ if(rep.selEnd[0] >= linesCount){
+ rep.selEnd[0] = linesCount-1;
+ }
+ updateBrowserSelectionFromRep();
+ var myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current
+ var caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214
+
+ // sometimes the first selection is -1 which causes problems (Especially with ep_page_view)
+ // so use focusNode.offsetTop value.
+ if(caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop;
+ setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document
+
+ }, 200);
+ }
+ /* Attempt to apply some sanity to cursor handling in Chrome after a copy / paste event
+ We have to do this the way we do because rep. doesn't hold the value for keyheld events IE if the user
+ presses and holds the arrow key .. Sorry if this is ugly, blame Chrome's weird handling of viewports after new content is added*/
+ if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40) && browser.chrome){
+ var viewport = getViewPortTopBottom();
var myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current
var caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214
+ var lineHeight = $(myselection.focusNode.parentNode).parent("div").height(); // get the line height of the caret line
+ // top.console.log("offsetTop", myselection.focusNode.parentNode.parentNode.offsetTop);
+ try {
+ lineHeight = $(myselection.focusNode).height() // needed for how chrome handles line heights of null objects
+ // console.log("lineHeight now", lineHeight);
+ }catch(e){}
+ var caretOffsetTopBottom = caretOffsetTop + lineHeight;
+ var visibleLineRange = getVisibleLineRange(); // the visible lines IE 1,10
- // sometimes the first selection is -1 which causes problems (Especially with ep_page_view)
- // so use focusNode.offsetTop value.
- if(caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop;
- setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document
-
- }, 200);
- }
- /* Attempt to apply some sanity to cursor handling in Chrome after a copy / paste event
- We have to do this the way we do because rep. doesn't hold the value for keyheld events IE if the user
- presses and holds the arrow key .. Sorry if this is ugly, blame Chrome's weird handling of viewports after new content is added*/
- if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40) && browser.chrome){
- var viewport = getViewPortTopBottom();
- var myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current
- var caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214
- var lineHeight = $(myselection.focusNode.parentNode).parent("div").height(); // get the line height of the caret line
- // top.console.log("offsetTop", myselection.focusNode.parentNode.parentNode.offsetTop);
- try {
- lineHeight = $(myselection.focusNode).height() // needed for how chrome handles line heights of null objects
- // console.log("lineHeight now", lineHeight);
- }catch(e){}
- var caretOffsetTopBottom = caretOffsetTop + lineHeight;
- var visibleLineRange = getVisibleLineRange(); // the visible lines IE 1,10
-
- if(caretOffsetTop){ // sometimes caretOffsetTop bugs out and returns 0, not sure why, possible Chrome bug? Either way if it does we don't wanna mess with it
- // top.console.log(caretOffsetTop, viewport.top, caretOffsetTopBottom, viewport.bottom);
- var caretIsNotVisible = (caretOffsetTop < viewport.top || caretOffsetTopBottom >= viewport.bottom); // Is the Caret Visible to the user?
- // Expect some weird behavior caretOffsetTopBottom is greater than viewport.bottom on a keypress down
- var offsetTopSamePlace = caretOffsetTop == viewport.top; // sometimes moving key left & up leaves the caret at the same point as the viewport.top, technically the caret is visible but it's not fully visible so we should move to it
- if(offsetTopSamePlace && (evt.which == 37 || evt.which == 38)){
- var newY = caretOffsetTop;
- setScrollY(newY);
- }
-
- if(caretIsNotVisible){ // is the cursor no longer visible to the user?
- // top.console.log("Caret is NOT visible to the user");
- // top.console.log(caretOffsetTop,viewport.top,caretOffsetTopBottom,viewport.bottom);
- // Oh boy the caret is out of the visible area, I need to scroll the browser window to lineNum.
- if(evt.which == 37 || evt.which == 38){ // If left or up arrow
- var newY = caretOffsetTop; // That was easy!
+ if(caretOffsetTop){ // sometimes caretOffsetTop bugs out and returns 0, not sure why, possible Chrome bug? Either way if it does we don't wanna mess with it
+ // top.console.log(caretOffsetTop, viewport.top, caretOffsetTopBottom, viewport.bottom);
+ var caretIsNotVisible = (caretOffsetTop < viewport.top || caretOffsetTopBottom >= viewport.bottom); // Is the Caret Visible to the user?
+ // Expect some weird behavior caretOffsetTopBottom is greater than viewport.bottom on a keypress down
+ var offsetTopSamePlace = caretOffsetTop == viewport.top; // sometimes moving key left & up leaves the caret at the same point as the viewport.top, technically the caret is visible but it's not fully visible so we should move to it
+ if(offsetTopSamePlace && (evt.which == 37 || evt.which == 38)){
+ var newY = caretOffsetTop;
+ setScrollY(newY);
}
- if(evt.which == 39 || evt.which == 40){ // if down or right arrow
- // only move the viewport if we're at the bottom of the viewport, if we hit down any other time the viewport shouldn't change
- // NOTE: This behavior only fires if Chrome decides to break the page layout after a paste, it's annoying but nothing I can do
- var selection = getSelection();
- // top.console.log("line #", rep.selStart[0]); // the line our caret is on
- // top.console.log("firstvisible", visibleLineRange[0]); // the first visiblel ine
- // top.console.log("lastVisible", visibleLineRange[1]); // the last visible line
- // top.console.log(rep.selStart[0], visibleLineRange[1], rep.selStart[0], visibleLineRange[0]);
- var newY = viewport.top + lineHeight;
- }
- if(newY){
- setScrollY(newY); // set the scrollY offset of the viewport on the document
+
+ if(caretIsNotVisible){ // is the cursor no longer visible to the user?
+ // top.console.log("Caret is NOT visible to the user");
+ // top.console.log(caretOffsetTop,viewport.top,caretOffsetTopBottom,viewport.bottom);
+ // Oh boy the caret is out of the visible area, I need to scroll the browser window to lineNum.
+ if(evt.which == 37 || evt.which == 38){ // If left or up arrow
+ var newY = caretOffsetTop; // That was easy!
+ }
+ if(evt.which == 39 || evt.which == 40){ // if down or right arrow
+ // only move the viewport if we're at the bottom of the viewport, if we hit down any other time the viewport shouldn't change
+ // NOTE: This behavior only fires if Chrome decides to break the page layout after a paste, it's annoying but nothing I can do
+ var selection = getSelection();
+ // top.console.log("line #", rep.selStart[0]); // the line our caret is on
+ // top.console.log("firstvisible", visibleLineRange[0]); // the first visiblel ine
+ // top.console.log("lastVisible", visibleLineRange[1]); // the last visible line
+ // top.console.log(rep.selStart[0], visibleLineRange[1], rep.selStart[0], visibleLineRange[0]);
+ var newY = viewport.top + lineHeight;
+ }
+ if(newY){
+ setScrollY(newY); // set the scrollY offset of the viewport on the document
+ }
}
}
}
}
- }
- if (type == "keydown")
- {
- idleWorkTimer.atLeast(500);
- }
- else if (type == "keypress")
- {
- if ((!specialHandled) && false /*parenModule.shouldNormalizeOnChar(charCode)*/)
- {
- idleWorkTimer.atMost(0);
- }
- else
+ if (type == "keydown")
{
idleWorkTimer.atLeast(500);
}
- }
- else if (type == "keyup")
- {
- var wait = 0;
- idleWorkTimer.atLeast(wait);
- idleWorkTimer.atMost(wait);
- }
-
- // Is part of multi-keystroke international character on Firefox Mac
- var isFirefoxHalfCharacter = (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0);
-
- // Is part of multi-keystroke international character on Safari Mac
- var isSafariHalfCharacter = (browser.safari && evt.altKey && keyCode == 229);
-
- if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter)
- {
- idleWorkTimer.atLeast(3000); // give user time to type
- // if this is a keydown, e.g., the keyup shouldn't trigger a normalize
- thisKeyDoesntTriggerNormalize = true;
- }
-
- if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition))
- {
- if (type != "keyup")
+ else if (type == "keypress")
{
- observeChangesAroundSelection();
+ if ((!specialHandled) && false /*parenModule.shouldNormalizeOnChar(charCode)*/)
+ {
+ idleWorkTimer.atMost(0);
+ }
+ else
+ {
+ idleWorkTimer.atLeast(500);
+ }
+ }
+ else if (type == "keyup")
+ {
+ var wait = 0;
+ idleWorkTimer.atLeast(wait);
+ idleWorkTimer.atMost(wait);
+ }
+
+ // Is part of multi-keystroke international character on Firefox Mac
+ var isFirefoxHalfCharacter = (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0);
+
+ // Is part of multi-keystroke international character on Safari Mac
+ var isSafariHalfCharacter = (browser.safari && evt.altKey && keyCode == 229);
+
+ if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter)
+ {
+ idleWorkTimer.atLeast(3000); // give user time to type
+ // if this is a keydown, e.g., the keyup shouldn't trigger a normalize
+ thisKeyDoesntTriggerNormalize = true;
+ }
+
+ if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition))
+ {
+ if (type != "keyup")
+ {
+ observeChangesAroundSelection();
+ }
+ }
+
+ if (type == "keyup")
+ {
+ thisKeyDoesntTriggerNormalize = false;
+ }
+ });
+ }
+
+ var thisKeyDoesntTriggerNormalize = false;
+
+ function doUndoRedo(which)
+ {
+ // precond: normalized DOM
+ if (undoModule.enabled)
+ {
+ var whichMethod;
+ if (which == "undo") whichMethod = 'performUndo';
+ if (which == "redo") whichMethod = 'performRedo';
+ if (whichMethod)
+ {
+ var oldEventType = currentCallStack.editEvent.eventType;
+ currentCallStack.startNewEvent(which);
+ undoModule[whichMethod](function(backset, selectionInfo)
+ {
+ if (backset)
+ {
+ performDocumentApplyChangeset(backset);
+ }
+ if (selectionInfo)
+ {
+ performSelectionChange(lineAndColumnFromChar(selectionInfo.selStart), lineAndColumnFromChar(selectionInfo.selEnd), selectionInfo.selFocusAtStart);
+ }
+ var oldEvent = currentCallStack.startNewEvent(oldEventType, true);
+ return oldEvent;
+ });
}
}
+ }
+ editorInfo.ace_doUndoRedo = doUndoRedo;
- if (type == "keyup")
+ function updateBrowserSelectionFromRep()
+ {
+ // requires normalized DOM!
+ var selStart = rep.selStart,
+ selEnd = rep.selEnd;
+
+ if (!(selStart && selEnd))
{
- thisKeyDoesntTriggerNormalize = false;
+ setSelection(null);
+ return;
}
- });
- }
- var thisKeyDoesntTriggerNormalize = false;
+ var selection = {};
- function doUndoRedo(which)
- {
- // precond: normalized DOM
- if (undoModule.enabled)
- {
- var whichMethod;
- if (which == "undo") whichMethod = 'performUndo';
- if (which == "redo") whichMethod = 'performRedo';
- if (whichMethod)
- {
- var oldEventType = currentCallStack.editEvent.eventType;
- currentCallStack.startNewEvent(which);
- undoModule[whichMethod](function(backset, selectionInfo)
- {
- if (backset)
- {
- performDocumentApplyChangeset(backset);
- }
- if (selectionInfo)
- {
- performSelectionChange(lineAndColumnFromChar(selectionInfo.selStart), lineAndColumnFromChar(selectionInfo.selEnd), selectionInfo.selFocusAtStart);
- }
- var oldEvent = currentCallStack.startNewEvent(oldEventType, true);
- return oldEvent;
- });
- }
+ var ss = [selStart[0], selStart[1]];
+ selection.startPoint = getPointForLineAndChar(ss);
+
+ var se = [selEnd[0], selEnd[1]];
+ selection.endPoint = getPointForLineAndChar(se);
+
+ selection.focusAtStart = !! rep.selFocusAtStart;
+ setSelection(selection);
}
- }
- editorInfo.ace_doUndoRedo = doUndoRedo;
+ editorInfo.ace_updateBrowserSelectionFromRep = updateBrowserSelectionFromRep;
- function updateBrowserSelectionFromRep()
- {
- // requires normalized DOM!
- var selStart = rep.selStart,
- selEnd = rep.selEnd;
-
- if (!(selStart && selEnd))
+ function nodeMaxIndex(nd)
{
- setSelection(null);
- return;
+ if (isNodeText(nd)) return nd.nodeValue.length;
+ else return 1;
}
- var selection = {};
-
- var ss = [selStart[0], selStart[1]];
- selection.startPoint = getPointForLineAndChar(ss);
-
- var se = [selEnd[0], selEnd[1]];
- selection.endPoint = getPointForLineAndChar(se);
-
- selection.focusAtStart = !! rep.selFocusAtStart;
- setSelection(selection);
- }
- editorInfo.ace_updateBrowserSelectionFromRep = updateBrowserSelectionFromRep;
-
- function nodeMaxIndex(nd)
- {
- if (isNodeText(nd)) return nd.nodeValue.length;
- else return 1;
- }
-
- function hasIESelection()
- {
- var browserSelection;
- try
- {
- browserSelection = doc.selection;
- }
- catch (e)
- {}
- if (!browserSelection) return false;
- var origSelectionRange;
- try
- {
- origSelectionRange = browserSelection.createRange();
- }
- catch (e)
- {}
- if (!origSelectionRange) return false;
- return true;
- }
-
- function getSelection()
- {
- // returns null, or a structure containing startPoint and endPoint,
- // each of which has node (a magicdom node), index, and maxIndex. If the node
- // is a text node, maxIndex is the length of the text; else maxIndex is 1.
- // index is between 0 and maxIndex, inclusive.
- if (browser.msie)
+ function hasIESelection()
{
var browserSelection;
try
@@ -4165,7 +4136,7 @@ function Ace2Inner(editorInfo){
}
catch (e)
{}
- if (!browserSelection) return null;
+ if (!browserSelection) return false;
var origSelectionRange;
try
{
@@ -4173,1300 +4144,1327 @@ function Ace2Inner(editorInfo){
}
catch (e)
{}
- if (!origSelectionRange) return null;
- var selectionParent = origSelectionRange.parentElement();
- if (selectionParent.ownerDocument != doc) return null;
+ if (!origSelectionRange) return false;
+ return true;
+ }
- var newRange = function()
+ function getSelection()
+ {
+ // returns null, or a structure containing startPoint and endPoint,
+ // each of which has node (a magicdom node), index, and maxIndex. If the node
+ // is a text node, maxIndex is the length of the text; else maxIndex is 1.
+ // index is between 0 and maxIndex, inclusive.
+ if (browser.msie)
{
- return doc.body.createTextRange();
- };
-
- var rangeForElementNode = function(nd)
- {
- var rng = newRange();
- // doesn't work on text nodes
- rng.moveToElementText(nd);
- return rng;
- };
-
- var pointFromCollapsedRange = function(rng)
- {
- var parNode = rng.parentElement();
- var elemBelow = -1;
- var elemAbove = parNode.childNodes.length;
- var rangeWithin = rangeForElementNode(parNode);
-
- if (rng.compareEndPoints("StartToStart", rangeWithin) === 0)
+ var browserSelection;
+ try
{
- return {
- node: parNode,
- index: 0,
- maxIndex: 1
- };
+ browserSelection = doc.selection;
}
- else if (rng.compareEndPoints("EndToEnd", rangeWithin) === 0)
+ catch (e)
+ {}
+ if (!browserSelection) return null;
+ var origSelectionRange;
+ try
{
- if (isBlockElement(parNode) && parNode.nextSibling)
+ origSelectionRange = browserSelection.createRange();
+ }
+ catch (e)
+ {}
+ if (!origSelectionRange) return null;
+ var selectionParent = origSelectionRange.parentElement();
+ if (selectionParent.ownerDocument != doc) return null;
+
+ var newRange = function()
+ {
+ return doc.body.createTextRange();
+ };
+
+ var rangeForElementNode = function(nd)
+ {
+ var rng = newRange();
+ // doesn't work on text nodes
+ rng.moveToElementText(nd);
+ return rng;
+ };
+
+ var pointFromCollapsedRange = function(rng)
+ {
+ var parNode = rng.parentElement();
+ var elemBelow = -1;
+ var elemAbove = parNode.childNodes.length;
+ var rangeWithin = rangeForElementNode(parNode);
+
+ if (rng.compareEndPoints("StartToStart", rangeWithin) === 0)
{
- // caret after block is not consistent across browsers
- // (same line vs next) so put caret before next node
return {
- node: parNode.nextSibling,
+ node: parNode,
index: 0,
maxIndex: 1
};
}
- return {
- node: parNode,
- index: 1,
- maxIndex: 1
- };
- }
- else if (parNode.childNodes.length === 0)
- {
- return {
- node: parNode,
- index: 0,
- maxIndex: 1
- };
- }
-
- for (var i = 0; i < parNode.childNodes.length; i++)
- {
- var n = parNode.childNodes.item(i);
- if (!isNodeText(n))
+ else if (rng.compareEndPoints("EndToEnd", rangeWithin) === 0)
{
- var nodeRange = rangeForElementNode(n);
- var startComp = rng.compareEndPoints("StartToStart", nodeRange);
- var endComp = rng.compareEndPoints("EndToEnd", nodeRange);
- if (startComp >= 0 && endComp <= 0)
+ if (isBlockElement(parNode) && parNode.nextSibling)
{
- var index = 0;
- if (startComp > 0)
- {
- index = 1;
- }
+ // caret after block is not consistent across browsers
+ // (same line vs next) so put caret before next node
return {
- node: n,
- index: index,
+ node: parNode.nextSibling,
+ index: 0,
maxIndex: 1
};
}
- else if (endComp > 0)
- {
- if (i > elemBelow)
- {
- elemBelow = i;
- rangeWithin.setEndPoint("StartToEnd", nodeRange);
- }
- }
- else if (startComp < 0)
- {
- if (i < elemAbove)
- {
- elemAbove = i;
- rangeWithin.setEndPoint("EndToStart", nodeRange);
- }
- }
- }
- }
- if ((elemAbove - elemBelow) == 1)
- {
- if (elemBelow >= 0)
- {
return {
- node: parNode.childNodes.item(elemBelow),
+ node: parNode,
index: 1,
maxIndex: 1
};
}
- else
+ else if (parNode.childNodes.length === 0)
{
return {
- node: parNode.childNodes.item(elemAbove),
+ node: parNode,
index: 0,
maxIndex: 1
};
}
- }
- var idx = 0;
- var r = rng.duplicate();
- // infinite stateful binary search! call function for values 0 to inf,
- // expecting the answer to be about 40. return index of smallest
- // true value.
- var indexIntoRange = binarySearchInfinite(40, function(i)
- {
- // the search algorithm whips the caret back and forth,
- // though it has to be moved relatively and may hit
- // the end of the buffer
- var delta = i - idx;
- var moved = Math.abs(r.move("character", -delta));
- // next line is work-around for fact that when moving left, the beginning
- // of a text node is considered to be after the start of the parent element:
- if (r.move("character", -1)) r.move("character", 1);
- if (delta < 0) idx -= moved;
- else idx += moved;
- return (r.compareEndPoints("StartToStart", rangeWithin) <= 0);
- });
- // iterate over consecutive text nodes, point is in one of them
- var textNode = elemBelow + 1;
- var indexLeft = indexIntoRange;
- while (textNode < elemAbove)
- {
- var tn = parNode.childNodes.item(textNode);
- if (indexLeft <= tn.nodeValue.length)
+
+ for (var i = 0; i < parNode.childNodes.length; i++)
{
- return {
- node: tn,
- index: indexLeft,
- maxIndex: tn.nodeValue.length
- };
- }
- indexLeft -= tn.nodeValue.length;
- textNode++;
- }
- var tn = parNode.childNodes.item(textNode - 1);
- return {
- node: tn,
- index: tn.nodeValue.length,
- maxIndex: tn.nodeValue.length
- };
- };
-
- var selection = {};
- if (origSelectionRange.compareEndPoints("StartToEnd", origSelectionRange) === 0)
- {
- // collapsed
- var pnt = pointFromCollapsedRange(origSelectionRange);
- selection.startPoint = pnt;
- selection.endPoint = {
- node: pnt.node,
- index: pnt.index,
- maxIndex: pnt.maxIndex
- };
- }
- else
- {
- var start = origSelectionRange.duplicate();
- start.collapse(true);
- var end = origSelectionRange.duplicate();
- end.collapse(false);
- selection.startPoint = pointFromCollapsedRange(start);
- selection.endPoint = pointFromCollapsedRange(end);
- }
- return selection;
- }
- else
- {
- // non-IE browser
- var browserSelection = window.getSelection();
- if (browserSelection && browserSelection.type != "None" && browserSelection.rangeCount !== 0)
- {
- var range = browserSelection.getRangeAt(0);
-
- function isInBody(n)
- {
- while (n && !(n.tagName && n.tagName.toLowerCase() == "body"))
- {
- n = n.parentNode;
- }
- return !!n;
- }
-
- function pointFromRangeBound(container, offset)
- {
- if (!isInBody(container))
- {
- // command-click in Firefox selects whole document, HEAD and BODY!
- return {
- node: root,
- index: 0,
- maxIndex: 1
- };
- }
- var n = container;
- var childCount = n.childNodes.length;
- if (isNodeText(n))
- {
- return {
- node: n,
- index: offset,
- maxIndex: n.nodeValue.length
- };
- }
- else if (childCount === 0)
- {
- return {
- node: n,
- index: 0,
- maxIndex: 1
- };
- }
- // treat point between two nodes as BEFORE the second (rather than after the first)
- // if possible; this way point at end of a line block-element is treated as
- // at beginning of next line
- else if (offset == childCount)
- {
- var nd = n.childNodes.item(childCount - 1);
- var max = nodeMaxIndex(nd);
- return {
- node: nd,
- index: max,
- maxIndex: max
- };
- }
- else
- {
- var nd = n.childNodes.item(offset);
- var max = nodeMaxIndex(nd);
- return {
- node: nd,
- index: 0,
- maxIndex: max
- };
- }
- }
- var selection = {};
- selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset);
- selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset);
- selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset));
-
- if(selection.startPoint.node.ownerDocument !== window.document){
- return null;
- }
-
- return selection;
- }
- else return null;
- }
- }
-
- function setSelection(selection)
- {
- function copyPoint(pt)
- {
- return {
- node: pt.node,
- index: pt.index,
- maxIndex: pt.maxIndex
- };
- }
- if (browser.msie)
- {
- // Oddly enough, accessing scrollHeight fixes return key handling on IE 8,
- // presumably by forcing some kind of internal DOM update.
- doc.body.scrollHeight;
-
- function moveToElementText(s, n)
- {
- while (n.firstChild && !isNodeText(n.firstChild))
- {
- n = n.firstChild;
- }
- s.moveToElementText(n);
- }
-
- function newRange()
- {
- return doc.body.createTextRange();
- }
-
- function setCollapsedBefore(s, n)
- {
- // s is an IE TextRange, n is a dom node
- if (isNodeText(n))
- {
- // previous node should not also be text, but prevent inf recurs
- if (n.previousSibling && !isNodeText(n.previousSibling))
- {
- setCollapsedAfter(s, n.previousSibling);
- }
- else
- {
- setCollapsedBefore(s, n.parentNode);
- }
- }
- else
- {
- moveToElementText(s, n);
- // work around for issue that caret at beginning of line
- // somehow ends up at end of previous line
- if (s.move('character', 1))
- {
- s.move('character', -1);
- }
- s.collapse(true); // to start
- }
- }
-
- function setCollapsedAfter(s, n)
- {
- // s is an IE TextRange, n is a magicdom node
- if (isNodeText(n))
- {
- // can't use end of container when no nextSibling (could be on next line),
- // so use previousSibling or start of container and move forward.
- setCollapsedBefore(s, n);
- s.move("character", n.nodeValue.length);
- }
- else
- {
- moveToElementText(s, n);
- s.collapse(false); // to end
- }
- }
-
- function getPointRange(point)
- {
- var s = newRange();
- var n = point.node;
- if (isNodeText(n))
- {
- setCollapsedBefore(s, n);
- s.move("character", point.index);
- }
- else if (point.index === 0)
- {
- setCollapsedBefore(s, n);
- }
- else
- {
- setCollapsedAfter(s, n);
- }
- return s;
- }
-
- if (selection)
- {
- if (!hasIESelection())
- {
- return; // don't steal focus
- }
-
- var startPoint = copyPoint(selection.startPoint);
- var endPoint = copyPoint(selection.endPoint);
-
- // fix issue where selection can't be extended past end of line
- // with shift-rightarrow or shift-downarrow
- if (endPoint.index == endPoint.maxIndex && endPoint.node.nextSibling)
- {
- endPoint.node = endPoint.node.nextSibling;
- endPoint.index = 0;
- endPoint.maxIndex = nodeMaxIndex(endPoint.node);
- }
- var range = getPointRange(startPoint);
- range.setEndPoint("EndToEnd", getPointRange(endPoint));
-
- // setting the selection in IE causes everything to scroll
- // so that the selection is visible. if setting the selection
- // definitely accomplishes nothing, don't do it.
-
-
- function isEqualToDocumentSelection(rng)
- {
- var browserSelection;
- try
- {
- browserSelection = doc.selection;
- }
- catch (e)
- {}
- if (!browserSelection) return false;
- var rng2 = browserSelection.createRange();
- if (rng2.parentElement().ownerDocument != doc) return false;
- if (rng.compareEndPoints("StartToStart", rng2) !== 0) return false;
- if (rng.compareEndPoints("EndToEnd", rng2) !== 0) return false;
- return true;
- }
- if (!isEqualToDocumentSelection(range))
- {
- //dmesg(toSource(selection));
- //dmesg(escapeHTML(doc.body.innerHTML));
- range.select();
- }
- }
- else
- {
- try
- {
- doc.selection.empty();
- }
- catch (e)
- {}
- }
- }
- else
- {
- // non-IE browser
- var isCollapsed;
-
- function pointToRangeBound(pt)
- {
- var p = copyPoint(pt);
- // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level,
- // and also problem where cut/copy of a whole line selected with fake arrow-keys
- // copies the next line too.
- if (isCollapsed)
- {
- function diveDeep()
- {
- while (p.node.childNodes.length > 0)
+ var n = parNode.childNodes.item(i);
+ if (!isNodeText(n))
{
- //&& (p.node == root || p.node.parentNode == root)) {
- if (p.index === 0)
+ var nodeRange = rangeForElementNode(n);
+ var startComp = rng.compareEndPoints("StartToStart", nodeRange);
+ var endComp = rng.compareEndPoints("EndToEnd", nodeRange);
+ if (startComp >= 0 && endComp <= 0)
{
- p.node = p.node.firstChild;
- p.maxIndex = nodeMaxIndex(p.node);
+ var index = 0;
+ if (startComp > 0)
+ {
+ index = 1;
+ }
+ return {
+ node: n,
+ index: index,
+ maxIndex: 1
+ };
}
- else if (p.index == p.maxIndex)
+ else if (endComp > 0)
{
- p.node = p.node.lastChild;
- p.maxIndex = nodeMaxIndex(p.node);
- p.index = p.maxIndex;
+ if (i > elemBelow)
+ {
+ elemBelow = i;
+ rangeWithin.setEndPoint("StartToEnd", nodeRange);
+ }
+ }
+ else if (startComp < 0)
+ {
+ if (i < elemAbove)
+ {
+ elemAbove = i;
+ rangeWithin.setEndPoint("EndToStart", nodeRange);
+ }
}
- else break;
}
}
- // now fix problem where cursor at end of text node at end of span-like element
- // with background doesn't seem to show up...
- if (isNodeText(p.node) && p.index == p.maxIndex)
+ if ((elemAbove - elemBelow) == 1)
{
- var n = p.node;
- while ((!n.nextSibling) && (n != root) && (n.parentNode != root))
+ if (elemBelow >= 0)
+ {
+ return {
+ node: parNode.childNodes.item(elemBelow),
+ index: 1,
+ maxIndex: 1
+ };
+ }
+ else
+ {
+ return {
+ node: parNode.childNodes.item(elemAbove),
+ index: 0,
+ maxIndex: 1
+ };
+ }
+ }
+ var idx = 0;
+ var r = rng.duplicate();
+ // infinite stateful binary search! call function for values 0 to inf,
+ // expecting the answer to be about 40. return index of smallest
+ // true value.
+ var indexIntoRange = binarySearchInfinite(40, function(i)
+ {
+ // the search algorithm whips the caret back and forth,
+ // though it has to be moved relatively and may hit
+ // the end of the buffer
+ var delta = i - idx;
+ var moved = Math.abs(r.move("character", -delta));
+ // next line is work-around for fact that when moving left, the beginning
+ // of a text node is considered to be after the start of the parent element:
+ if (r.move("character", -1)) r.move("character", 1);
+ if (delta < 0) idx -= moved;
+ else idx += moved;
+ return (r.compareEndPoints("StartToStart", rangeWithin) <= 0);
+ });
+ // iterate over consecutive text nodes, point is in one of them
+ var textNode = elemBelow + 1;
+ var indexLeft = indexIntoRange;
+ while (textNode < elemAbove)
+ {
+ var tn = parNode.childNodes.item(textNode);
+ if (indexLeft <= tn.nodeValue.length)
+ {
+ return {
+ node: tn,
+ index: indexLeft,
+ maxIndex: tn.nodeValue.length
+ };
+ }
+ indexLeft -= tn.nodeValue.length;
+ textNode++;
+ }
+ var tn = parNode.childNodes.item(textNode - 1);
+ return {
+ node: tn,
+ index: tn.nodeValue.length,
+ maxIndex: tn.nodeValue.length
+ };
+ };
+
+ var selection = {};
+ if (origSelectionRange.compareEndPoints("StartToEnd", origSelectionRange) === 0)
+ {
+ // collapsed
+ var pnt = pointFromCollapsedRange(origSelectionRange);
+ selection.startPoint = pnt;
+ selection.endPoint = {
+ node: pnt.node,
+ index: pnt.index,
+ maxIndex: pnt.maxIndex
+ };
+ }
+ else
+ {
+ var start = origSelectionRange.duplicate();
+ start.collapse(true);
+ var end = origSelectionRange.duplicate();
+ end.collapse(false);
+ selection.startPoint = pointFromCollapsedRange(start);
+ selection.endPoint = pointFromCollapsedRange(end);
+ }
+ return selection;
+ }
+ else
+ {
+ // non-IE browser
+ var browserSelection = window.getSelection();
+ if (browserSelection && browserSelection.type != "None" && browserSelection.rangeCount !== 0)
+ {
+ var range = browserSelection.getRangeAt(0);
+
+ function isInBody(n)
+ {
+ while (n && !(n.tagName && n.tagName.toLowerCase() == "body"))
{
n = n.parentNode;
}
- if (n.nextSibling && (!((typeof n.nextSibling.tagName) == "string" && n.nextSibling.tagName.toLowerCase() == "br")) && (n != p.node) && (n != root) && (n.parentNode != root))
+ return !!n;
+ }
+
+ function pointFromRangeBound(container, offset)
+ {
+ if (!isInBody(container))
{
- // found a parent, go to next node and dive in
- p.node = n.nextSibling;
- p.maxIndex = nodeMaxIndex(p.node);
- p.index = 0;
- diveDeep();
+ // command-click in Firefox selects whole document, HEAD and BODY!
+ return {
+ node: root,
+ index: 0,
+ maxIndex: 1
+ };
+ }
+ var n = container;
+ var childCount = n.childNodes.length;
+ if (isNodeText(n))
+ {
+ return {
+ node: n,
+ index: offset,
+ maxIndex: n.nodeValue.length
+ };
+ }
+ else if (childCount === 0)
+ {
+ return {
+ node: n,
+ index: 0,
+ maxIndex: 1
+ };
+ }
+ // treat point between two nodes as BEFORE the second (rather than after the first)
+ // if possible; this way point at end of a line block-element is treated as
+ // at beginning of next line
+ else if (offset == childCount)
+ {
+ var nd = n.childNodes.item(childCount - 1);
+ var max = nodeMaxIndex(nd);
+ return {
+ node: nd,
+ index: max,
+ maxIndex: max
+ };
+ }
+ else
+ {
+ var nd = n.childNodes.item(offset);
+ var max = nodeMaxIndex(nd);
+ return {
+ node: nd,
+ index: 0,
+ maxIndex: max
+ };
}
}
- // try to make sure insertion point is styled;
- // also fixes other FF problems
- if (!isNodeText(p.node))
- {
- diveDeep();
- }
- }
- if (isNodeText(p.node))
- {
- return {
- container: p.node,
- offset: p.index
- };
- }
- else
- {
- // p.index in {0,1}
- return {
- container: p.node.parentNode,
- offset: childIndex(p.node) + p.index
- };
- }
- }
- var browserSelection = window.getSelection();
- if (browserSelection)
- {
- browserSelection.removeAllRanges();
- if (selection)
- {
- isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index);
- var start = pointToRangeBound(selection.startPoint);
- var end = pointToRangeBound(selection.endPoint);
+ var selection = {};
+ selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset);
+ selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset);
+ selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset));
- if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend)
+ if(selection.startPoint.node.ownerDocument !== window.document){
+ return null;
+ }
+
+ return selection;
+ }
+ else return null;
+ }
+ }
+
+ function setSelection(selection)
+ {
+ function copyPoint(pt)
+ {
+ return {
+ node: pt.node,
+ index: pt.index,
+ maxIndex: pt.maxIndex
+ };
+ }
+ if (browser.msie)
+ {
+ // Oddly enough, accessing scrollHeight fixes return key handling on IE 8,
+ // presumably by forcing some kind of internal DOM update.
+ doc.body.scrollHeight;
+
+ function moveToElementText(s, n)
+ {
+ while (n.firstChild && !isNodeText(n.firstChild))
{
- // can handle "backwards"-oriented selection, shift-arrow-keys move start
- // of selection
- browserSelection.collapse(end.container, end.offset);
- //console.trace();
- //console.log(htmlPrettyEscape(rep.alltext));
- //console.log("%o %o", rep.selStart, rep.selEnd);
- //console.log("%o %d", start.container, start.offset);
- browserSelection.extend(start.container, start.offset);
+ n = n.firstChild;
+ }
+ s.moveToElementText(n);
+ }
+
+ function newRange()
+ {
+ return doc.body.createTextRange();
+ }
+
+ function setCollapsedBefore(s, n)
+ {
+ // s is an IE TextRange, n is a dom node
+ if (isNodeText(n))
+ {
+ // previous node should not also be text, but prevent inf recurs
+ if (n.previousSibling && !isNodeText(n.previousSibling))
+ {
+ setCollapsedAfter(s, n.previousSibling);
+ }
+ else
+ {
+ setCollapsedBefore(s, n.parentNode);
+ }
}
else
{
- var range = doc.createRange();
- range.setStart(start.container, start.offset);
- range.setEnd(end.container, end.offset);
- browserSelection.removeAllRanges();
- browserSelection.addRange(range);
+ moveToElementText(s, n);
+ // work around for issue that caret at beginning of line
+ // somehow ends up at end of previous line
+ if (s.move('character', 1))
+ {
+ s.move('character', -1);
+ }
+ s.collapse(true); // to start
}
}
- }
- }
- }
- function childIndex(n)
- {
- var idx = 0;
- while (n.previousSibling)
- {
- idx++;
- n = n.previousSibling;
- }
- return idx;
- }
-
- function fixView()
- {
- //return; // TODO: look into this later
- // calling this method repeatedly should be fast
- if (getInnerWidth() === 0 || getInnerHeight() === 0)
- {
- return;
- }
-
- function setIfNecessary(obj, prop, value)
- {
- if (obj[prop] != value)
- {
- obj[prop] = value;
- }
- }
-
- var lineNumberWidth = sideDiv.firstChild.offsetWidth;
- var newSideDivWidth = lineNumberWidth + LINE_NUMBER_PADDING_LEFT;
- if (newSideDivWidth < MIN_LINEDIV_WIDTH) newSideDivWidth = MIN_LINEDIV_WIDTH;
- iframePadLeft = EDIT_BODY_PADDING_LEFT;
- if (hasLineNumbers) iframePadLeft += newSideDivWidth + LINE_NUMBER_PADDING_RIGHT;
- setIfNecessary(innerdocbody.style, "left", iframePadLeft + "px");
- setIfNecessary(sideDiv.style, "width", newSideDivWidth + "px");
-
- for (var i = 0; i < 2; i++)
- {
- var newHeight = root.clientHeight;
- var newWidth = (browser.msie ? root.createTextRange().boundingWidth : root.clientWidth);
- var viewHeight = getInnerHeight() - iframePadBottom - iframePadTop;
- var viewWidth = getInnerWidth() - iframePadLeft - iframePadRight;
- if (newHeight < viewHeight)
- {
- newHeight = viewHeight;
- if (browser.msie) setIfNecessary(window.document.documentElement.style, 'overflowY', 'auto');
- }
- else
- {
- if (browser.msie) setIfNecessary(window.document.documentElement.style, 'overflowY', 'scroll');
- }
- if (doesWrap)
- {
- newWidth = viewWidth;
- }
- else
- {
- if (newWidth < viewWidth) newWidth = viewWidth;
- }
- setIfNecessary(innerdocbody.style, "height", newHeight + "px");
- setIfNecessary(innerdocbody.style, "width", newWidth + "px");
- setIfNecessary(sideDiv.style, "height", newHeight + "px");
- }
- if (browser.firefox)
- {
- if (!doesWrap)
- {
- // the body:display:table-cell hack makes mozilla do scrolling
- // correctly by shrinking the to fit around its content,
- // but mozilla won't act on clicks below the body. We keep the
- // style.height property set to the viewport height (editor height
- // not including scrollbar), so it will never shrink so that part of
- // the editor isn't clickable.
- var body = root;
- var styleHeight = viewHeight + "px";
- setIfNecessary(body.style, "height", styleHeight);
- }
- else
- {
- setIfNecessary(root.style, "height", "");
- }
- }
- // if near edge, scroll to edge
- var scrollX = getScrollX();
- var scrollY = getScrollY();
- var win = window;
- var r = 20;
-
- enforceEditability();
-
- $(sideDiv).addClass('sidedivdelayed');
- }
-
- function getScrollXY()
- {
- var win = window;
- var odoc = window.document;
- if (typeof(win.pageYOffset) == "number")
- {
- return {
- x: win.pageXOffset,
- y: win.pageYOffset
- };
- }
- var docel = odoc.documentElement;
- if (docel && typeof(docel.scrollTop) == "number")
- {
- return {
- x: docel.scrollLeft,
- y: docel.scrollTop
- };
- }
- }
-
- function getScrollX()
- {
- return getScrollXY().x;
- }
-
- function getScrollY()
- {
- return getScrollXY().y;
- }
-
- function setScrollX(x)
- {
- window.scrollTo(x, getScrollY());
- }
-
- function setScrollY(y)
- {
- window.scrollTo(getScrollX(), y);
- }
-
- function setScrollXY(x, y)
- {
- window.scrollTo(x, y);
- }
-
- var _teardownActions = [];
-
- function teardown()
- {
- _.each(_teardownActions, function(a)
- {
- a();
- });
- }
-
- function setDesignMode(newVal)
- {
- try
- {
- function setIfNecessary(target, prop, val)
- {
- if (String(target[prop]).toLowerCase() != val)
+ function setCollapsedAfter(s, n)
{
- target[prop] = val;
- return true;
+ // s is an IE TextRange, n is a magicdom node
+ if (isNodeText(n))
+ {
+ // can't use end of container when no nextSibling (could be on next line),
+ // so use previousSibling or start of container and move forward.
+ setCollapsedBefore(s, n);
+ s.move("character", n.nodeValue.length);
+ }
+ else
+ {
+ moveToElementText(s, n);
+ s.collapse(false); // to end
+ }
}
- return false;
- }
- if (browser.msie || browser.safari)
- {
- setIfNecessary(root, 'contentEditable', (newVal ? 'true' : 'false'));
- }
- else
- {
- var wasSet = setIfNecessary(doc, 'designMode', (newVal ? 'on' : 'off'));
- if (wasSet && newVal && browser.opera)
+
+ function getPointRange(point)
{
- // turning on designMode clears event handlers
- bindTheEventHandlers();
+ var s = newRange();
+ var n = point.node;
+ if (isNodeText(n))
+ {
+ setCollapsedBefore(s, n);
+ s.move("character", point.index);
+ }
+ else if (point.index === 0)
+ {
+ setCollapsedBefore(s, n);
+ }
+ else
+ {
+ setCollapsedAfter(s, n);
+ }
+ return s;
}
- }
- return true;
- }
- catch (e)
- {
- return false;
- }
- }
- var iePastedLines = null;
-
- function handleIEPaste(evt)
- {
- // Pasting in IE loses blank lines in a way that loses information;
- // "one\n\ntwo\nthree" becomes "one
two
three
",
- // which becomes "one\ntwo\nthree". We can get the correct text
- // from the clipboard directly, but we still have to let the paste
- // happen to get the style information.
- var clipText = window.clipboardData && window.clipboardData.getData("Text");
- if (clipText && doc.selection)
- {
- // this "paste" event seems to mess with the selection whether we try to
- // stop it or not, so can't really do document-level manipulation now
- // or in an idle call-stack. instead, use IE native manipulation
- //function escapeLine(txt) {
- //return processSpaces(escapeHTML(textify(txt)));
- //}
- //var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('
');
- //doc.selection.createRange().pasteHTML(newHTML);
- //evt.preventDefault();
- //iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify);
- }
- }
-
-
- 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;
- }
- }
-
- editorInfo.ace_getInInternationalComposition = function ()
- {
- return inInternationalComposition;
- }
-
- function bindTheEventHandlers()
- {
- $(document).on("keydown", handleKeyEvent);
- $(document).on("keypress", handleKeyEvent);
- $(document).on("keyup", handleKeyEvent);
- $(document).on("click", handleClick);
-
- // Disabled: https://github.com/ether/etherpad-lite/issues/2546
- // Will break OL re-numbering: https://github.com/ether/etherpad-lite/pull/2533
- // $(document).on("cut", handleCut);
-
- $(root).on("blur", handleBlur);
- if (browser.msie)
- {
- $(document).on("click", handleIEOuterClick);
- }
- if (browser.msie) $(root).on("paste", handleIEPaste);
-
- // Don't paste on middle click of links
- $(root).on("paste", function(e){
- // TODO: this breaks pasting strings into URLS when using
- // Control C and Control V -- the Event is never available
- // here.. :(
- if(e.target.a || e.target.localName === "a"){
- e.preventDefault();
- }
- })
-
- // CompositionEvent is not implemented below IE version 8
- if ( !(browser.msie && parseInt(browser.version <= 9)) && document.documentElement)
- {
- $(document.documentElement).on("compositionstart", handleCompositionEvent);
- $(document.documentElement).on("compositionend", handleCompositionEvent);
- }
- }
-
- function handleIEOuterClick(evt)
- {
- if ((evt.target.tagName || '').toLowerCase() != "html")
- {
- return;
- }
- if (!(evt.pageY > root.clientHeight))
- {
- return;
- }
-
- // click below the body
- inCallStackIfNecessary("handleOuterClick", function()
- {
- // put caret at bottom of doc
- fastIncorp(11);
- if (isCaret())
- { // don't interfere with drag
- var lastLine = rep.lines.length() - 1;
- var lastCol = rep.lines.atIndex(lastLine).text.length;
- performSelectionChange([lastLine, lastCol], [lastLine, lastCol]);
- }
- });
- }
-
- function getClassArray(elem, optFilter)
- {
- var bodyClasses = [];
- (elem.className || '').replace(/\S+/g, function(c)
- {
- if ((!optFilter) || (optFilter(c)))
- {
- bodyClasses.push(c);
- }
- });
- return bodyClasses;
- }
-
- function setClassArray(elem, array)
- {
- elem.className = array.join(' ');
- }
-
- function setClassPresence(elem, className, present)
- {
- if (present) $(elem).addClass(className);
- else $(elem).removeClass(className);
- }
-
- function focus()
- {
- window.focus();
- }
-
- function handleBlur(evt)
- {
- if (browser.msie)
- {
- // a fix: in IE, clicking on a control like a button outside the
- // iframe can "blur" the editor, causing it to stop getting
- // events, though typing still affects it(!).
- setSelection(null);
- }
- }
-
- function getSelectionPointX(point)
- {
- // doesn't work in wrap-mode
- var node = point.node;
- var index = point.index;
-
- function leftOf(n)
- {
- return n.offsetLeft;
- }
-
- function rightOf(n)
- {
- return n.offsetLeft + n.offsetWidth;
- }
- if (!isNodeText(node))
- {
- if (index === 0) return leftOf(node);
- else return rightOf(node);
- }
- else
- {
- // we can get bounds of element nodes, so look for those.
- // allow consecutive text nodes for robustness.
- var charsToLeft = index;
- var charsToRight = node.nodeValue.length - index;
- var n;
- for (n = node.previousSibling; n && isNodeText(n); n = n.previousSibling)
- charsToLeft += n.nodeValue;
- var leftEdge = (n ? rightOf(n) : leftOf(node.parentNode));
- for (n = node.nextSibling; n && isNodeText(n); n = n.nextSibling)
- charsToRight += n.nodeValue;
- var rightEdge = (n ? leftOf(n) : rightOf(node.parentNode));
- var frac = (charsToLeft / (charsToLeft + charsToRight));
- var pixLoc = leftEdge + frac * (rightEdge - leftEdge);
- return Math.round(pixLoc);
- }
- }
-
- function getPageHeight()
- {
- var win = window;
- var odoc = win.document;
- if (win.innerHeight && win.scrollMaxY) return win.innerHeight + win.scrollMaxY;
- else if (odoc.body.scrollHeight > odoc.body.offsetHeight) return odoc.body.scrollHeight;
- else return odoc.body.offsetHeight;
- }
-
- function getPageWidth()
- {
- var win = window;
- var odoc = win.document;
- if (win.innerWidth && win.scrollMaxX) return win.innerWidth + win.scrollMaxX;
- else if (odoc.body.scrollWidth > odoc.body.offsetWidth) return odoc.body.scrollWidth;
- else return odoc.body.offsetWidth;
- }
-
- function getInnerHeight()
- {
- var win = window;
- var odoc = win.document;
- var h;
- if (browser.opera) h = win.innerHeight;
- else h = odoc.documentElement.clientHeight;
- if (h) return h;
-
- // deal with case where iframe is hidden, hope that
- // style.height of iframe container is set in px
- return Number(editorInfo.frame.parentNode.style.height.replace(/[^0-9]/g, '') || 0);
- }
-
- function getInnerWidth()
- {
- var win = window;
- var odoc = win.document;
- return odoc.documentElement.clientWidth;
- }
-
- function scrollNodeVerticallyIntoView(node)
- {
- // requires element (non-text) node;
- // if node extends above top of viewport or below bottom of viewport (or top of scrollbar),
- // scroll it the minimum distance needed to be completely in view.
- var win = window;
- var odoc = window.document;
- var distBelowTop = node.offsetTop + iframePadTop - win.scrollY;
- var distAboveBottom = win.scrollY + getInnerHeight() - (node.offsetTop + iframePadTop + node.offsetHeight);
-
- if (distBelowTop < 0)
- {
- win.scrollBy(0, distBelowTop);
- }
- else if (distAboveBottom < 0)
- {
- win.scrollBy(0, -distAboveBottom);
- }
- }
-
- function scrollXHorizontallyIntoView(pixelX)
- {
- var win = window;
- var odoc = window.document;
- pixelX += iframePadLeft;
- var distInsideLeft = pixelX - win.scrollX;
- var distInsideRight = win.scrollX + getInnerWidth() - pixelX;
- if (distInsideLeft < 0)
- {
- win.scrollBy(distInsideLeft, 0);
- }
- else if (distInsideRight < 0)
- {
- win.scrollBy(-distInsideRight + 1, 0);
- }
- }
-
- function scrollSelectionIntoView()
- {
- if (!rep.selStart) return;
- fixView();
- var focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
- scrollNodeVerticallyIntoView(rep.lines.atIndex(focusLine).lineNode);
- if (!doesWrap)
- {
- var browserSelection = getSelection();
- if (browserSelection)
- {
- var focusPoint = (browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint);
- var selectionPointX = getSelectionPointX(focusPoint);
- scrollXHorizontallyIntoView(selectionPointX);
- fixView();
- }
- }
- }
-
- var listAttributeName = 'list';
-
- function getLineListType(lineNum)
- {
- return documentAttributeManager.getAttributeOnLine(lineNum, listAttributeName)
- }
-
- function setLineListType(lineNum, listType)
- {
- if(listType == ''){
- documentAttributeManager.removeAttributeOnLine(lineNum, listAttributeName);
- documentAttributeManager.removeAttributeOnLine(lineNum, 'start');
- }else{
- documentAttributeManager.setAttributeOnLine(lineNum, listAttributeName, listType);
- }
-
- //if the list has been removed, it is necessary to renumber
- //starting from the *next* line because the list may have been
- //separated. If it returns null, it means that the list was not cut, try
- //from the current one.
- if(renumberList(lineNum+1)==null)
- {
- renumberList(lineNum);
- }
- }
-
- function renumberList(lineNum){
- //1-check we are in a list
- var type = getLineListType(lineNum);
- if(!type)
- {
- return null;
- }
- type = /([a-z]+)[0-9]+/.exec(type);
- if(type[1] == "indent")
- {
- return null;
- }
-
- //2-find the first line of the list
- while(lineNum-1 >= 0 && (type=getLineListType(lineNum-1)))
- {
- type = /([a-z]+)[0-9]+/.exec(type);
- if(type[1] == "indent")
- break;
- lineNum--;
- }
-
- //3-renumber every list item of the same level from the beginning, level 1
- //IMPORTANT: never skip a level because there imbrication may be arbitrary
- var builder = Changeset.builder(rep.lines.totalWidth());
- var loc = [0,0];
- function applyNumberList(line, level)
- {
- //init
- var position = 1;
- var curLevel = level;
- var listType;
- //loop over the lines
- while(listType = getLineListType(line))
- {
- //apply new num
- listType = /([a-z]+)([0-9]+)/.exec(listType);
- curLevel = Number(listType[2]);
- if(isNaN(curLevel) || listType[0] == "indent")
+ if (selection)
{
- return line;
- }
- else if(curLevel == level)
- {
- ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
- ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
- ['start', position]
- ], rep.apool);
+ if (!hasIESelection())
+ {
+ return; // don't steal focus
+ }
- position++;
- line++;
- }
- else if(curLevel < level)
- {
- return line;//back to parent
+ var startPoint = copyPoint(selection.startPoint);
+ var endPoint = copyPoint(selection.endPoint);
+
+ // fix issue where selection can't be extended past end of line
+ // with shift-rightarrow or shift-downarrow
+ if (endPoint.index == endPoint.maxIndex && endPoint.node.nextSibling)
+ {
+ endPoint.node = endPoint.node.nextSibling;
+ endPoint.index = 0;
+ endPoint.maxIndex = nodeMaxIndex(endPoint.node);
+ }
+ var range = getPointRange(startPoint);
+ range.setEndPoint("EndToEnd", getPointRange(endPoint));
+
+ // setting the selection in IE causes everything to scroll
+ // so that the selection is visible. if setting the selection
+ // definitely accomplishes nothing, don't do it.
+
+
+ function isEqualToDocumentSelection(rng)
+ {
+ var browserSelection;
+ try
+ {
+ browserSelection = doc.selection;
+ }
+ catch (e)
+ {}
+ if (!browserSelection) return false;
+ var rng2 = browserSelection.createRange();
+ if (rng2.parentElement().ownerDocument != doc) return false;
+ if (rng.compareEndPoints("StartToStart", rng2) !== 0) return false;
+ if (rng.compareEndPoints("EndToEnd", rng2) !== 0) return false;
+ return true;
+ }
+ if (!isEqualToDocumentSelection(range))
+ {
+ //dmesg(toSource(selection));
+ //dmesg(escapeHTML(doc.body.innerHTML));
+ range.select();
+ }
}
else
{
- line = applyNumberList(line, level+1);//recursive call
+ try
+ {
+ doc.selection.empty();
+ }
+ catch (e)
+ {}
}
}
- return line;
- }
-
- applyNumberList(lineNum, 1);
- var cs = builder.toString();
- if (!Changeset.isIdentity(cs))
- {
- performDocumentApplyChangeset(cs);
- }
-
- //4-apply the modifications
-
-
- }
-
-
- function doInsertList(type)
- {
- if (!(rep.selStart && rep.selEnd))
- {
- return;
- }
-
- var firstLine, lastLine;
- firstLine = rep.selStart[0];
- lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0));
-
- var allLinesAreList = true;
- for (var n = firstLine; n <= lastLine; n++)
- {
- var listType = getLineListType(n);
- if (!listType || listType.slice(0, type.length) != type)
+ else
{
- allLinesAreList = false;
- break;
- }
- }
+ // non-IE browser
+ var isCollapsed;
- var mods = [];
- for (var n = firstLine; n <= lastLine; n++)
- {
- var t = '';
- var level = 0;
- var listType = /([a-z]+)([0-9]+)/.exec(getLineListType(n));
- if (listType)
- {
- t = listType[1];
- level = Number(listType[2]);
- }
- var t = getLineListType(n);
- mods.push([n, allLinesAreList ? 'indent' + level : (t ? type + level : type + '1')]);
- }
-
- _.each(mods, function(mod){
- setLineListType(mod[0], mod[1]);
- });
- }
-
- function doInsertUnorderedList(){
- doInsertList('bullet');
- }
- function doInsertOrderedList(){
- doInsertList('number');
- }
- editorInfo.ace_doInsertUnorderedList = doInsertUnorderedList;
- editorInfo.ace_doInsertOrderedList = doInsertOrderedList;
-
- var lineNumbersShown;
- var sideDivInner;
-
- function initLineNumbers()
- {
- lineNumbersShown = 1;
- sideDiv.innerHTML = '';
- sideDivInner = window.document.getElementById("sidedivinner");
- }
-
- function updateLineNumbers()
- {
- var newNumLines = rep.lines.length();
- if (newNumLines < 1) newNumLines = 1;
- //update height of all current line numbers
-
- var a = sideDivInner.firstChild;
- var b = innerdocbody.firstChild;
- var n = 0;
-
- if (currentCallStack && currentCallStack.domClean)
- {
-
- while (a && b)
- {
- if(n > lineNumbersShown) //all updated, break
- break;
- var h = (b.clientHeight || b.offsetHeight);
- if (b.nextSibling)
+ function pointToRangeBound(pt)
{
- // when text is zoomed in mozilla, divs have fractional
- // heights (though the properties are always integers)
- // and the line-numbers don't line up unless we pay
- // attention to where the divs are actually placed...
- // (also: padding on TTs/SPANs in IE...)
- h = b.nextSibling.offsetTop - b.offsetTop;
- }
- if (h)
- {
- var hpx = h + "px";
- if (a.style.height != hpx) {
- a.style.height = hpx;
+ var p = copyPoint(pt);
+ // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level,
+ // and also problem where cut/copy of a whole line selected with fake arrow-keys
+ // copies the next line too.
+ if (isCollapsed)
+ {
+ function diveDeep()
+ {
+ while (p.node.childNodes.length > 0)
+ {
+ //&& (p.node == root || p.node.parentNode == root)) {
+ if (p.index === 0)
+ {
+ p.node = p.node.firstChild;
+ p.maxIndex = nodeMaxIndex(p.node);
+ }
+ else if (p.index == p.maxIndex)
+ {
+ p.node = p.node.lastChild;
+ p.maxIndex = nodeMaxIndex(p.node);
+ p.index = p.maxIndex;
+ }
+ else break;
+ }
+ }
+ // now fix problem where cursor at end of text node at end of span-like element
+ // with background doesn't seem to show up...
+ if (isNodeText(p.node) && p.index == p.maxIndex)
+ {
+ var n = p.node;
+ while ((!n.nextSibling) && (n != root) && (n.parentNode != root))
+ {
+ n = n.parentNode;
+ }
+ if (n.nextSibling && (!((typeof n.nextSibling.tagName) == "string" && n.nextSibling.tagName.toLowerCase() == "br")) && (n != p.node) && (n != root) && (n.parentNode != root))
+ {
+ // found a parent, go to next node and dive in
+ p.node = n.nextSibling;
+ p.maxIndex = nodeMaxIndex(p.node);
+ p.index = 0;
+ diveDeep();
+ }
+ }
+ // try to make sure insertion point is styled;
+ // also fixes other FF problems
+ if (!isNodeText(p.node))
+ {
+ diveDeep();
+ }
+ }
+ if (isNodeText(p.node))
+ {
+ return {
+ container: p.node,
+ offset: p.index
+ };
+ }
+ else
+ {
+ // p.index in {0,1}
+ return {
+ container: p.node.parentNode,
+ offset: childIndex(p.node) + p.index
+ };
+ }
+ }
+ var browserSelection = window.getSelection();
+ if (browserSelection)
+ {
+ browserSelection.removeAllRanges();
+ if (selection)
+ {
+ isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index);
+ var start = pointToRangeBound(selection.startPoint);
+ var end = pointToRangeBound(selection.endPoint);
+
+ if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend)
+ {
+ // can handle "backwards"-oriented selection, shift-arrow-keys move start
+ // of selection
+ browserSelection.collapse(end.container, end.offset);
+ //console.trace();
+ //console.log(htmlPrettyEscape(rep.alltext));
+ //console.log("%o %o", rep.selStart, rep.selEnd);
+ //console.log("%o %d", start.container, start.offset);
+ browserSelection.extend(start.container, start.offset);
+ }
+ else
+ {
+ var range = doc.createRange();
+ range.setStart(start.container, start.offset);
+ range.setEnd(end.container, end.offset);
+ browserSelection.removeAllRanges();
+ browserSelection.addRange(range);
+ }
}
}
- a = a.nextSibling;
- b = b.nextSibling;
- n++;
}
}
- if (newNumLines != lineNumbersShown)
+ function childIndex(n)
{
- var container = sideDivInner;
- var odoc = window.document;
- var fragment = odoc.createDocumentFragment();
-
- while (lineNumbersShown < newNumLines)
+ var idx = 0;
+ while (n.previousSibling)
{
- lineNumbersShown++;
- var n = lineNumbersShown;
- var div = odoc.createElement("DIV");
- //calculate height for new line number
- if(b){
- var h = (b.clientHeight || b.offsetHeight);
+ idx++;
+ n = n.previousSibling;
+ }
+ return idx;
+ }
- if (b.nextSibling){
+ function fixView()
+ {
+ //return; // TODO: look into this later
+ // calling this method repeatedly should be fast
+ if (getInnerWidth() === 0 || getInnerHeight() === 0)
+ {
+ return;
+ }
+
+ function setIfNecessary(obj, prop, value)
+ {
+ if (obj[prop] != value)
+ {
+ obj[prop] = value;
+ }
+ }
+
+ var lineNumberWidth = sideDiv.firstChild.offsetWidth;
+ var newSideDivWidth = lineNumberWidth + LINE_NUMBER_PADDING_LEFT;
+ if (newSideDivWidth < MIN_LINEDIV_WIDTH) newSideDivWidth = MIN_LINEDIV_WIDTH;
+ iframePadLeft = EDIT_BODY_PADDING_LEFT;
+ if (hasLineNumbers) iframePadLeft += newSideDivWidth + LINE_NUMBER_PADDING_RIGHT;
+ setIfNecessary(innerdocbody.style, "left", iframePadLeft + "px");
+ setIfNecessary(sideDiv.style, "width", newSideDivWidth + "px");
+
+ for (var i = 0; i < 2; i++)
+ {
+ var newHeight = root.clientHeight;
+ var newWidth = (browser.msie ? root.createTextRange().boundingWidth : root.clientWidth);
+ var viewHeight = getInnerHeight() - iframePadBottom - iframePadTop;
+ var viewWidth = getInnerWidth() - iframePadLeft - iframePadRight;
+ if (newHeight < viewHeight)
+ {
+ newHeight = viewHeight;
+ if (browser.msie) setIfNecessary(window.document.documentElement.style, 'overflowY', 'auto');
+ }
+ else
+ {
+ if (browser.msie) setIfNecessary(window.document.documentElement.style, 'overflowY', 'scroll');
+ }
+ if (doesWrap)
+ {
+ newWidth = viewWidth;
+ }
+ else
+ {
+ if (newWidth < viewWidth) newWidth = viewWidth;
+ }
+ setIfNecessary(innerdocbody.style, "height", newHeight + "px");
+ setIfNecessary(innerdocbody.style, "width", newWidth + "px");
+ setIfNecessary(sideDiv.style, "height", newHeight + "px");
+ }
+ if (browser.firefox)
+ {
+ if (!doesWrap)
+ {
+ // the body:display:table-cell hack makes mozilla do scrolling
+ // correctly by shrinking the to fit around its content,
+ // but mozilla won't act on clicks below the body. We keep the
+ // style.height property set to the viewport height (editor height
+ // not including scrollbar), so it will never shrink so that part of
+ // the editor isn't clickable.
+ var body = root;
+ var styleHeight = viewHeight + "px";
+ setIfNecessary(body.style, "height", styleHeight);
+ }
+ else
+ {
+ setIfNecessary(root.style, "height", "");
+ }
+ }
+ // if near edge, scroll to edge
+ var scrollX = getScrollX();
+ var scrollY = getScrollY();
+ var win = window;
+ var r = 20;
+
+ enforceEditability();
+
+ $(sideDiv).addClass('sidedivdelayed');
+ }
+
+ function getScrollXY()
+ {
+ var win = window;
+ var odoc = window.document;
+ if (typeof(win.pageYOffset) == "number")
+ {
+ return {
+ x: win.pageXOffset,
+ y: win.pageYOffset
+ };
+ }
+ var docel = odoc.documentElement;
+ if (docel && typeof(docel.scrollTop) == "number")
+ {
+ return {
+ x: docel.scrollLeft,
+ y: docel.scrollTop
+ };
+ }
+ }
+
+ function getScrollX()
+ {
+ return getScrollXY().x;
+ }
+
+ function getScrollY()
+ {
+ return getScrollXY().y;
+ }
+
+ function setScrollX(x)
+ {
+ window.scrollTo(x, getScrollY());
+ }
+
+ function setScrollY(y)
+ {
+ window.scrollTo(getScrollX(), y);
+ }
+
+ function setScrollXY(x, y)
+ {
+ window.scrollTo(x, y);
+ }
+
+ var _teardownActions = [];
+
+ function teardown()
+ {
+ _.each(_teardownActions, function(a)
+ {
+ a();
+ });
+ }
+
+ function setDesignMode(newVal)
+ {
+ try
+ {
+ function setIfNecessary(target, prop, val)
+ {
+ if (String(target[prop]).toLowerCase() != val)
+ {
+ target[prop] = val;
+ return true;
+ }
+ return false;
+ }
+ if (browser.msie || browser.safari)
+ {
+ setIfNecessary(root, 'contentEditable', (newVal ? 'true' : 'false'));
+ }
+ else
+ {
+ var wasSet = setIfNecessary(doc, 'designMode', (newVal ? 'on' : 'off'));
+ if (wasSet && newVal && browser.opera)
+ {
+ // turning on designMode clears event handlers
+ bindTheEventHandlers();
+ }
+ }
+ return true;
+ }
+ catch (e)
+ {
+ return false;
+ }
+ }
+
+ var iePastedLines = null;
+
+ function handleIEPaste(evt)
+ {
+ // Pasting in IE loses blank lines in a way that loses information;
+ // "one\n\ntwo\nthree" becomes "one
two
three
",
+ // which becomes "one\ntwo\nthree". We can get the correct text
+ // from the clipboard directly, but we still have to let the paste
+ // happen to get the style information.
+ var clipText = window.clipboardData && window.clipboardData.getData("Text");
+ if (clipText && doc.selection)
+ {
+ // this "paste" event seems to mess with the selection whether we try to
+ // stop it or not, so can't really do document-level manipulation now
+ // or in an idle call-stack. instead, use IE native manipulation
+ //function escapeLine(txt) {
+ //return processSpaces(escapeHTML(textify(txt)));
+ //}
+ //var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('
');
+ //doc.selection.createRange().pasteHTML(newHTML);
+ //evt.preventDefault();
+ //iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify);
+ }
+ }
+
+
+ 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;
+ }
+ }
+
+ editorInfo.ace_getInInternationalComposition = function ()
+ {
+ return inInternationalComposition;
+ }
+
+ function bindTheEventHandlers()
+ {
+ $(document).on("keydown", handleKeyEvent);
+ $(document).on("keypress", handleKeyEvent);
+ $(document).on("keyup", handleKeyEvent);
+ $(document).on("click", handleClick);
+
+ // Disabled: https://github.com/ether/etherpad-lite/issues/2546
+ // Will break OL re-numbering: https://github.com/ether/etherpad-lite/pull/2533
+ // $(document).on("cut", handleCut);
+
+ $(root).on("blur", handleBlur);
+ if (browser.msie)
+ {
+ $(document).on("click", handleIEOuterClick);
+ }
+ if (browser.msie) $(root).on("paste", handleIEPaste);
+
+ // Don't paste on middle click of links
+ $(root).on("paste", function(e){
+ // TODO: this breaks pasting strings into URLS when using
+ // Control C and Control V -- the Event is never available
+ // here.. :(
+ if(e.target.a || e.target.localName === "a"){
+ e.preventDefault();
+ }
+ })
+
+ // CompositionEvent is not implemented below IE version 8
+ if ( !(browser.msie && parseInt(browser.version <= 9)) && document.documentElement)
+ {
+ $(document.documentElement).on("compositionstart", handleCompositionEvent);
+ $(document.documentElement).on("compositionend", handleCompositionEvent);
+ }
+ }
+
+ function handleIEOuterClick(evt)
+ {
+ if ((evt.target.tagName || '').toLowerCase() != "html")
+ {
+ return;
+ }
+ if (!(evt.pageY > root.clientHeight))
+ {
+ return;
+ }
+
+ // click below the body
+ inCallStackIfNecessary("handleOuterClick", function()
+ {
+ // put caret at bottom of doc
+ fastIncorp(11);
+ if (isCaret())
+ { // don't interfere with drag
+ var lastLine = rep.lines.length() - 1;
+ var lastCol = rep.lines.atIndex(lastLine).text.length;
+ performSelectionChange([lastLine, lastCol], [lastLine, lastCol]);
+ }
+ });
+ }
+
+ function getClassArray(elem, optFilter)
+ {
+ var bodyClasses = [];
+ (elem.className || '').replace(/\S+/g, function(c)
+ {
+ if ((!optFilter) || (optFilter(c)))
+ {
+ bodyClasses.push(c);
+ }
+ });
+ return bodyClasses;
+ }
+
+ function setClassArray(elem, array)
+ {
+ elem.className = array.join(' ');
+ }
+
+ function setClassPresence(elem, className, present)
+ {
+ if (present) $(elem).addClass(className);
+ else $(elem).removeClass(className);
+ }
+
+ function focus()
+ {
+ window.focus();
+ }
+
+ function handleBlur(evt)
+ {
+ if (browser.msie)
+ {
+ // a fix: in IE, clicking on a control like a button outside the
+ // iframe can "blur" the editor, causing it to stop getting
+ // events, though typing still affects it(!).
+ setSelection(null);
+ }
+ }
+
+ function getSelectionPointX(point)
+ {
+ // doesn't work in wrap-mode
+ var node = point.node;
+ var index = point.index;
+
+ function leftOf(n)
+ {
+ return n.offsetLeft;
+ }
+
+ function rightOf(n)
+ {
+ return n.offsetLeft + n.offsetWidth;
+ }
+ if (!isNodeText(node))
+ {
+ if (index === 0) return leftOf(node);
+ else return rightOf(node);
+ }
+ else
+ {
+ // we can get bounds of element nodes, so look for those.
+ // allow consecutive text nodes for robustness.
+ var charsToLeft = index;
+ var charsToRight = node.nodeValue.length - index;
+ var n;
+ for (n = node.previousSibling; n && isNodeText(n); n = n.previousSibling)
+ charsToLeft += n.nodeValue;
+ var leftEdge = (n ? rightOf(n) : leftOf(node.parentNode));
+ for (n = node.nextSibling; n && isNodeText(n); n = n.nextSibling)
+ charsToRight += n.nodeValue;
+ var rightEdge = (n ? leftOf(n) : rightOf(node.parentNode));
+ var frac = (charsToLeft / (charsToLeft + charsToRight));
+ var pixLoc = leftEdge + frac * (rightEdge - leftEdge);
+ return Math.round(pixLoc);
+ }
+ }
+
+ function getPageHeight()
+ {
+ var win = window;
+ var odoc = win.document;
+ if (win.innerHeight && win.scrollMaxY) return win.innerHeight + win.scrollMaxY;
+ else if (odoc.body.scrollHeight > odoc.body.offsetHeight) return odoc.body.scrollHeight;
+ else return odoc.body.offsetHeight;
+ }
+
+ function getPageWidth()
+ {
+ var win = window;
+ var odoc = win.document;
+ if (win.innerWidth && win.scrollMaxX) return win.innerWidth + win.scrollMaxX;
+ else if (odoc.body.scrollWidth > odoc.body.offsetWidth) return odoc.body.scrollWidth;
+ else return odoc.body.offsetWidth;
+ }
+
+ function getInnerHeight()
+ {
+ var win = window;
+ var odoc = win.document;
+ var h;
+ if (browser.opera) h = win.innerHeight;
+ else h = odoc.documentElement.clientHeight;
+ if (h) return h;
+
+ // deal with case where iframe is hidden, hope that
+ // style.height of iframe container is set in px
+ return Number(editorInfo.frame.parentNode.style.height.replace(/[^0-9]/g, '') || 0);
+ }
+
+ function getInnerWidth()
+ {
+ var win = window;
+ var odoc = win.document;
+ return odoc.documentElement.clientWidth;
+ }
+
+ function scrollNodeVerticallyIntoView(node)
+ {
+ // requires element (non-text) node;
+ // if node extends above top of viewport or below bottom of viewport (or top of scrollbar),
+ // scroll it the minimum distance needed to be completely in view.
+ var win = window;
+ var odoc = window.document;
+ var distBelowTop = node.offsetTop + iframePadTop - win.scrollY;
+ var distAboveBottom = win.scrollY + getInnerHeight() - (node.offsetTop + iframePadTop + node.offsetHeight);
+
+ if (distBelowTop < 0)
+ {
+ win.scrollBy(0, distBelowTop);
+ }
+ else if (distAboveBottom < 0)
+ {
+ win.scrollBy(0, -distAboveBottom);
+ }
+ }
+
+ function scrollXHorizontallyIntoView(pixelX)
+ {
+ var win = window;
+ var odoc = window.document;
+ pixelX += iframePadLeft;
+ var distInsideLeft = pixelX - win.scrollX;
+ var distInsideRight = win.scrollX + getInnerWidth() - pixelX;
+ if (distInsideLeft < 0)
+ {
+ win.scrollBy(distInsideLeft, 0);
+ }
+ else if (distInsideRight < 0)
+ {
+ win.scrollBy(-distInsideRight + 1, 0);
+ }
+ }
+
+ function scrollSelectionIntoView()
+ {
+ if (!rep.selStart) return;
+ fixView();
+ var focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
+ scrollNodeVerticallyIntoView(rep.lines.atIndex(focusLine).lineNode);
+ if (!doesWrap)
+ {
+ var browserSelection = getSelection();
+ if (browserSelection)
+ {
+ var focusPoint = (browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint);
+ var selectionPointX = getSelectionPointX(focusPoint);
+ scrollXHorizontallyIntoView(selectionPointX);
+ fixView();
+ }
+ }
+ }
+
+ var listAttributeName = 'list';
+
+ function getLineListType(lineNum)
+ {
+ return documentAttributeManager.getAttributeOnLine(lineNum, listAttributeName)
+ }
+
+ function setLineListType(lineNum, listType)
+ {
+ if(listType == ''){
+ documentAttributeManager.removeAttributeOnLine(lineNum, listAttributeName);
+ documentAttributeManager.removeAttributeOnLine(lineNum, 'start');
+ }else{
+ documentAttributeManager.setAttributeOnLine(lineNum, listAttributeName, listType);
+ }
+
+ //if the list has been removed, it is necessary to renumber
+ //starting from the *next* line because the list may have been
+ //separated. If it returns null, it means that the list was not cut, try
+ //from the current one.
+ if(renumberList(lineNum+1)==null)
+ {
+ renumberList(lineNum);
+ }
+ }
+
+ function renumberList(lineNum){
+ //1-check we are in a list
+ var type = getLineListType(lineNum);
+ if(!type)
+ {
+ return null;
+ }
+ type = /([a-z]+)[0-9]+/.exec(type);
+ if(type[1] == "indent")
+ {
+ return null;
+ }
+
+ //2-find the first line of the list
+ while(lineNum-1 >= 0 && (type=getLineListType(lineNum-1)))
+ {
+ type = /([a-z]+)[0-9]+/.exec(type);
+ if(type[1] == "indent")
+ break;
+ lineNum--;
+ }
+
+ //3-renumber every list item of the same level from the beginning, level 1
+ //IMPORTANT: never skip a level because there imbrication may be arbitrary
+ var builder = Changeset.builder(rep.lines.totalWidth());
+ var loc = [0,0];
+ function applyNumberList(line, level)
+ {
+ //init
+ var position = 1;
+ var curLevel = level;
+ var listType;
+ //loop over the lines
+ while(listType = getLineListType(line))
+ {
+ //apply new num
+ listType = /([a-z]+)([0-9]+)/.exec(listType);
+ curLevel = Number(listType[2]);
+ if(isNaN(curLevel) || listType[0] == "indent")
+ {
+ return line;
+ }
+ else if(curLevel == level)
+ {
+ ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
+ ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
+ ['start', position]
+ ], rep.apool);
+
+ position++;
+ line++;
+ }
+ else if(curLevel < level)
+ {
+ return line;//back to parent
+ }
+ else
+ {
+ line = applyNumberList(line, level+1);//recursive call
+ }
+ }
+ return line;
+ }
+
+ applyNumberList(lineNum, 1);
+ var cs = builder.toString();
+ if (!Changeset.isIdentity(cs))
+ {
+ performDocumentApplyChangeset(cs);
+ }
+
+ //4-apply the modifications
+
+
+ }
+
+
+ function doInsertList(type)
+ {
+ if (!(rep.selStart && rep.selEnd))
+ {
+ return;
+ }
+
+ var firstLine, lastLine;
+ firstLine = rep.selStart[0];
+ lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0));
+
+ var allLinesAreList = true;
+ for (var n = firstLine; n <= lastLine; n++)
+ {
+ var listType = getLineListType(n);
+ if (!listType || listType.slice(0, type.length) != type)
+ {
+ allLinesAreList = false;
+ break;
+ }
+ }
+
+ var mods = [];
+ for (var n = firstLine; n <= lastLine; n++)
+ {
+ var t = '';
+ var level = 0;
+ var listType = /([a-z]+)([0-9]+)/.exec(getLineListType(n));
+ if (listType)
+ {
+ t = listType[1];
+ level = Number(listType[2]);
+ }
+ var t = getLineListType(n);
+ mods.push([n, allLinesAreList ? 'indent' + level : (t ? type + level : type + '1')]);
+ }
+
+ _.each(mods, function(mod){
+ setLineListType(mod[0], mod[1]);
+ });
+ }
+
+ function doInsertUnorderedList(){
+ doInsertList('bullet');
+ }
+ function doInsertOrderedList(){
+ doInsertList('number');
+ }
+ editorInfo.ace_doInsertUnorderedList = doInsertUnorderedList;
+ editorInfo.ace_doInsertOrderedList = doInsertOrderedList;
+
+ var lineNumbersShown;
+ var sideDivInner;
+
+ function initLineNumbers()
+ {
+ lineNumbersShown = 1;
+ sideDiv.innerHTML = '';
+ sideDivInner = window.document.getElementById("sidedivinner");
+ }
+
+ function updateLineNumbers()
+ {
+ var newNumLines = rep.lines.length();
+ if (newNumLines < 1) newNumLines = 1;
+ //update height of all current line numbers
+
+ var a = sideDivInner.firstChild;
+ var b = innerdocbody.firstChild;
+ var n = 0;
+
+ if (currentCallStack && currentCallStack.domClean)
+ {
+
+ while (a && b)
+ {
+ if(n > lineNumbersShown) //all updated, break
+ break;
+ var h = (b.clientHeight || b.offsetHeight);
+ if (b.nextSibling)
+ {
+ // when text is zoomed in mozilla, divs have fractional
+ // heights (though the properties are always integers)
+ // and the line-numbers don't line up unless we pay
+ // attention to where the divs are actually placed...
+ // (also: padding on TTs/SPANs in IE...)
h = b.nextSibling.offsetTop - b.offsetTop;
}
- }
-
- if(h){ // apply style to div
- div.style.height = h +"px";
- }
-
- div.appendChild(odoc.createTextNode(String(n)));
- fragment.appendChild(div);
- if(b){
+ if (h)
+ {
+ var hpx = h + "px";
+ if (a.style.height != hpx) {
+ a.style.height = hpx;
+ }
+ }
+ a = a.nextSibling;
b = b.nextSibling;
+ n++;
}
}
- container.appendChild(fragment);
- while (lineNumbersShown > newNumLines)
+ if (newNumLines != lineNumbersShown)
{
- container.removeChild(container.lastChild);
- lineNumbersShown--;
+ var container = sideDivInner;
+ var odoc = window.document;
+ var fragment = odoc.createDocumentFragment();
+
+ while (lineNumbersShown < newNumLines)
+ {
+ lineNumbersShown++;
+ var n = lineNumbersShown;
+ var div = odoc.createElement("DIV");
+ //calculate height for new line number
+ if(b){
+ var h = (b.clientHeight || b.offsetHeight);
+
+ if (b.nextSibling){
+ h = b.nextSibling.offsetTop - b.offsetTop;
+ }
+ }
+
+ if(h){ // apply style to div
+ div.style.height = h +"px";
+ }
+
+ div.appendChild(odoc.createTextNode(String(n)));
+ fragment.appendChild(div);
+ if(b){
+ b = b.nextSibling;
+ }
+ }
+
+ container.appendChild(fragment);
+ while (lineNumbersShown > newNumLines)
+ {
+ container.removeChild(container.lastChild);
+ lineNumbersShown--;
+ }
}
}
+
+
+ // Init documentAttributeManager
+ documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset);
+ editorInfo.ace_performDocumentApplyAttributesToRange = function () {
+ return documentAttributeManager.setAttributesOnRange.apply(documentAttributeManager, arguments);
+ };
+
+ this.init = function () {
+ $(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.firefox) $(root).addClass("mozilla");
+ if (browser.safari) $(root).addClass("safari");
+ if (browser.msie) $(root).addClass("msie");
+ 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();
+
+ });
+
+ hooks.callAll('aceInitialized', {
+ editorInfo: editorInfo,
+ rep: rep,
+ documentAttributeManager: documentAttributeManager
+ });
+
+ scheduler.setTimeout(function()
+ {
+ editorInfo.onEditorReady(); // defined in code that sets up the inner iframe
+ }, 0);
+
+ isSetUp = true;
+ });
+ }
+
}
-
- // Init documentAttributeManager
- documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset);
- editorInfo.ace_performDocumentApplyAttributesToRange = function () {
- return documentAttributeManager.setAttributesOnRange.apply(documentAttributeManager, arguments);
+ exports.init = function (editorInfo) {
+ var editor = new Ace2Inner(editorInfo)
+ editor.init();
};
- this.init = function () {
- $(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.firefox) $(root).addClass("mozilla");
- if (browser.safari) $(root).addClass("safari");
- if (browser.msie) $(root).addClass("msie");
- 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();
-
- });
-
- hooks.callAll('aceInitialized', {
- editorInfo: editorInfo,
- rep: rep,
- documentAttributeManager: documentAttributeManager
- });
-
- scheduler.setTimeout(function()
- {
- editorInfo.onEditorReady(); // defined in code that sets up the inner iframe
- }, 0);
-
- isSetUp = true;
- });
- }
-
-}
-
-exports.init = function (editorInfo) {
- var editor = new Ace2Inner(editorInfo)
- editor.init();
-};
-
return exports;
});
diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js
index 983402683..75560a9ad 100644
--- a/src/static/js/broadcast.js
+++ b/src/static/js/broadcast.js
@@ -20,577 +20,580 @@
* limitations under the License.
*/
-var makeCSSManager = require('./cssmanager').makeCSSManager;
-var domline = require('./domline').domline;
-var AttribPool = require('./AttributePool');
-var Changeset = require('./Changeset');
-var linestylefilter = require('./linestylefilter').linestylefilter;
-var colorutils = require('./colorutils').colorutils;
-var _ = require('./underscore');
-var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
+define(['ep_etherpad-lite/static/js/pluginfw/hooks', 'ep_etherpad-lite/static/js/linestylefilter', 'ep_etherpad-lite/static/js/domline', 'underscore'], function (hooks, linetyleFilterMod, domlineMod, _) {
+ var exports = {};
+ var makeCSSManager = require('./cssmanager').makeCSSManager;
+ var domline = domlineMod.domline;
+ var AttribPool = require('./AttributePool');
+ var Changeset = require('./Changeset');
+ var linestylefilter = linetyleFilterMod.linestylefilter;
+ var colorutils = require('./colorutils').colorutils;
-// These parameters were global, now they are injected. A reference to the
-// Timeslider controller would probably be more appropriate.
-function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider)
-{
- var changesetLoader = undefined;
-
- // Below Array#indexOf code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_indexof.htm
- if (!Array.prototype.indexOf)
+ // These parameters were global, now they are injected. A reference to the
+ // Timeslider controller would probably be more appropriate.
+ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider)
{
- Array.prototype.indexOf = function(elt /*, from*/ )
+ var changesetLoader = undefined;
+
+ // Below Array#indexOf code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_indexof.htm
+ if (!Array.prototype.indexOf)
{
- var len = this.length >>> 0;
-
- var from = Number(arguments[1]) || 0;
- from = (from < 0) ? Math.ceil(from) : Math.floor(from);
- if (from < 0) from += len;
-
- for (; from < len; from++)
+ Array.prototype.indexOf = function(elt /*, from*/ )
{
- if (from in this && this[from] === elt) return from;
- }
- return -1;
- };
- }
+ var len = this.length >>> 0;
- function debugLog()
- {
- try
- {
- if (window.console) console.log.apply(console, arguments);
- }
- catch (e)
- {
- if (window.console) console.log("error printing: ", e);
- }
- }
+ var from = Number(arguments[1]) || 0;
+ from = (from < 0) ? Math.ceil(from) : Math.floor(from);
+ if (from < 0) from += len;
- //var socket;
- var channelState = "DISCONNECTED";
-
- var appLevelDisconnectReason = null;
-
- var padContents = {
- currentRevision: clientVars.collab_client_vars.rev,
- currentTime: clientVars.collab_client_vars.time,
- currentLines: Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
- currentDivs: null,
- // to be filled in once the dom loads
- apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
- alines: Changeset.splitAttributionLines(
- clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.text),
-
- // generates a jquery element containing HTML for a line
- lineToElement: function(line, aline)
- {
- var element = document.createElement("div");
- var emptyLine = (line == '\n');
- var domInfo = domline.createDomLine(!emptyLine, true);
- linestylefilter.populateDomLine(line, aline, this.apool, domInfo);
- domInfo.prepareForAdd();
- element.className = domInfo.node.className;
- element.innerHTML = domInfo.node.innerHTML;
- element.id = Math.random();
- return $(element);
- },
-
- applySpliceToDivs: function(start, numRemoved, newLines)
- {
- // remove spliced-out lines from DOM
- for (var i = start; i < start + numRemoved && i < this.currentDivs.length; i++)
- {
- debugLog("removing", this.currentDivs[i].attr('id'));
- this.currentDivs[i].remove();
- }
-
- // remove spliced-out line divs from currentDivs array
- this.currentDivs.splice(start, numRemoved);
-
- var newDivs = [];
- for (var i = 0; i < newLines.length; i++)
- {
- newDivs.push(this.lineToElement(newLines[i], this.alines[start + i]));
- }
-
- // grab the div just before the first one
- var startDiv = this.currentDivs[start - 1] || null;
-
- // insert the div elements into the correct place, in the correct order
- for (var i = 0; i < newDivs.length; i++)
- {
- if (startDiv)
+ for (; from < len; from++)
{
- startDiv.after(newDivs[i]);
+ if (from in this && this[from] === elt) return from;
}
- else
- {
- $("#padcontent").prepend(newDivs[i]);
- }
- startDiv = newDivs[i];
- }
-
- // insert new divs into currentDivs array
- newDivs.unshift(0); // remove 0 elements
- newDivs.unshift(start);
- this.currentDivs.splice.apply(this.currentDivs, newDivs);
- return this;
- },
-
- // splice the lines
- splice: function(start, numRemoved, newLinesVA)
- {
- var newLines = _.map(Array.prototype.slice.call(arguments, 2), function(s) {
- return s;
- });
-
- // apply this splice to the divs
- this.applySpliceToDivs(start, numRemoved, newLines);
-
- // call currentLines.splice, to keep the currentLines array up to date
- newLines.unshift(numRemoved);
- newLines.unshift(start);
- this.currentLines.splice.apply(this.currentLines, arguments);
- },
- // returns the contents of the specified line I
- get: function(i)
- {
- return this.currentLines[i];
- },
- // returns the number of lines in the document
- length: function()
- {
- return this.currentLines.length;
- },
-
- getActiveAuthors: function()
- {
- var self = this;
- var authors = [];
- var seenNums = {};
- var alines = self.alines;
- for (var i = 0; i < alines.length; i++)
- {
- Changeset.eachAttribNumber(alines[i], function(n)
- {
- if (!seenNums[n])
- {
- seenNums[n] = true;
- if (self.apool.getAttribKey(n) == 'author')
- {
- var a = self.apool.getAttribValue(n);
- if (a)
- {
- authors.push(a);
- }
- }
- }
- });
- }
- authors.sort();
- return authors;
+ return -1;
+ };
}
- };
- function callCatchingErrors(catcher, func)
- {
- try
- {
- wrapRecordingErrors(catcher, func)();
- }
- catch (e)
- { /*absorb*/
- }
- }
-
- function wrapRecordingErrors(catcher, func)
- {
- return function()
+ function debugLog()
{
try
{
- return func.apply(this, Array.prototype.slice.call(arguments));
+ if (window.console) console.log.apply(console, arguments);
}
catch (e)
{
- // caughtErrors.push(e);
- // caughtErrorCatchers.push(catcher);
- // caughtErrorTimes.push(+new Date());
- // console.dir({catcher: catcher, e: e});
- debugLog(e); // TODO(kroo): added temporary, to catch errors
- throw e;
+ if (window.console) console.log("error printing: ", e);
+ }
+ }
+
+ //var socket;
+ var channelState = "DISCONNECTED";
+
+ var appLevelDisconnectReason = null;
+
+ var padContents = {
+ currentRevision: clientVars.collab_client_vars.rev,
+ currentTime: clientVars.collab_client_vars.time,
+ currentLines: Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
+ currentDivs: null,
+ // to be filled in once the dom loads
+ apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
+ alines: Changeset.splitAttributionLines(
+ clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.text),
+
+ // generates a jquery element containing HTML for a line
+ lineToElement: function(line, aline)
+ {
+ var element = document.createElement("div");
+ var emptyLine = (line == '\n');
+ var domInfo = domline.createDomLine(!emptyLine, true);
+ linestylefilter.populateDomLine(line, aline, this.apool, domInfo);
+ domInfo.prepareForAdd();
+ element.className = domInfo.node.className;
+ element.innerHTML = domInfo.node.innerHTML;
+ element.id = Math.random();
+ return $(element);
+ },
+
+ applySpliceToDivs: function(start, numRemoved, newLines)
+ {
+ // remove spliced-out lines from DOM
+ for (var i = start; i < start + numRemoved && i < this.currentDivs.length; i++)
+ {
+ debugLog("removing", this.currentDivs[i].attr('id'));
+ this.currentDivs[i].remove();
+ }
+
+ // remove spliced-out line divs from currentDivs array
+ this.currentDivs.splice(start, numRemoved);
+
+ var newDivs = [];
+ for (var i = 0; i < newLines.length; i++)
+ {
+ newDivs.push(this.lineToElement(newLines[i], this.alines[start + i]));
+ }
+
+ // grab the div just before the first one
+ var startDiv = this.currentDivs[start - 1] || null;
+
+ // insert the div elements into the correct place, in the correct order
+ for (var i = 0; i < newDivs.length; i++)
+ {
+ if (startDiv)
+ {
+ startDiv.after(newDivs[i]);
+ }
+ else
+ {
+ $("#padcontent").prepend(newDivs[i]);
+ }
+ startDiv = newDivs[i];
+ }
+
+ // insert new divs into currentDivs array
+ newDivs.unshift(0); // remove 0 elements
+ newDivs.unshift(start);
+ this.currentDivs.splice.apply(this.currentDivs, newDivs);
+ return this;
+ },
+
+ // splice the lines
+ splice: function(start, numRemoved, newLinesVA)
+ {
+ var newLines = _.map(Array.prototype.slice.call(arguments, 2), function(s) {
+ return s;
+ });
+
+ // apply this splice to the divs
+ this.applySpliceToDivs(start, numRemoved, newLines);
+
+ // call currentLines.splice, to keep the currentLines array up to date
+ newLines.unshift(numRemoved);
+ newLines.unshift(start);
+ this.currentLines.splice.apply(this.currentLines, arguments);
+ },
+ // returns the contents of the specified line I
+ get: function(i)
+ {
+ return this.currentLines[i];
+ },
+ // returns the number of lines in the document
+ length: function()
+ {
+ return this.currentLines.length;
+ },
+
+ getActiveAuthors: function()
+ {
+ var self = this;
+ var authors = [];
+ var seenNums = {};
+ var alines = self.alines;
+ for (var i = 0; i < alines.length; i++)
+ {
+ Changeset.eachAttribNumber(alines[i], function(n)
+ {
+ if (!seenNums[n])
+ {
+ seenNums[n] = true;
+ if (self.apool.getAttribKey(n) == 'author')
+ {
+ var a = self.apool.getAttribValue(n);
+ if (a)
+ {
+ authors.push(a);
+ }
+ }
+ }
+ });
+ }
+ authors.sort();
+ return authors;
}
};
- }
- function loadedNewChangeset(changesetForward, changesetBackward, revision, timeDelta)
- {
- var broadcasting = (BroadcastSlider.getSliderPosition() == revisionInfo.latest);
- debugLog("broadcasting:", broadcasting, BroadcastSlider.getSliderPosition(), revisionInfo.latest, revision);
- revisionInfo.addChangeset(revision, revision + 1, changesetForward, changesetBackward, timeDelta);
- BroadcastSlider.setSliderLength(revisionInfo.latest);
- if (broadcasting) applyChangeset(changesetForward, revision + 1, false, timeDelta);
- }
-
- /*
- At this point, we must be certain that the changeset really does map from
- the current revision to the specified revision. Any mistakes here will
- cause the whole slider to get out of sync.
- */
-
- function applyChangeset(changeset, revision, preventSliderMovement, timeDelta)
- {
- // disable the next 'gotorevision' call handled by a timeslider update
- if (!preventSliderMovement)
+ function callCatchingErrors(catcher, func)
{
- goToRevisionIfEnabledCount++;
- BroadcastSlider.setSliderPosition(revision);
+ try
+ {
+ wrapRecordingErrors(catcher, func)();
+ }
+ catch (e)
+ { /*absorb*/
+ }
}
- try
+ function wrapRecordingErrors(catcher, func)
{
- // must mutate attribution lines before text lines
- Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
- }
- catch (e)
- {
- debugLog(e);
+ return function()
+ {
+ try
+ {
+ return func.apply(this, Array.prototype.slice.call(arguments));
+ }
+ catch (e)
+ {
+ // caughtErrors.push(e);
+ // caughtErrorCatchers.push(catcher);
+ // caughtErrorTimes.push(+new Date());
+ // console.dir({catcher: catcher, e: e});
+ debugLog(e); // TODO(kroo): added temporary, to catch errors
+ throw e;
+ }
+ };
}
- Changeset.mutateTextLines(changeset, padContents);
- padContents.currentRevision = revision;
- padContents.currentTime += timeDelta * 1000;
+ function loadedNewChangeset(changesetForward, changesetBackward, revision, timeDelta)
+ {
+ var broadcasting = (BroadcastSlider.getSliderPosition() == revisionInfo.latest);
+ debugLog("broadcasting:", broadcasting, BroadcastSlider.getSliderPosition(), revisionInfo.latest, revision);
+ revisionInfo.addChangeset(revision, revision + 1, changesetForward, changesetBackward, timeDelta);
+ BroadcastSlider.setSliderLength(revisionInfo.latest);
+ if (broadcasting) applyChangeset(changesetForward, revision + 1, false, timeDelta);
+ }
+
+ /*
+ At this point, we must be certain that the changeset really does map from
+ the current revision to the specified revision. Any mistakes here will
+ cause the whole slider to get out of sync.
+ */
+
+ function applyChangeset(changeset, revision, preventSliderMovement, timeDelta)
+ {
+ // disable the next 'gotorevision' call handled by a timeslider update
+ if (!preventSliderMovement)
+ {
+ goToRevisionIfEnabledCount++;
+ BroadcastSlider.setSliderPosition(revision);
+ }
+
+ try
+ {
+ // must mutate attribution lines before text lines
+ Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
+ }
+ catch (e)
+ {
+ debugLog(e);
+ }
+
+ Changeset.mutateTextLines(changeset, padContents);
+ padContents.currentRevision = revision;
+ padContents.currentTime += timeDelta * 1000;
+
+ debugLog('Time Delta: ', timeDelta)
+ updateTimer();
+
+ var authors = _.map(padContents.getActiveAuthors(), function(name)
+ {
+ return authorData[name];
+ });
+
+ BroadcastSlider.setAuthors(authors);
+ }
+
+ function updateTimer()
+ {
+ var zpad = function(str, length)
+ {
+ str = str + "";
+ while (str.length < length)
+ str = '0' + str;
+ return str;
+ }
+
+ var date = new Date(padContents.currentTime);
+ var dateFormat = function()
+ {
+ var month = zpad(date.getMonth() + 1, 2);
+ var day = zpad(date.getDate(), 2);
+ var year = (date.getFullYear());
+ var hours = zpad(date.getHours(), 2);
+ var minutes = zpad(date.getMinutes(), 2);
+ var seconds = zpad(date.getSeconds(), 2);
+ return (html10n.get("timeslider.dateformat", {
+ "day": day,
+ "month": month,
+ "year": year,
+ "hours": hours,
+ "minutes": minutes,
+ "seconds": seconds
+ }));
+ }
+
+
+
+
+
+ $('#timer').html(dateFormat());
+ var revisionDate = html10n.get("timeslider.saved", {
+ "day": date.getDate(),
+ "month": [
+ html10n.get("timeslider.month.january"),
+ html10n.get("timeslider.month.february"),
+ html10n.get("timeslider.month.march"),
+ html10n.get("timeslider.month.april"),
+ html10n.get("timeslider.month.may"),
+ html10n.get("timeslider.month.june"),
+ html10n.get("timeslider.month.july"),
+ html10n.get("timeslider.month.august"),
+ html10n.get("timeslider.month.september"),
+ html10n.get("timeslider.month.october"),
+ html10n.get("timeslider.month.november"),
+ html10n.get("timeslider.month.december")
+ ][date.getMonth()],
+ "year": date.getFullYear()
+ });
+ $('#revision_date').html(revisionDate)
+
+ }
- debugLog('Time Delta: ', timeDelta)
updateTimer();
-
- var authors = _.map(padContents.getActiveAuthors(), function(name)
- {
- return authorData[name];
- });
-
- BroadcastSlider.setAuthors(authors);
- }
- function updateTimer()
- {
- var zpad = function(str, length)
- {
- str = str + "";
- while (str.length < length)
- str = '0' + str;
- return str;
- }
-
- var date = new Date(padContents.currentTime);
- var dateFormat = function()
- {
- var month = zpad(date.getMonth() + 1, 2);
- var day = zpad(date.getDate(), 2);
- var year = (date.getFullYear());
- var hours = zpad(date.getHours(), 2);
- var minutes = zpad(date.getMinutes(), 2);
- var seconds = zpad(date.getSeconds(), 2);
- return (html10n.get("timeslider.dateformat", {
- "day": day,
- "month": month,
- "year": year,
- "hours": hours,
- "minutes": minutes,
- "seconds": seconds
- }));
- }
-
-
-
-
-
- $('#timer').html(dateFormat());
- var revisionDate = html10n.get("timeslider.saved", {
- "day": date.getDate(),
- "month": [
- html10n.get("timeslider.month.january"),
- html10n.get("timeslider.month.february"),
- html10n.get("timeslider.month.march"),
- html10n.get("timeslider.month.april"),
- html10n.get("timeslider.month.may"),
- html10n.get("timeslider.month.june"),
- html10n.get("timeslider.month.july"),
- html10n.get("timeslider.month.august"),
- html10n.get("timeslider.month.september"),
- html10n.get("timeslider.month.october"),
- html10n.get("timeslider.month.november"),
- html10n.get("timeslider.month.december")
- ][date.getMonth()],
- "year": date.getFullYear()
- });
- $('#revision_date').html(revisionDate)
-
- }
-
- updateTimer();
-
- function goToRevision(newRevision)
- {
- padContents.targetRevision = newRevision;
- var self = this;
- var path = revisionInfo.getPath(padContents.currentRevision, newRevision);
- debugLog('newRev: ', padContents.currentRevision, path);
- if (path.status == 'complete')
+ function goToRevision(newRevision)
{
- var cs = path.changesets;
- debugLog("status: complete, changesets: ", cs, "path:", path);
- var changeset = cs[0];
- var timeDelta = path.times[0];
- for (var i = 1; i < cs.length; i++)
+ padContents.targetRevision = newRevision;
+ var self = this;
+ var path = revisionInfo.getPath(padContents.currentRevision, newRevision);
+ debugLog('newRev: ', padContents.currentRevision, path);
+ if (path.status == 'complete')
{
- changeset = Changeset.compose(changeset, cs[i], padContents.apool);
- timeDelta += path.times[i];
- }
- if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
- }
- else if (path.status == "partial")
- {
- debugLog('partial');
- var sliderLocation = padContents.currentRevision;
- // callback is called after changeset information is pulled from server
- // this may never get called, if the changeset has already been loaded
- var update = function(start, end)
+ var cs = path.changesets;
+ debugLog("status: complete, changesets: ", cs, "path:", path);
+ var changeset = cs[0];
+ var timeDelta = path.times[0];
+ for (var i = 1; i < cs.length; i++)
{
- // if we've called goToRevision in the time since, don't goToRevision
- goToRevision(padContents.targetRevision);
- };
-
- // do our best with what we have...
- var cs = path.changesets;
-
- var changeset = cs[0];
- var timeDelta = path.times[0];
- for (var i = 1; i < cs.length; i++)
- {
- changeset = Changeset.compose(changeset, cs[i], padContents.apool);
- timeDelta += path.times[i];
+ changeset = Changeset.compose(changeset, cs[i], padContents.apool);
+ timeDelta += path.times[i];
+ }
+ if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
}
- if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
-
- // Loading changeset history for new revision
- loadChangesetsForRevision(newRevision, update);
- // Loading changeset history for old revision (to make diff between old and new revision)
- loadChangesetsForRevision(padContents.currentRevision - 1);
- }
-
- var authors = _.map(padContents.getActiveAuthors(), function(name){
- return authorData[name];
- });
- BroadcastSlider.setAuthors(authors);
- }
-
- function loadChangesetsForRevision(revision, callback) {
- if (BroadcastSlider.getSliderLength() > 10000)
- {
- var start = (Math.floor((revision) / 10000) * 10000); // revision 0 to 10
- changesetLoader.queueUp(start, 100);
- }
-
- if (BroadcastSlider.getSliderLength() > 1000)
- {
- var start = (Math.floor((revision) / 1000) * 1000); // (start from -1, go to 19) + 1
- changesetLoader.queueUp(start, 10);
- }
-
- start = (Math.floor((revision) / 100) * 100);
-
- changesetLoader.queueUp(start, 1, callback);
- }
-
- changesetLoader = {
- running: false,
- resolved: [],
- requestQueue1: [],
- requestQueue2: [],
- requestQueue3: [],
- reqCallbacks: [],
- queueUp: function(revision, width, callback)
- {
- if (revision < 0) revision = 0;
- // if(changesetLoader.requestQueue.indexOf(revision) != -1)
- // return; // already in the queue.
- if (changesetLoader.resolved.indexOf(revision + "_" + width) != -1) return; // already loaded from the server
- changesetLoader.resolved.push(revision + "_" + width);
-
- var requestQueue = width == 1 ? changesetLoader.requestQueue3 : width == 10 ? changesetLoader.requestQueue2 : changesetLoader.requestQueue1;
- requestQueue.push(
+ else if (path.status == "partial")
{
- 'rev': revision,
- 'res': width,
- 'callback': callback
+ debugLog('partial');
+ var sliderLocation = padContents.currentRevision;
+ // callback is called after changeset information is pulled from server
+ // this may never get called, if the changeset has already been loaded
+ var update = function(start, end)
+ {
+ // if we've called goToRevision in the time since, don't goToRevision
+ goToRevision(padContents.targetRevision);
+ };
+
+ // do our best with what we have...
+ var cs = path.changesets;
+
+ var changeset = cs[0];
+ var timeDelta = path.times[0];
+ for (var i = 1; i < cs.length; i++)
+ {
+ changeset = Changeset.compose(changeset, cs[i], padContents.apool);
+ timeDelta += path.times[i];
+ }
+ if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
+
+ // Loading changeset history for new revision
+ loadChangesetsForRevision(newRevision, update);
+ // Loading changeset history for old revision (to make diff between old and new revision)
+ loadChangesetsForRevision(padContents.currentRevision - 1);
+ }
+
+ var authors = _.map(padContents.getActiveAuthors(), function(name){
+ return authorData[name];
});
- if (!changesetLoader.running)
- {
- changesetLoader.running = true;
- setTimeout(changesetLoader.loadFromQueue, 10);
- }
- },
- loadFromQueue: function()
- {
- var self = changesetLoader;
- var requestQueue = self.requestQueue1.length > 0 ? self.requestQueue1 : self.requestQueue2.length > 0 ? self.requestQueue2 : self.requestQueue3.length > 0 ? self.requestQueue3 : null;
+ BroadcastSlider.setAuthors(authors);
+ }
- if (!requestQueue)
+ function loadChangesetsForRevision(revision, callback) {
+ if (BroadcastSlider.getSliderLength() > 10000)
{
- self.running = false;
- return;
+ var start = (Math.floor((revision) / 10000) * 10000); // revision 0 to 10
+ changesetLoader.queueUp(start, 100);
}
- var request = requestQueue.pop();
- var granularity = request.res;
- var callback = request.callback;
- var start = request.rev;
- var requestID = Math.floor(Math.random() * 100000);
-
- sendSocketMsg("CHANGESET_REQ", {
- "start": start,
- "granularity": granularity,
- "requestID": requestID
- });
-
- self.reqCallbacks[requestID] = callback;
- },
- handleSocketResponse: function(message)
- {
- var self = changesetLoader;
-
- var start = message.data.start;
- var granularity = message.data.granularity;
- var callback = self.reqCallbacks[message.data.requestID];
- delete self.reqCallbacks[message.data.requestID];
-
- self.handleResponse(message.data, start, granularity, callback);
- setTimeout(self.loadFromQueue, 10);
- },
- handleResponse: function(data, start, granularity, callback)
- {
- debugLog("response: ", data);
- var pool = (new AttribPool()).fromJsonable(data.apool);
- for (var i = 0; i < data.forwardsChangesets.length; i++)
+ if (BroadcastSlider.getSliderLength() > 1000)
{
- var astart = start + i * granularity - 1; // rev -1 is a blank single line
- var aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision
- if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
- //debugLog("adding changeset:", astart, aend);
- var forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
- var backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
- revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
+ var start = (Math.floor((revision) / 1000) * 1000); // (start from -1, go to 19) + 1
+ changesetLoader.queueUp(start, 10);
}
- if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
- },
- handleMessageFromServer: function (obj)
- {
- debugLog("handleMessage:", arguments);
- if (obj.type == "COLLABROOM")
+ start = (Math.floor((revision) / 100) * 100);
+
+ changesetLoader.queueUp(start, 1, callback);
+ }
+
+ changesetLoader = {
+ running: false,
+ resolved: [],
+ requestQueue1: [],
+ requestQueue2: [],
+ requestQueue3: [],
+ reqCallbacks: [],
+ queueUp: function(revision, width, callback)
{
- obj = obj.data;
+ if (revision < 0) revision = 0;
+ // if(changesetLoader.requestQueue.indexOf(revision) != -1)
+ // return; // already in the queue.
+ if (changesetLoader.resolved.indexOf(revision + "_" + width) != -1) return; // already loaded from the server
+ changesetLoader.resolved.push(revision + "_" + width);
- if (obj.type == "NEW_CHANGES")
+ var requestQueue = width == 1 ? changesetLoader.requestQueue3 : width == 10 ? changesetLoader.requestQueue2 : changesetLoader.requestQueue1;
+ requestQueue.push(
{
- debugLog(obj);
- var changeset = Changeset.moveOpsToNewPool(
- obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
-
- var changesetBack = Changeset.inverse(
- obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
-
- var changesetBack = Changeset.moveOpsToNewPool(
- changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
-
- loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
- }
- else if (obj.type == "NEW_AUTHORDATA")
+ 'rev': revision,
+ 'res': width,
+ 'callback': callback
+ });
+ if (!changesetLoader.running)
{
- var authorMap = {};
- authorMap[obj.author] = obj.data;
- receiveAuthorData(authorMap);
-
- var authors = _.map(padContents.getActiveAuthors(), function(name) {
- return authorData[name];
- });
-
- BroadcastSlider.setAuthors(authors);
+ changesetLoader.running = true;
+ setTimeout(changesetLoader.loadFromQueue, 10);
}
- else if (obj.type == "NEW_SAVEDREV")
- {
- var savedRev = obj.savedRev;
- BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
- }
- hooks.callAll('handleClientTimesliderMessage_' + obj.type, {payload: obj});
- }
- else if(obj.type == "CHANGESET_REQ")
+ },
+ loadFromQueue: function()
{
- changesetLoader.handleSocketResponse(obj);
+ var self = changesetLoader;
+ var requestQueue = self.requestQueue1.length > 0 ? self.requestQueue1 : self.requestQueue2.length > 0 ? self.requestQueue2 : self.requestQueue3.length > 0 ? self.requestQueue3 : null;
+
+ if (!requestQueue)
+ {
+ self.running = false;
+ return;
+ }
+
+ var request = requestQueue.pop();
+ var granularity = request.res;
+ var callback = request.callback;
+ var start = request.rev;
+ var requestID = Math.floor(Math.random() * 100000);
+
+ sendSocketMsg("CHANGESET_REQ", {
+ "start": start,
+ "granularity": granularity,
+ "requestID": requestID
+ });
+
+ self.reqCallbacks[requestID] = callback;
+ },
+ handleSocketResponse: function(message)
+ {
+ var self = changesetLoader;
+
+ var start = message.data.start;
+ var granularity = message.data.granularity;
+ var callback = self.reqCallbacks[message.data.requestID];
+ delete self.reqCallbacks[message.data.requestID];
+
+ self.handleResponse(message.data, start, granularity, callback);
+ setTimeout(self.loadFromQueue, 10);
+ },
+ handleResponse: function(data, start, granularity, callback)
+ {
+ debugLog("response: ", data);
+ var pool = (new AttribPool()).fromJsonable(data.apool);
+ for (var i = 0; i < data.forwardsChangesets.length; i++)
+ {
+ var astart = start + i * granularity - 1; // rev -1 is a blank single line
+ var aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision
+ if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
+ //debugLog("adding changeset:", astart, aend);
+ var forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
+ var backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
+ revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
+ }
+ if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
+ },
+ handleMessageFromServer: function (obj)
+ {
+ debugLog("handleMessage:", arguments);
+
+ if (obj.type == "COLLABROOM")
+ {
+ obj = obj.data;
+
+ if (obj.type == "NEW_CHANGES")
+ {
+ debugLog(obj);
+ var changeset = Changeset.moveOpsToNewPool(
+ obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
+
+ var changesetBack = Changeset.inverse(
+ obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
+
+ var changesetBack = Changeset.moveOpsToNewPool(
+ changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
+
+ loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
+ }
+ else if (obj.type == "NEW_AUTHORDATA")
+ {
+ var authorMap = {};
+ authorMap[obj.author] = obj.data;
+ receiveAuthorData(authorMap);
+
+ var authors = _.map(padContents.getActiveAuthors(), function(name) {
+ return authorData[name];
+ });
+
+ BroadcastSlider.setAuthors(authors);
+ }
+ else if (obj.type == "NEW_SAVEDREV")
+ {
+ var savedRev = obj.savedRev;
+ BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
+ }
+ hooks.callAll('handleClientTimesliderMessage_' + obj.type, {payload: obj});
+ }
+ else if(obj.type == "CHANGESET_REQ")
+ {
+ changesetLoader.handleSocketResponse(obj);
+ }
+ else
+ {
+ debugLog("Unknown message type: " + obj.type);
+ }
+ }
+ };
+
+ // to start upon window load, just push a function onto this array
+ //window['onloadFuncts'].push(setUpSocket);
+ //window['onloadFuncts'].push(function ()
+ fireWhenAllScriptsAreLoaded.push(function()
+ {
+ // set up the currentDivs and DOM
+ padContents.currentDivs = [];
+ $("#padcontent").html("");
+ for (var i = 0; i < padContents.currentLines.length; i++)
+ {
+ var div = padContents.lineToElement(padContents.currentLines[i], padContents.alines[i]);
+ padContents.currentDivs.push(div);
+ $("#padcontent").append(div);
+ }
+ debugLog(padContents.currentDivs);
+ });
+
+ // this is necessary to keep infinite loops of events firing,
+ // since goToRevision changes the slider position
+ var goToRevisionIfEnabledCount = 0;
+ var goToRevisionIfEnabled = function() {
+ if (goToRevisionIfEnabledCount > 0)
+ {
+ goToRevisionIfEnabledCount--;
}
else
{
- debugLog("Unknown message type: " + obj.type);
+ goToRevision.apply(goToRevision, arguments);
}
}
- };
- // to start upon window load, just push a function onto this array
- //window['onloadFuncts'].push(setUpSocket);
- //window['onloadFuncts'].push(function ()
- fireWhenAllScriptsAreLoaded.push(function()
- {
- // set up the currentDivs and DOM
- padContents.currentDivs = [];
- $("#padcontent").html("");
- for (var i = 0; i < padContents.currentLines.length; i++)
- {
- var div = padContents.lineToElement(padContents.currentLines[i], padContents.alines[i]);
- padContents.currentDivs.push(div);
- $("#padcontent").append(div);
- }
- debugLog(padContents.currentDivs);
- });
+ BroadcastSlider.onSlider(goToRevisionIfEnabled);
- // this is necessary to keep infinite loops of events firing,
- // since goToRevision changes the slider position
- var goToRevisionIfEnabledCount = 0;
- var goToRevisionIfEnabled = function() {
- if (goToRevisionIfEnabledCount > 0)
- {
- goToRevisionIfEnabledCount--;
- }
- else
- {
- goToRevision.apply(goToRevision, arguments);
- }
- }
-
- BroadcastSlider.onSlider(goToRevisionIfEnabled);
+ var dynamicCSS = makeCSSManager('dynamicsyntax');
+ var authorData = {};
- var dynamicCSS = makeCSSManager('dynamicsyntax');
- var authorData = {};
-
- function receiveAuthorData(newAuthorData)
- {
- for (var author in newAuthorData)
+ function receiveAuthorData(newAuthorData)
{
- var data = newAuthorData[author];
- var bgcolor = typeof data.colorId == "number" ? clientVars.colorPalette[data.colorId] : data.colorId;
- if (bgcolor && dynamicCSS)
+ for (var author in newAuthorData)
{
- var selector = dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author));
- selector.backgroundColor = bgcolor
- selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5) ? '#ffffff' : '#000000'; //see ace2_inner.js for the other part
+ var data = newAuthorData[author];
+ var bgcolor = typeof data.colorId == "number" ? clientVars.colorPalette[data.colorId] : data.colorId;
+ if (bgcolor && dynamicCSS)
+ {
+ var selector = dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author));
+ selector.backgroundColor = bgcolor
+ selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5) ? '#ffffff' : '#000000'; //see ace2_inner.js for the other part
+ }
+ authorData[author] = data;
}
- authorData[author] = data;
}
+
+ receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData);
+
+ return changesetLoader;
}
- receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData);
+ exports.loadBroadcastJS = loadBroadcastJS;
- return changesetLoader;
-}
-
-exports.loadBroadcastJS = loadBroadcastJS;
+ return exports;
+});
diff --git a/src/static/js/domline.js b/src/static/js/domline.js
index c40c6e204..f97a5ca80 100644
--- a/src/static/js/domline.js
+++ b/src/static/js/domline.js
@@ -26,296 +26,300 @@
// requires: plugins
// requires: undefined
-var Security = require('./security');
-var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
-var _ = require('./underscore');
-var lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
-var noop = function(){};
+define(['ep_etherpad-lite/static/js/pluginfw/hooks', 'ep_etherpad-lite/static/js/linestylefilter', 'underscore'], function(hooks, linestylefilterMod, _) {
+ var exports = {};
+
+ var Security = require('./security');
+ var lineAttributeMarker = linestylefilterMod.lineAttributeMarker;
+ var noop = function(){};
-var domline = {};
+ var domline = {};
-domline.addToLineClass = function(lineClass, cls)
-{
- // an "empty span" at any point can be used to add classes to
- // the line, using line:className. otherwise, we ignore
- // the span.
- cls.replace(/\S+/g, function(c)
+ domline.addToLineClass = function(lineClass, cls)
{
- if (c.indexOf("line:") == 0)
+ // an "empty span" at any point can be used to add classes to
+ // the line, using line:className. otherwise, we ignore
+ // the span.
+ cls.replace(/\S+/g, function(c)
{
- // add class to line
- lineClass = (lineClass ? lineClass + ' ' : '') + c.substring(5);
- }
- });
- return lineClass;
-}
-
-// if "document" is falsy we don't create a DOM node, just
-// an object with innerHTML and className
-domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument)
-{
- var result = {
- node: null,
- appendSpan: noop,
- prepareForAdd: noop,
- notifyAdded: noop,
- clearSpans: noop,
- finishUpdate: noop,
- lineMarker: 0
- };
-
- var document = optDocument;
-
- if (document)
- {
- result.node = document.createElement("div");
+ if (c.indexOf("line:") == 0)
+ {
+ // add class to line
+ lineClass = (lineClass ? lineClass + ' ' : '') + c.substring(5);
+ }
+ });
+ return lineClass;
}
- else
+
+ // if "document" is falsy we don't create a DOM node, just
+ // an object with innerHTML and className
+ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument)
{
- result.node = {
- innerHTML: '',
- className: ''
+ var result = {
+ node: null,
+ appendSpan: noop,
+ prepareForAdd: noop,
+ notifyAdded: noop,
+ clearSpans: noop,
+ finishUpdate: noop,
+ lineMarker: 0
};
- }
- var html = [];
- var preHtml = '',
- postHtml = '';
- var curHTML = null;
+ var document = optDocument;
- function processSpaces(s)
- {
- return domline.processSpaces(s, doesWrap);
- }
-
- var perTextNodeProcess = (doesWrap ? _.identity : processSpaces);
- var perHtmlLineProcess = (doesWrap ? processSpaces : _.identity);
- var lineClass = 'ace-line';
-
- result.appendSpan = function(txt, cls)
- {
-
- var processedMarker = false;
- // Handle lineAttributeMarker, if present
- if (cls.indexOf(lineAttributeMarker) >= 0)
+ if (document)
{
- var listType = /(?:^| )list:(\S+)/.exec(cls);
- var start = /(?:^| )start:(\S+)/.exec(cls);
+ result.node = document.createElement("div");
+ }
+ else
+ {
+ result.node = {
+ innerHTML: '',
+ className: ''
+ };
+ }
- _.map(hooks.callAll("aceDomLinePreProcessLineAttributes", {
- domline: domline,
- cls: cls
- }), function(modifier)
- {
- preHtml += modifier.preHtml;
- postHtml += modifier.postHtml;
- processedMarker |= modifier.processedMarker;
- });
+ var html = [];
+ var preHtml = '',
+ postHtml = '';
+ var curHTML = null;
- if (listType)
+ function processSpaces(s)
+ {
+ return domline.processSpaces(s, doesWrap);
+ }
+
+ var perTextNodeProcess = (doesWrap ? _.identity : processSpaces);
+ var perHtmlLineProcess = (doesWrap ? processSpaces : _.identity);
+ var lineClass = 'ace-line';
+
+ result.appendSpan = function(txt, cls)
+ {
+
+ var processedMarker = false;
+ // Handle lineAttributeMarker, if present
+ if (cls.indexOf(lineAttributeMarker) >= 0)
{
- listType = listType[1];
+ var listType = /(?:^| )list:(\S+)/.exec(cls);
+ var start = /(?:^| )start:(\S+)/.exec(cls);
+
+ _.map(hooks.callAll("aceDomLinePreProcessLineAttributes", {
+ domline: domline,
+ cls: cls
+ }), function(modifier)
+ {
+ preHtml += modifier.preHtml;
+ postHtml += modifier.postHtml;
+ processedMarker |= modifier.processedMarker;
+ });
+
if (listType)
{
- if(listType.indexOf("number") < 0)
+ listType = listType[1];
+ if (listType)
{
- preHtml += '' + postHtml;
- }
- else
- {
- if(start){ // is it a start of a list with more than one item in?
- if(start[1] == 1){ // if its the first one at this level?
- lineClass = lineClass + " " + "list-start-" + listType; // Add start class to DIV node
- }
- preHtml += '- ';
- }else{
- preHtml += '
- '; // Handles pasted contents into existing lists
+ if(listType.indexOf("number") < 0)
+ {
+ preHtml += '' + postHtml;
}
- postHtml += '
';
- }
+ else
+ {
+ if(start){ // is it a start of a list with more than one item in?
+ if(start[1] == 1){ // if its the first one at this level?
+ lineClass = lineClass + " " + "list-start-" + listType; // Add start class to DIV node
+ }
+ preHtml += '- ';
+ }else{
+ preHtml += '
- '; // Handles pasted contents into existing lists
+ }
+ postHtml += '
';
+ }
+ }
+ processedMarker = true;
+ }
+ _.map(hooks.callAll("aceDomLineProcessLineAttributes", {
+ domline: domline,
+ cls: cls
+ }), function(modifier)
+ {
+ preHtml += modifier.preHtml;
+ postHtml += modifier.postHtml;
+ processedMarker |= modifier.processedMarker;
+ });
+ if( processedMarker ){
+ result.lineMarker += txt.length;
+ return; // don't append any text
}
- processedMarker = true;
}
- _.map(hooks.callAll("aceDomLineProcessLineAttributes", {
+ var href = null;
+ var simpleTags = null;
+ if (cls.indexOf('url') >= 0)
+ {
+ cls = cls.replace(/(^| )url:(\S+)/g, function(x0, space, url)
+ {
+ href = url;
+ return space + "url";
+ });
+ }
+ if (cls.indexOf('tag') >= 0)
+ {
+ cls = cls.replace(/(^| )tag:(\S+)/g, function(x0, space, tag)
+ {
+ if (!simpleTags) simpleTags = [];
+ simpleTags.push(tag.toLowerCase());
+ return space + tag;
+ });
+ }
+
+ var extraOpenTags = "";
+ var extraCloseTags = "";
+
+ _.map(hooks.callAll("aceCreateDomLine", {
domline: domline,
cls: cls
}), function(modifier)
{
- preHtml += modifier.preHtml;
- postHtml += modifier.postHtml;
- processedMarker |= modifier.processedMarker;
+ cls = modifier.cls;
+ extraOpenTags = extraOpenTags + modifier.extraOpenTags;
+ extraCloseTags = modifier.extraCloseTags + extraCloseTags;
});
- if( processedMarker ){
- result.lineMarker += txt.length;
- return; // don't append any text
- }
- }
- var href = null;
- var simpleTags = null;
- if (cls.indexOf('url') >= 0)
- {
- cls = cls.replace(/(^| )url:(\S+)/g, function(x0, space, url)
- {
- href = url;
- return space + "url";
- });
- }
- if (cls.indexOf('tag') >= 0)
- {
- cls = cls.replace(/(^| )tag:(\S+)/g, function(x0, space, tag)
- {
- if (!simpleTags) simpleTags = [];
- simpleTags.push(tag.toLowerCase());
- return space + tag;
- });
- }
- var extraOpenTags = "";
- var extraCloseTags = "";
-
- _.map(hooks.callAll("aceCreateDomLine", {
- domline: domline,
- cls: cls
- }), function(modifier)
- {
- cls = modifier.cls;
- extraOpenTags = extraOpenTags + modifier.extraOpenTags;
- extraCloseTags = modifier.extraCloseTags + extraCloseTags;
- });
-
- if ((!txt) && cls)
- {
- lineClass = domline.addToLineClass(lineClass, cls);
- }
- else if (txt)
- {
- if (href)
+ if ((!txt) && cls)
{
- if(!~href.indexOf("://") && !~href.indexOf("mailto:")) // if the url doesn't include a protocol prefix, assume http
+ lineClass = domline.addToLineClass(lineClass, cls);
+ }
+ else if (txt)
+ {
+ if (href)
{
- href = "http://"+href;
+ if(!~href.indexOf("://") && !~href.indexOf("mailto:")) // if the url doesn't include a protocol prefix, assume http
+ {
+ href = "http://"+href;
+ }
+ extraOpenTags = extraOpenTags + '';
+ extraCloseTags = '' + extraCloseTags;
}
- extraOpenTags = extraOpenTags + '';
- extraCloseTags = '' + extraCloseTags;
+ if (simpleTags)
+ {
+ simpleTags.sort();
+ extraOpenTags = extraOpenTags + '<' + simpleTags.join('><') + '>';
+ simpleTags.reverse();
+ extraCloseTags = '' + simpleTags.join('>') + '>' + extraCloseTags;
+ }
+ html.push('', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, '');
}
- if (simpleTags)
+ };
+ result.clearSpans = function()
+ {
+ html = [];
+ lineClass = ''; // non-null to cause update
+ result.lineMarker = 0;
+ };
+
+ function writeHTML()
+ {
+ var newHTML = perHtmlLineProcess(html.join(''));
+ if (!newHTML)
{
- simpleTags.sort();
- extraOpenTags = extraOpenTags + '<' + simpleTags.join('><') + '>';
- simpleTags.reverse();
- extraCloseTags = '' + simpleTags.join('>') + '>' + extraCloseTags;
+ if ((!document) || (!optBrowser))
+ {
+ newHTML += ' ';
+ }
+ else if (!optBrowser.msie)
+ {
+ newHTML += '
';
+ }
}
- html.push('', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, '');
+ if (nonEmpty)
+ {
+ newHTML = (preHtml || '') + newHTML + (postHtml || '');
+ }
+ html = preHtml = postHtml = ''; // free memory
+ if (newHTML !== curHTML)
+ {
+ curHTML = newHTML;
+ result.node.innerHTML = curHTML;
+ }
+ if (lineClass !== null) result.node.className = lineClass;
+
+ hooks.callAll("acePostWriteDomLineHTML", {
+ node: result.node
+ });
}
- };
- result.clearSpans = function()
- {
- html = [];
- lineClass = ''; // non-null to cause update
- result.lineMarker = 0;
+ result.prepareForAdd = writeHTML;
+ result.finishUpdate = writeHTML;
+ result.getInnerHTML = function()
+ {
+ return curHTML || '';
+ };
+ return result;
};
- function writeHTML()
+ domline.processSpaces = function(s, doesWrap)
{
- var newHTML = perHtmlLineProcess(html.join(''));
- if (!newHTML)
+ if (s.indexOf("<") < 0 && !doesWrap)
{
- if ((!document) || (!optBrowser))
- {
- newHTML += ' ';
- }
- else if (!optBrowser.msie)
- {
- newHTML += '
';
- }
+ // short-cut
+ return s.replace(/ /g, ' ');
}
- if (nonEmpty)
+ var parts = [];
+ s.replace(/<[^>]*>?| |[^ <]+/g, function(m)
{
- newHTML = (preHtml || '') + newHTML + (postHtml || '');
- }
- html = preHtml = postHtml = ''; // free memory
- if (newHTML !== curHTML)
- {
- curHTML = newHTML;
- result.node.innerHTML = curHTML;
- }
- if (lineClass !== null) result.node.className = lineClass;
-
- hooks.callAll("acePostWriteDomLineHTML", {
- node: result.node
+ parts.push(m);
});
- }
- result.prepareForAdd = writeHTML;
- result.finishUpdate = writeHTML;
- result.getInnerHTML = function()
- {
- return curHTML || '';
+ if (doesWrap)
+ {
+ var endOfLine = true;
+ var beforeSpace = false;
+ // last space in a run is normal, others are nbsp,
+ // end of line is nbsp
+ for (var i = parts.length - 1; i >= 0; i--)
+ {
+ var p = parts[i];
+ if (p == " ")
+ {
+ if (endOfLine || beforeSpace) parts[i] = ' ';
+ endOfLine = false;
+ beforeSpace = true;
+ }
+ else if (p.charAt(0) != "<")
+ {
+ endOfLine = false;
+ beforeSpace = false;
+ }
+ }
+ // beginning of line is nbsp
+ for (var i = 0; i < parts.length; i++)
+ {
+ var p = parts[i];
+ if (p == " ")
+ {
+ parts[i] = ' ';
+ break;
+ }
+ else if (p.charAt(0) != "<")
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ for (var i = 0; i < parts.length; i++)
+ {
+ var p = parts[i];
+ if (p == " ")
+ {
+ parts[i] = ' ';
+ }
+ }
+ }
+ return parts.join('');
};
- return result;
-};
-domline.processSpaces = function(s, doesWrap)
-{
- if (s.indexOf("<") < 0 && !doesWrap)
- {
- // short-cut
- return s.replace(/ /g, ' ');
- }
- var parts = [];
- s.replace(/<[^>]*>?| |[^ <]+/g, function(m)
- {
- parts.push(m);
- });
- if (doesWrap)
- {
- var endOfLine = true;
- var beforeSpace = false;
- // last space in a run is normal, others are nbsp,
- // end of line is nbsp
- for (var i = parts.length - 1; i >= 0; i--)
- {
- var p = parts[i];
- if (p == " ")
- {
- if (endOfLine || beforeSpace) parts[i] = ' ';
- endOfLine = false;
- beforeSpace = true;
- }
- else if (p.charAt(0) != "<")
- {
- endOfLine = false;
- beforeSpace = false;
- }
- }
- // beginning of line is nbsp
- for (var i = 0; i < parts.length; i++)
- {
- var p = parts[i];
- if (p == " ")
- {
- parts[i] = ' ';
- break;
- }
- else if (p.charAt(0) != "<")
- {
- break;
- }
- }
- }
- else
- {
- for (var i = 0; i < parts.length; i++)
- {
- var p = parts[i];
- if (p == " ")
- {
- parts[i] = ' ';
- }
- }
- }
- return parts.join('');
-};
+ exports.domline = domline;
-exports.domline = domline;
+ return exports;
+});
diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js
index 82efe2d98..ff45fffd3 100644
--- a/src/static/js/linestylefilter.js
+++ b/src/static/js/linestylefilter.js
@@ -28,338 +28,342 @@
// requires: plugins
// requires: undefined
-var Changeset = require('./Changeset');
-var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
-var linestylefilter = {};
-var _ = require('./underscore');
-var AttributeManager = require('./AttributeManager');
+define(['ep_etherpad-lite/static/js/pluginfw/hooks', 'underscore'], function (hooks, _) {
+ var exports = {};
-linestylefilter.ATTRIB_CLASSES = {
- 'bold': 'tag:b',
- 'italic': 'tag:i',
- 'underline': 'tag:u',
- 'strikethrough': 'tag:s'
-};
+ var Changeset = require('./Changeset');
+ var linestylefilter = {};
+ var AttributeManager = require('./AttributeManager');
-var lineAttributeMarker = 'lineAttribMarker';
-exports.lineAttributeMarker = lineAttributeMarker;
+ linestylefilter.ATTRIB_CLASSES = {
+ 'bold': 'tag:b',
+ 'italic': 'tag:i',
+ 'underline': 'tag:u',
+ 'strikethrough': 'tag:s'
+ };
-linestylefilter.getAuthorClassName = function(author)
-{
- return "author-" + author.replace(/[^a-y0-9]/g, function(c)
+ var lineAttributeMarker = 'lineAttribMarker';
+ exports.lineAttributeMarker = lineAttributeMarker;
+
+ linestylefilter.getAuthorClassName = function(author)
{
- if (c == ".") return "-";
- return 'z' + c.charCodeAt(0) + 'z';
- });
-};
-
-// lineLength is without newline; aline includes newline,
-// but may be falsy if lineLength == 0
-linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFunc, apool)
-{
-
- // Plugin Hook to add more Attrib Classes
- hooks.aCallAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES, function(err, ATTRIB_CLASSES){
- if(ATTRIB_CLASSES.length >= 1){
- linestylefilter.ATTRIB_CLASSES = ATTRIB_CLASSES[0];
- }
- });
-
- if (lineLength == 0) return textAndClassFunc;
-
- var nextAfterAuthorColors = textAndClassFunc;
-
- var authorColorFunc = (function()
- {
- var lineEnd = lineLength;
- var curIndex = 0;
- var extraClasses;
- var leftInAuthor;
-
- function attribsToClasses(attribs)
+ return "author-" + author.replace(/[^a-y0-9]/g, function(c)
{
- var classes = '';
- var isLineAttribMarker = false;
-
- Changeset.eachAttribNumber(attribs, function(n)
+ if (c == ".") return "-";
+ return 'z' + c.charCodeAt(0) + 'z';
+ });
+ };
+
+ // lineLength is without newline; aline includes newline,
+ // but may be falsy if lineLength == 0
+ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFunc, apool)
+ {
+
+ // Plugin Hook to add more Attrib Classes
+ hooks.aCallAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES, function(err, ATTRIB_CLASSES){
+ if(ATTRIB_CLASSES.length >= 1){
+ linestylefilter.ATTRIB_CLASSES = ATTRIB_CLASSES[0];
+ }
+ });
+
+ if (lineLength == 0) return textAndClassFunc;
+
+ var nextAfterAuthorColors = textAndClassFunc;
+
+ var authorColorFunc = (function()
+ {
+ var lineEnd = lineLength;
+ var curIndex = 0;
+ var extraClasses;
+ var leftInAuthor;
+
+ function attribsToClasses(attribs)
{
- var key = apool.getAttribKey(n);
- if (key)
+ var classes = '';
+ var isLineAttribMarker = false;
+
+ Changeset.eachAttribNumber(attribs, function(n)
{
- var value = apool.getAttribValue(n);
- if (value)
+ var key = apool.getAttribKey(n);
+ if (key)
{
- if (!isLineAttribMarker && _.indexOf(AttributeManager.lineAttributes, key) >= 0){
- isLineAttribMarker = true;
- }
- if (key == 'author')
+ var value = apool.getAttribValue(n);
+ if (value)
{
- classes += ' ' + linestylefilter.getAuthorClassName(value);
+ if (!isLineAttribMarker && _.indexOf(AttributeManager.lineAttributes, key) >= 0){
+ isLineAttribMarker = true;
+ }
+ if (key == 'author')
+ {
+ classes += ' ' + linestylefilter.getAuthorClassName(value);
+ }
+ else if (key == 'list')
+ {
+ classes += ' list:' + value;
+ }
+ else if (key == 'start')
+ {
+ classes += ' start:' + value;
+ }
+ else if (linestylefilter.ATTRIB_CLASSES[key])
+ {
+ classes += ' ' + linestylefilter.ATTRIB_CLASSES[key];
+ }
+ else
+ {
+ classes += hooks.callAllStr("aceAttribsToClasses", {
+ linestylefilter: linestylefilter,
+ key: key,
+ value: value
+ }, " ", " ", "");
+ }
}
- else if (key == 'list')
- {
- classes += ' list:' + value;
- }
- else if (key == 'start')
- {
- classes += ' start:' + value;
- }
- else if (linestylefilter.ATTRIB_CLASSES[key])
- {
- classes += ' ' + linestylefilter.ATTRIB_CLASSES[key];
- }
- else
- {
- classes += hooks.callAllStr("aceAttribsToClasses", {
- linestylefilter: linestylefilter,
- key: key,
- value: value
- }, " ", " ", "");
- }
+ }
+ });
+
+ if(isLineAttribMarker) classes += ' ' + lineAttributeMarker;
+ return classes.substring(1);
+ }
+
+ var attributionIter = Changeset.opIterator(aline);
+ var nextOp, nextOpClasses;
+
+ function goNextOp()
+ {
+ nextOp = attributionIter.next();
+ nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
+ }
+ goNextOp();
+
+ function nextClasses()
+ {
+ if (curIndex < lineEnd)
+ {
+ extraClasses = nextOpClasses;
+ leftInAuthor = nextOp.chars;
+ goNextOp();
+ while (nextOp.opcode && nextOpClasses == extraClasses)
+ {
+ leftInAuthor += nextOp.chars;
+ goNextOp();
}
}
- });
-
- if(isLineAttribMarker) classes += ' ' + lineAttributeMarker;
- return classes.substring(1);
- }
-
- var attributionIter = Changeset.opIterator(aline);
- var nextOp, nextOpClasses;
-
- function goNextOp()
- {
- nextOp = attributionIter.next();
- nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
- }
- goNextOp();
-
- function nextClasses()
- {
- if (curIndex < lineEnd)
- {
- extraClasses = nextOpClasses;
- leftInAuthor = nextOp.chars;
- goNextOp();
- while (nextOp.opcode && nextOpClasses == extraClasses)
- {
- leftInAuthor += nextOp.chars;
- goNextOp();
- }
}
- }
- nextClasses();
+ nextClasses();
- 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 || disableAuthors)
- {
- // prevent infinite loop if something funny's going on
- return nextAfterAuthorColors(txt, cls);
- }
- var spanSize = txt.length;
- if (spanSize > leftInAuthor)
- {
- spanSize = leftInAuthor;
- }
- var curTxt = txt.substring(0, spanSize);
- txt = txt.substring(spanSize);
- nextAfterAuthorColors(curTxt, (cls && cls + " ") + extraClasses);
- curIndex += spanSize;
- leftInAuthor -= spanSize;
- if (leftInAuthor == 0)
- {
- nextClasses();
- }
- }
- };
- })();
- return authorColorFunc;
-};
-
-linestylefilter.getAtSignSplitterFilter = function(lineText, textAndClassFunc)
-{
- var at = /@/g;
- at.lastIndex = 0;
- var splitPoints = null;
- var execResult;
- while ((execResult = at.exec(lineText)))
- {
- if (!splitPoints)
- {
- splitPoints = [];
- }
- splitPoints.push(execResult.index);
- }
-
- if (!splitPoints) return textAndClassFunc;
-
- return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints);
-};
-
-linestylefilter.getRegexpFilter = function(regExp, tag)
-{
- return function(lineText, textAndClassFunc)
- {
- regExp.lastIndex = 0;
- var regExpMatchs = null;
- var splitPoints = null;
- var execResult;
- while ((execResult = regExp.exec(lineText)))
- {
- if (!regExpMatchs)
- {
- regExpMatchs = [];
- splitPoints = [];
- }
- var startIndex = execResult.index;
- var regExpMatch = execResult[0];
- regExpMatchs.push([startIndex, regExpMatch]);
- splitPoints.push(startIndex, startIndex + regExpMatch.length);
- }
-
- if (!regExpMatchs) return textAndClassFunc;
-
- function regExpMatchForIndex(idx)
- {
- for (var k = 0; k < regExpMatchs.length; k++)
- {
- var u = regExpMatchs[k];
- if (idx >= u[0] && idx < u[0] + u[1].length)
- {
- return u[1];
- }
- }
- return false;
- }
-
- var handleRegExpMatchsAfterSplit = (function()
- {
- var curIndex = 0;
return function(txt, cls)
{
- var txtlen = txt.length;
- var newCls = cls;
- var regExpMatch = regExpMatchForIndex(curIndex);
- if (regExpMatch)
+
+ 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)
{
- newCls += " " + tag + ":" + regExpMatch;
+ if (leftInAuthor <= 0 || disableAuthors)
+ {
+ // prevent infinite loop if something funny's going on
+ return nextAfterAuthorColors(txt, cls);
+ }
+ var spanSize = txt.length;
+ if (spanSize > leftInAuthor)
+ {
+ spanSize = leftInAuthor;
+ }
+ var curTxt = txt.substring(0, spanSize);
+ txt = txt.substring(spanSize);
+ nextAfterAuthorColors(curTxt, (cls && cls + " ") + extraClasses);
+ curIndex += spanSize;
+ leftInAuthor -= spanSize;
+ if (leftInAuthor == 0)
+ {
+ nextClasses();
+ }
}
- textAndClassFunc(txt, newCls);
- curIndex += txtlen;
};
})();
-
- return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints);
+ return authorColorFunc;
};
-};
-
-linestylefilter.REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
-linestylefilter.REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=!;()\[\]$]/.source + '|' + linestylefilter.REGEX_WORDCHAR.source + ')');
-linestylefilter.REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|nfs):\/\/|mailto:|www\.)/.source + linestylefilter.REGEX_URLCHAR.source + '*(?![:.,;])' + linestylefilter.REGEX_URLCHAR.source, 'g');
-linestylefilter.getURLFilter = linestylefilter.getRegexpFilter(
-linestylefilter.REGEX_URL, 'url');
-
-linestylefilter.textAndClassFuncSplitter = function(func, splitPointsOpt)
-{
- var nextPointIndex = 0;
- var idx = 0;
-
- // don't split at 0
- while (splitPointsOpt && nextPointIndex < splitPointsOpt.length && splitPointsOpt[nextPointIndex] == 0)
+ linestylefilter.getAtSignSplitterFilter = function(lineText, textAndClassFunc)
{
- nextPointIndex++;
- }
-
- function spanHandler(txt, cls)
- {
- if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length)
+ var at = /@/g;
+ at.lastIndex = 0;
+ var splitPoints = null;
+ var execResult;
+ while ((execResult = at.exec(lineText)))
{
- func(txt, cls);
- idx += txt.length;
+ if (!splitPoints)
+ {
+ splitPoints = [];
+ }
+ splitPoints.push(execResult.index);
}
- else
+
+ if (!splitPoints) return textAndClassFunc;
+
+ return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints);
+ };
+
+ linestylefilter.getRegexpFilter = function(regExp, tag)
+ {
+ return function(lineText, textAndClassFunc)
{
- var splitPoints = splitPointsOpt;
- var pointLocInSpan = splitPoints[nextPointIndex] - idx;
- var txtlen = txt.length;
- if (pointLocInSpan >= txtlen)
+ regExp.lastIndex = 0;
+ var regExpMatchs = null;
+ var splitPoints = null;
+ var execResult;
+ while ((execResult = regExp.exec(lineText)))
+ {
+ if (!regExpMatchs)
+ {
+ regExpMatchs = [];
+ splitPoints = [];
+ }
+ var startIndex = execResult.index;
+ var regExpMatch = execResult[0];
+ regExpMatchs.push([startIndex, regExpMatch]);
+ splitPoints.push(startIndex, startIndex + regExpMatch.length);
+ }
+
+ if (!regExpMatchs) return textAndClassFunc;
+
+ function regExpMatchForIndex(idx)
+ {
+ for (var k = 0; k < regExpMatchs.length; k++)
+ {
+ var u = regExpMatchs[k];
+ if (idx >= u[0] && idx < u[0] + u[1].length)
+ {
+ return u[1];
+ }
+ }
+ return false;
+ }
+
+ var handleRegExpMatchsAfterSplit = (function()
+ {
+ var curIndex = 0;
+ return function(txt, cls)
+ {
+ var txtlen = txt.length;
+ var newCls = cls;
+ var regExpMatch = regExpMatchForIndex(curIndex);
+ if (regExpMatch)
+ {
+ newCls += " " + tag + ":" + regExpMatch;
+ }
+ textAndClassFunc(txt, newCls);
+ curIndex += txtlen;
+ };
+ })();
+
+ return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints);
+ };
+ };
+
+
+ linestylefilter.REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
+ linestylefilter.REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=!;()\[\]$]/.source + '|' + linestylefilter.REGEX_WORDCHAR.source + ')');
+ linestylefilter.REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|nfs):\/\/|mailto:|www\.)/.source + linestylefilter.REGEX_URLCHAR.source + '*(?![:.,;])' + linestylefilter.REGEX_URLCHAR.source, 'g');
+ linestylefilter.getURLFilter = linestylefilter.getRegexpFilter(
+ linestylefilter.REGEX_URL, 'url');
+
+ linestylefilter.textAndClassFuncSplitter = function(func, splitPointsOpt)
+ {
+ var nextPointIndex = 0;
+ var idx = 0;
+
+ // don't split at 0
+ while (splitPointsOpt && nextPointIndex < splitPointsOpt.length && splitPointsOpt[nextPointIndex] == 0)
+ {
+ nextPointIndex++;
+ }
+
+ function spanHandler(txt, cls)
+ {
+ if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length)
{
func(txt, cls);
idx += txt.length;
- if (pointLocInSpan == txtlen)
- {
- nextPointIndex++;
- }
}
else
{
- if (pointLocInSpan > 0)
+ var splitPoints = splitPointsOpt;
+ var pointLocInSpan = splitPoints[nextPointIndex] - idx;
+ var txtlen = txt.length;
+ if (pointLocInSpan >= txtlen)
{
- func(txt.substring(0, pointLocInSpan), cls);
- idx += pointLocInSpan;
+ func(txt, cls);
+ idx += txt.length;
+ if (pointLocInSpan == txtlen)
+ {
+ nextPointIndex++;
+ }
+ }
+ else
+ {
+ if (pointLocInSpan > 0)
+ {
+ func(txt.substring(0, pointLocInSpan), cls);
+ idx += pointLocInSpan;
+ }
+ nextPointIndex++;
+ // recurse
+ spanHandler(txt.substring(pointLocInSpan), cls);
}
- nextPointIndex++;
- // recurse
- spanHandler(txt.substring(pointLocInSpan), cls);
}
}
- }
- return spanHandler;
-};
+ return spanHandler;
+ };
-linestylefilter.getFilterStack = function(lineText, textAndClassFunc, abrowser)
-{
- var func = linestylefilter.getURLFilter(lineText, textAndClassFunc);
-
- var hookFilters = hooks.callAll("aceGetFilterStack", {
- linestylefilter: linestylefilter,
- browser: abrowser
- });
- _.map(hookFilters ,function(hookFilter)
+ linestylefilter.getFilterStack = function(lineText, textAndClassFunc, abrowser)
{
- func = hookFilter(lineText, func);
- });
+ var func = linestylefilter.getURLFilter(lineText, textAndClassFunc);
- if (abrowser !== undefined && abrowser.msie)
+ var hookFilters = hooks.callAll("aceGetFilterStack", {
+ linestylefilter: linestylefilter,
+ browser: abrowser
+ });
+ _.map(hookFilters ,function(hookFilter)
+ {
+ func = hookFilter(lineText, func);
+ });
+
+ if (abrowser !== undefined && abrowser.msie)
+ {
+ // IE7+ will take an e-mail address like and linkify it to foo@bar.com.
+ // We then normalize it back to text with no angle brackets. It's weird. So always
+ // break spans at an "at" sign.
+ func = linestylefilter.getAtSignSplitterFilter(
+ lineText, func);
+ }
+ return func;
+ };
+
+ // domLineObj is like that returned by domline.createDomLine
+ linestylefilter.populateDomLine = function(textLine, aline, apool, domLineObj)
{
- // IE7+ will take an e-mail address like and linkify it to foo@bar.com.
- // We then normalize it back to text with no angle brackets. It's weird. So always
- // break spans at an "at" sign.
- func = linestylefilter.getAtSignSplitterFilter(
- lineText, func);
- }
- return func;
-};
+ // remove final newline from text if any
+ var text = textLine;
+ if (text.slice(-1) == '\n')
+ {
+ text = text.substring(0, text.length - 1);
+ }
-// domLineObj is like that returned by domline.createDomLine
-linestylefilter.populateDomLine = function(textLine, aline, apool, domLineObj)
-{
- // remove final newline from text if any
- var text = textLine;
- if (text.slice(-1) == '\n')
- {
- text = text.substring(0, text.length - 1);
- }
+ function textAndClassFunc(tokenText, tokenClass)
+ {
+ domLineObj.appendSpan(tokenText, tokenClass);
+ }
- function textAndClassFunc(tokenText, tokenClass)
- {
- domLineObj.appendSpan(tokenText, tokenClass);
- }
+ var func = linestylefilter.getFilterStack(text, textAndClassFunc);
+ func = linestylefilter.getLineStyleFilter(text.length, aline, func, apool);
+ func(text, '');
+ };
- var func = linestylefilter.getFilterStack(text, textAndClassFunc);
- func = linestylefilter.getLineStyleFilter(text.length, aline, func, apool);
- func(text, '');
-};
+ exports.linestylefilter = linestylefilter;
-exports.linestylefilter = linestylefilter;
+ return exports;
+});
diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js
index d85b71e64..69eaa7c78 100644
--- a/src/static/js/timeslider.js
+++ b/src/static/js/timeslider.js
@@ -27,8 +27,9 @@ define([
'ep_etherpad-lite/static/js/rjquery',
'ep_etherpad-lite/static/js/pluginfw/hooks',
'ep_etherpad-lite/static/js/pad_utils',
- 'ep_etherpad-lite/static/js/broadcast_slider'
-], function($, hooks, padUtilsMod, broadcastSliderMod) {
+ 'ep_etherpad-lite/static/js/broadcast_slider',
+ 'ep_etherpad-lite/static/js/broadcast',
+], function($, hooks, padUtilsMod, broadcastSliderMod, broadcastMod) {
var exports = {};
JSON = window.requireKernel('./json2');
@@ -36,6 +37,11 @@ define([
var createCookie = padUtilsMod.createCookie;
var readCookie = padUtilsMod.readCookie;
var randomString = padUtilsMod.randomString;
+ var broadcastRevisionsMod = require('./broadcast_revisions');
+ var padimpexpMod = require('./pad_impexp');
+
+ //initialize export ui
+ require('./pad_impexp').padimpexp.init();
var token, padId, export_links;
@@ -142,11 +148,11 @@ define([
//load all script that doesn't work without the clientVars
BroadcastSlider = broadcastSliderMod.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
- require('./broadcast_revisions').loadBroadcastRevisionsJS();
- changesetLoader = require('./broadcast').loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider);
+ broadcastRevisionsMod.loadBroadcastRevisionsJS();
+ changesetLoader = broadcastMod.loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider);
//initialize export ui
- require('./pad_impexp').padimpexp.init();
+ padimpexpMod.padimpexp.init();
//change export urls when the slider moves
BroadcastSlider.onSlider(function(revno)
diff --git a/src/templates/pad.html b/src/templates/pad.html
index 47c84a635..f92a9a12d 100644
--- a/src/templates/pad.html
+++ b/src/templates/pad.html
@@ -406,11 +406,9 @@
[
'ep_etherpad-lite/static/js/rjquery',
'ep_etherpad-lite/static/js/pluginfw/client_plugins',
- 'ep_etherpad-lite/static/js/pluginfw/hooks',
- 'ep_etherpad-lite/static/js/pad',
- 'ep_etherpad-lite/static/js/chat',
- 'ep_etherpad-lite/static/js/pad_editbar',
- ], function ($, plugins, hooks, padMod, chatMod, padEditbarMod) {
+ 'ep_etherpad-lite/static/js/pluginfw/hooks'
+ ], function ($, plugins, hooks) {
+ console.log("hooks & plugins modules loaded");
window.$ = $; // Expose jQuery #HACK
window.jQuery = $;
@@ -423,20 +421,32 @@
plugins.update(function () {
hooks.plugins = plugins;
+ console.log("hooks.plugins initialized");
+
// Call documentReady hook
$(function() {
hooks.aCallAll('documentReady');
});
- padMod.baseURL = baseURL;
- padMod.init();
- });
+ requirejs(
+ [
+ 'ep_etherpad-lite/static/js/pad',
+ 'ep_etherpad-lite/static/js/chat',
+ 'ep_etherpad-lite/static/js/pad_editbar',
+ ], function (padMod, chatMod, padEditbarMod) {
+ console.log("pad loaded");
- /* TODO: These globals shouldn't exist. */
- pad = padMod.pad;
- chat = chatMod.chat;
- padeditbar = padEditbarMod.padeditbar;
- padimpexp = window.requireKernel('ep_etherpad-lite/static/js/pad_impexp').padimpexp;
+ padMod.baseURL = baseURL;
+ padMod.init();
+
+ /* TODO: These globals shouldn't exist. */
+ pad = padMod.pad;
+ chat = chatMod.chat;
+ padeditbar = padEditbarMod.padeditbar;
+ padimpexp = window.requireKernel('ep_etherpad-lite/static/js/pad_impexp').padimpexp;
+ }
+ );
+ });
}
);
}());