etherpad-lite/src/static/js/ace2_inner.js

5521 lines
162 KiB
JavaScript

/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var _, $, jQuery, plugins, Ace2Common;
var browser = require('./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;
}
}
Ace2Common = require('./ace2_common');
plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
$ = jQuery = require('./rjquery').$;
_ = require("./underscore");
var isNodeText = Ace2Common.isNodeText,
getAssoc = Ace2Common.getAssoc,
setAssoc = Ace2Common.setAssoc,
isTextNode = Ace2Common.isTextNode,
binarySearchInfinite = Ace2Common.binarySearchInfinite,
htmlPrettyEscape = Ace2Common.htmlPrettyEscape,
noop = Ace2Common.noop;
var hooks = require('./pluginfw/hooks');
function Ace2Inner(){
var makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
var colorutils = require('./colorutils').colorutils;
var makeContentCollector = require('./contentcollector').makeContentCollector;
var makeCSSManager = require('./cssmanager').makeCSSManager;
var domline = require('./domline').domline;
var AttribPool = require('./AttributePool');
var Changeset = require('./Changeset');
var ChangesetUtils = require('./ChangesetUtils');
var linestylefilter = require('./linestylefilter').linestylefilter;
var SkipList = require('./skiplist');
var undoModule = require('./undomodule').undoModule;
var AttributeManager = require('./AttributeManager');
var Scroll = require('./scroll');
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 FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough'];
var SELECT_BUTTON_CLASS = 'selected';
var caughtErrors = [];
var thisAuthor = '';
var disposed = false;
var editorInfo = parent.editorInfo;
var iframe = window.frameElement;
var outerWin = iframe.ace_outerWin;
iframe.ace_outerWin = null; // prevent IE 6 memory leak
var sideDiv = iframe.nextSibling;
var lineMetricsDiv = sideDiv.nextSibling;
initLineNumbers();
var scroll = Scroll.init(outerWin);
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;
}
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()
{
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()
{
fastIncorp(1);
f(
{
setDocumentAttributedText: function(atext)
{
setDocAText(atext);
},
applyChangesetToDocument: function(changeset, preferInsertionAfterCaret)
{
var oldEventType = currentCallStack.editEvent.eventType;
currentCallStack.startNewEvent("nonundoable");
performDocumentApplyChangeset(changeset, preferInsertionAfterCaret);
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
}
if (!info)
{
dynamicCSS.removeSelectorStyle(authorSelector);
parentDynamicCSS.removeSelectorStyle(authorSelector);
}
else
{
if (info.bgcolor)
{
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')
// 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;
}
// 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")
{
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 (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 _nonScrollableEditEvents = {
"applyChangesToBase": 1
};
_.each(hooks.callAll('aceRegisterNonScrollableEditEvents'), function(eventType) {
_nonScrollableEditEvents[eventType] = 1;
});
function isScrollableEditEvent(eventType)
{
return !_nonScrollableEditEvents[eventType];
}
var currentCallStack = null;
function inCallStack(type, action)
{
if (disposed) return;
if (currentCallStack)
{
console.error("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 (isPadLoading(evt.eventType))
{
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) && isScrollableEditEvent(cs.type))
{
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;
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").addClass("noprewrap");
}
}
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);
});
}
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) || '&nbsp;' /*empty line*/ );
entry = rep.lines.next(entry);
}
}
return '<div class="syntax"><div>' + buf.join('</div>\n<div>') + '</div></div>';
}
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(outerWin.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 {
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 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 = scroll.getVisibleCharRange(rep);
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();
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 = topLevel(nds[i]);
if (n && 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 = "<div><!-- --></div>";
}
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(scroll.getVisibleCharRange(rep), 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
};
function isStyleAttribute(aname)
{
return !!STYLE_ATTRIBS[aname];
}
function isDefaultLineAttribute(aname)
{
return AttributeManager.DEFAULT_LINE_ATTRIBUTES.indexOf(aname) !== -1;
}
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];
var shouldIndent = parent.parent.clientVars.indentationOnNewLine;
if (shouldIndent && /[\[\(\:\{]\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)
{
if (browser.msie) {
return n.innerText;
} else {
return 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];
var result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
}
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)
{
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)
{
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))
{
currentCallStack.selectionAffected = true;
}
}
}
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())
{
var o = opIter.next();
if (o.opcode == '-' || o.opcode == '=')
{
curOffset += o.chars;
if (o.lines)
{
curLine += o.lines;
curCol = 0;
}
else
{
curCol += o.chars;
}
}
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");
}
(function doRecordUndoInformation(changes)
{
var editEvent = currentCallStack.editEvent;
if (editEvent.eventType == "nonundoable")
{
if (!editEvent.changeset)
{
editEvent.changeset = changes;
}
else
{
editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
}
}
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]: <CCC end[1] CCC>-------\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, prevChar){
if (!(rep.selStart && rep.selEnd)) return
var isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]);
if(isNotSelection){
if(prevChar){
// If it's not the start of the line
if(rep.selStart[1] !== 0){
rep.selStart[1]--;
}
}
}
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;
if (documentAttributeManager.lineHasMarker(n)) {
selectionStartInLine = 1; // ignore "*" used as line marker
}
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;
}
}
var attributeValue = selectionAllHasIt ? '' : 'true';
documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [[attributeName, attributeValue]]);
if (attribIsFormattingStyle(attributeName)) {
updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ...
}
}
editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection;
function performDocumentReplaceSelection(newText)
{
if (!(rep.selStart && rep.selEnd)) return;
performDocumentReplaceRange(rep.selStart, rep.selEnd, newText);
}
// 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;
});
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;
}
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];
};
}
function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint)
{
// we need to take into account both the styles attributes & attributes defined by
// the plugins, so basically we can ignore only the default line attribs used by
// Etherpad
function incorpedAttribFilter(anum)
{
return !isDefaultLineAttribute(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;
// select the formatting buttons when there is the style applied on selection
selectFormattingButtonIfLineHasStyleApplied(rep);
hooks.callAll('aceSelectionChanged', {
rep: rep,
callstack: currentCallStack,
documentAttributeManager: documentAttributeManager,
});
// we scroll when user places the caret at the last line of the pad
// when this settings is enabled
var docTextChanged = currentCallStack.docTextChanged;
if(!docTextChanged){
var isScrollableEvent = !isPadLoading(currentCallStack.type) && isScrollableEditEvent(currentCallStack.type);
var innerHeight = getInnerHeight();
scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight);
}
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 isPadLoading(eventType)
{
return (eventType === 'setup') || (eventType === 'setBaseText') || (eventType === 'importText');
}
function updateStyleButtonState(attribName, hasStyleOnRepSelection) {
var $formattingButton = parent.parent.$('[data-key="' + attribName + '"]').find('a');
$formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection);
}
function attribIsFormattingStyle(attributeName) {
return _.contains(FORMATTING_STYLES, attributeName);
}
function selectFormattingButtonIfLineHasStyleApplied (rep) {
_.each(FORMATTING_STYLES, function (style) {
var hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style);
updateStyleButtonState(style, hasStyleOnRepSelection);
})
}
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
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;
}
}
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;
}
function getViewPortTopBottom()
{
var theTop = scroll.getScrollY();
var doc = outerWin.document;
var height = doc.documentElement.clientHeight; // includes padding
// we have to get the exactly height of the viewport. So it has to subtract all the values which changes
// the viewport height (E.g. padding, position top)
var viewportExtraSpacesAndPosition = getEditorPositionTop() + getPaddingTopAddedWhenPageViewIsEnable();
return {
top: theTop,
bottom: (theTop + height - viewportExtraSpacesAndPosition)
};
}
function getEditorPositionTop()
{
var editor = parent.document.getElementsByTagName('iframe');
var editorPositionTop = editor[0].offsetTop;
return editorPositionTop;
}
// ep_page_view adds padding-top, which makes the viewport smaller
function getPaddingTopAddedWhenPageViewIsEnable()
{
var rootDocument = parent.parent.document;
var aceOuter = rootDocument.getElementsByName("ace_outer");
var aceOuterPaddingTop = parseInt($(aceOuter).css("padding-top"));
return aceOuterPaddingTop;
}
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();
}
}
hideEditBarDropdowns();
}
function hideEditBarDropdowns()
{
if(window.parent.parent.padeditbar){ // required in case its in an iframe should probably use parent.. See Issue 327 https://github.com/ether/etherpad-lite/issues/327
window.parent.parent.padeditbar.toggleDropDown("none");
}
}
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)
{
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 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)
{
// 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)
{
// 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
{
var docChar = caretDocChar();
if (docChar > 0)
{
if (evt.metaKey || evt.ctrlKey || evt.altKey)
{
// 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, '');
}
}
}
}
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;
// 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 = parent.parent.$('#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){
parent.parent.$("#chaticon").css("opacity",".3"); // make chaticon opacity low when user types near it
}else{
parent.parent.$("#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 || browser.firefox) ? (type == "keydown") : (type == "keypress"));
var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (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 (evt.key === "Dead"){
// If it's a dead key we don't want to do any Etherpad behavior.
stopped = true;
return true;
}
else if (type == "keydown")
{
outsideKeyDown(evt);
}
if (!stopped)
{
var specialHandledInHook = hooks.callAll('aceKeyEvent', {
callstack: currentCallStack,
editorInfo: editorInfo,
rep: rep,
documentAttributeManager: documentAttributeManager,
evt:evt
});
// if any hook returned true, set specialHandled with true
if (specialHandledInHook) {
specialHandled = _.contains(specialHandledInHook, true);
}
var padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled;
if ((!specialHandled) && altKey && isTypeForSpecialKey && keyCode == 120 && padShortcutEnabled.altF9){
// 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 = parent.parent.$('#editbar').children("ul").first().children().first().children().first().children().first();
$(this).blur();
firstEditbarElement.focus();
evt.preventDefault();
}
if ((!specialHandled) && altKey && keyCode == 67 && type === "keydown" && padShortcutEnabled.altC){
// Alt c focuses on the Chat window
$(this).blur();
parent.parent.chat.show();
parent.parent.$("#chatinput").focus();
evt.preventDefault();
}
if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === "keydown" && padShortcutEnabled.cmdShift2){
// 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 = parent.parent.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(parent.parent.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(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(" & ");
}
parent.parent.$.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 && padShortcutEnabled.delete)
{
// "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 && padShortcutEnabled.return)
{
// 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()
{
outerWin.scrollBy(-100, 0);
}, 0);
specialHandled = true;
}
if ((!specialHandled) && isTypeForSpecialKey && keyCode == 27 && padShortcutEnabled.esc)
{
// prevent esc key;
// in mozilla versions 14-19 avoid reconnecting pad.
fastIncorp(4);
evt.preventDefault();
specialHandled = true;
}
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "s" && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdS) /* Do a saved revision on ctrl S */
{
evt.preventDefault();
var originalBackground = parent.parent.$('#revisionlink').css("background")
parent.parent.$('#revisionlink').css({"background":"lightyellow"});
scheduler.setTimeout(function(){
parent.parent.$('#revisionlink').css({"background":originalBackground});
}, 1000);
parent.parent.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) && padShortcutEnabled.tab)
{
// 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 && padShortcutEnabled.cmdZ)
{
// 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) && padShortcutEnabled.cmdY)
{
// cmd-Y (redo)
fastIncorp(10);
evt.preventDefault();
doUndoRedo("redo");
specialHandled = true;
}
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "b" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdB)
{
// cmd-B (bold)
fastIncorp(13);
evt.preventDefault();
toggleAttributeOnSelection('bold');
specialHandled = true;
}
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "i" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdI)
{
// cmd-I (italic)
fastIncorp(14);
evt.preventDefault();
toggleAttributeOnSelection('italic');
specialHandled = true;
}
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "u" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdU)
{
// cmd-U (underline)
fastIncorp(15);
evt.preventDefault();
toggleAttributeOnSelection('underline');
specialHandled = true;
}
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "5" && (evt.metaKey || evt.ctrlKey) && evt.altKey !== true && padShortcutEnabled.cmd5)
{
// 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 && padShortcutEnabled.cmdShiftL)
{
// cmd-shift-L (unorderedlist)
fastIncorp(9);
evt.preventDefault();
doInsertUnorderedList()
specialHandled = true;
}
if ((!specialHandled) && isTypeForCmdKey && ((String.fromCharCode(which).toLowerCase() == "n" && padShortcutEnabled.cmdShiftN) || (String.fromCharCode(which) == 1 && padShortcutEnabled.cmdShift1)) && (evt.metaKey || evt.ctrlKey) && evt.shiftKey)
{
// cmd-shift-N and cmd-shift-1 (orderedlist)
fastIncorp(9);
evt.preventDefault();
doInsertOrderedList()
specialHandled = true;
}
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "c" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftC) {
// cmd-shift-C (clearauthorship)
fastIncorp(9);
evt.preventDefault();
CMDS.clearauthorship();
}
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "h" && (evt.ctrlKey) && padShortcutEnabled.cmdH)
{
// cmd-H (backspace)
fastIncorp(20);
evt.preventDefault();
doDeleteKey();
specialHandled = true;
}
if((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome){ scroll.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 = scroll.getVisibleLineRange(rep);
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 = scroll.getVisibleLineRange(rep); // 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 && padShortcutEnabled.pageUp){
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 && padShortcutEnabled.pageDown){ // 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;
scroll.setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document
}, 200);
}
// scroll to viewport when user presses arrow keys and caret is out of the viewport
if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40)){
// we use arrowKeyWasReleased to avoid triggering the animation when a key is continuously pressed
// this makes the scroll smooth
if(!continuouslyPressingArrowKey(type)){
// We use getSelection() instead of rep to get the caret position. This avoids errors like when
// the caret position is not synchronized with the rep. For example, when an user presses arrow
// down to scroll the pad without releasing the key. When the key is released the rep is not
// synchronized, so we don't get the right node where caret is.
var selection = getSelection();
if(selection){
var arrowUp = evt.which === 38;
var innerHeight = getInnerHeight();
scroll.scrollWhenPressArrowKeys(arrowUp, rep, innerHeight);
}
}
}
}
if (type == "keydown")
{
idleWorkTimer.atLeast(500);
}
else if (type == "keypress")
{
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;
var arrowKeyWasReleased = true;
function continuouslyPressingArrowKey(type) {
var firstTimeKeyIsContinuouslyPressed = false;
if (type == 'keyup') arrowKeyWasReleased = true;
else if (type == 'keydown' && arrowKeyWasReleased) {
firstTimeKeyIsContinuouslyPressed = true;
arrowKeyWasReleased = false;
}
return !firstTimeKeyIsContinuouslyPressed;
}
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;
function updateBrowserSelectionFromRep()
{
// requires normalized DOM!
var selStart = rep.selStart,
selEnd = rep.selEnd;
if (!(selStart && selEnd))
{
setSelection(null);
return;
}
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)
{
var browserSelection;
try
{
browserSelection = doc.selection;
}
catch (e)
{}
if (!browserSelection) return null;
var origSelectionRange;
try
{
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)
{
return {
node: parNode,
index: 0,
maxIndex: 1
};
}
else if (rng.compareEndPoints("EndToEnd", rangeWithin) === 0)
{
if (isBlockElement(parNode) && parNode.nextSibling)
{
// caret after block is not consistent across browsers
// (same line vs next) so put caret before next node
return {
node: parNode.nextSibling,
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))
{
var nodeRange = rangeForElementNode(n);
var startComp = rng.compareEndPoints("StartToStart", nodeRange);
var endComp = rng.compareEndPoints("EndToEnd", nodeRange);
if (startComp >= 0 && endComp <= 0)
{
var index = 0;
if (startComp > 0)
{
index = 1;
}
return {
node: n,
index: index,
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),
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;
}
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)
{
//&& (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);
}
}
}
}
}
function childIndex(n)
{
var idx = 0;
while (n.previousSibling)
{
idx++;
n = n.previousSibling;
}
return idx;
}
function fixView()
{
// 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(iframe.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(outerWin.document.documentElement.style, 'overflowY', 'auto');
}
else
{
if (browser.msie) setIfNecessary(outerWin.document.documentElement.style, 'overflowY', 'scroll');
}
if (doesWrap)
{
newWidth = viewWidth;
}
else
{
if (newWidth < viewWidth) newWidth = viewWidth;
}
setIfNecessary(iframe.style, "height", newHeight + "px");
setIfNecessary(iframe.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 <body> 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", "");
}
}
var win = outerWin;
var r = 20;
enforceEditability();
$(sideDiv).addClass('sidedivdelayed');
}
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 "<p>one</p><p>two</p><p>three</p>",
// 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('<br>');
//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);
// dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer
$(outerWin.document).on("click", hideEditBarDropdowns);
// 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();
}
// Call paste hook
hooks.callAll('acePaste', {
editorInfo: editorInfo,
rep: rep,
documentAttributeManager: documentAttributeManager,
e: e
});
})
// We reference document here, this is because if we don't this will expose a bug
// in Google Chrome. This bug will cause the last character on the last line to
// not fire an event when dropped into..
$(document).on("drop", function(e){
if(e.target.a || e.target.localName === "a"){
e.preventDefault();
}
// Bug fix: when user drags some content and drop it far from its origin, we
// need to merge the changes into a single changeset. So mark origin with <style>,
// in order to make content be observed by incorporateUserChanges() (see
// observeSuspiciousNodes() for more info)
var selection = getSelection();
if (selection){
var firstLineSelected = topLevel(selection.startPoint.node);
var lastLineSelected = topLevel(selection.endPoint.node);
var lineBeforeSelection = firstLineSelected.previousSibling;
var lineAfterSelection = lastLineSelected.nextSibling;
var neighbor = lineBeforeSelection || lineAfterSelection;
neighbor.appendChild(document.createElement('style'));
}
// Call drop hook
hooks.callAll('aceDrop', {
editorInfo: editorInfo,
rep: rep,
documentAttributeManager: documentAttributeManager,
e: e
});
});
// 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 topLevel(n)
{
if ((!n) || n == root) return null;
while (n.parentNode != root)
{
n = n.parentNode;
}
return n;
}
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 = outerWin;
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 = outerWin;
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 = outerWin;
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 = outerWin;
var odoc = win.document;
return odoc.documentElement.clientWidth;
}
function scrollXHorizontallyIntoView(pixelX)
{
var win = outerWin;
var odoc = outerWin.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 innerHeight = getInnerHeight();
scroll.scrollNodeVerticallyIntoView(rep, innerHeight);
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);
// if already a list, deindent
if (allLinesAreList && level != 1) { level = level - 1; }
// if already indented, then add a level of indentation to the list
else if (t && !allLinesAreList) { level = level + 1; }
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 = '<table border="0" cellpadding="0" cellspacing="0" align="right"><tr><td id="sidedivinner" class="sidedivinner"><div>1</div></td></tr></table>';
sideDivInner = outerWin.document.getElementById("sidedivinner");
$(sideDiv).addClass("sidediv");
}
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 = doc.body.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...)
if (b === doc.body.firstChild) {
// It's the first line. For line number alignment purposes, its
// height is taken to be the top offset of the next line. If we
// didn't do this special case, we would miss out on any top margin
// included on the first line. The default stylesheet doesn't add
// extra margins/padding, but plugins might.
h = b.nextSibling.offsetTop - window.getComputedStyle(doc.body).getPropertyValue("padding-top");
} else {
h = b.nextSibling.offsetTop - b.offsetTop;
}
}
if (h)
{
var hpx = h + "px";
if (a.style.height != hpx) {
a.style.height = hpx;
}
}
a = a.nextSibling;
b = b.nextSibling;
n++;
}
}
if (newNumLines != lineNumbersShown)
{
var container = sideDivInner;
var odoc = outerWin.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()
{
parent.readyFunc(); // defined in code that sets up the inner iframe
}, 0);
isSetUp = true;
});
}
}
exports.init = function () {
var editor = new Ace2Inner()
editor.init();
};