/** * 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 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) || ' ' /*empty line*/ ); entry = rep.lines.next(entry); } } return '
' + buf.join('
\n
') + '
'; } 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 = "
"; } 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 }; var OTHER_INCORPED_ATTRIBS = { insertorder: true, author: true }; function isStyleAttribute(aname) { return !!STYLE_ATTRIBS[aname]; } function isOtherIncorpedAttribute(aname) { return !!OTHER_INCORPED_ATTRIBS[aname]; } function insertDomLines(nodeToAddAfter, infoStructs, isTimeUp) { isTimeUp = (isTimeUp || function() { return false; }); var lastEntry; var lineStartOffset; if (infoStructs.length < 1) return; var startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node)); var endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node)); var charStart = rep.lines.offsetOfEntry(startEntry); var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp); _.each(infoStructs, function(info) { var p2 = PROFILER("insertLine", false); var node = info.node; var key = uniqueId(node); var entry; p2.mark("findEntry"); if (lastEntry) { // optimization to avoid recalculation var next = rep.lines.next(lastEntry); if (next && next.key == key) { entry = next; lineStartOffset += lastEntry.width; } } if (!entry) { p2.literal(1, "nonopt"); entry = rep.lines.atKey(key); lineStartOffset = rep.lines.offsetOfKey(key); } else p2.literal(0, "nonopt"); lastEntry = entry; p2.mark("spans"); getSpansForLine(entry, function(tokenText, tokenClass) { info.appendSpan(tokenText, tokenClass); }, lineStartOffset, isTimeUp()); //else if (entry.text.length > 0) { //info.appendSpan(entry.text, 'dirty'); //} p2.mark("addLine"); info.prepareForAdd(); entry.lineMarker = info.lineMarker; if (!nodeToAddAfter) { root.insertBefore(node, root.firstChild); } else { root.insertBefore(node, nodeToAddAfter.nextSibling); } nodeToAddAfter = node; info.notifyAdded(); p2.mark("markClean"); markNodeClean(node); p2.end(); }); } function isCaret() { return (rep.selStart && rep.selEnd && rep.selStart[0] == rep.selEnd[0] && rep.selStart[1] == rep.selEnd[1]); } editorInfo.ace_isCaret = isCaret; // prereq: isCaret() function caretLine() { return rep.selStart[0]; } editorInfo.ace_caretLine = caretLine; function caretColumn() { return rep.selStart[1]; } editorInfo.ace_caretColumn = caretColumn; function caretDocChar() { return rep.lines.offsetOfIndex(caretLine()) + caretColumn(); } editorInfo.ace_caretDocChar = caretDocChar; function handleReturnIndentation() { // on return, indent to level of previous line if (isCaret() && caretColumn() === 0 && caretLine() > 0) { var lineNum = caretLine(); var thisLine = rep.lines.atIndex(lineNum); var prevLine = rep.lines.prev(thisLine); var prevLineText = prevLine.text; var theIndent = /^ *(?:)/.exec(prevLineText)[0]; 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]: -------\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; } } if (selectionAllHasIt) { documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ [attributeName, ''] ]); } else { documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ [attributeName, 'true'] ]); } } editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection; function performDocumentReplaceSelection(newText) { if (!(rep.selStart && rep.selEnd)) return; performDocumentReplaceRange(rep.selStart, rep.selEnd, newText); } // 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) { function incorpedAttribFilter(anum) { return !isOtherIncorpedAttribute(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; 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 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) ? (type == "keydown") : (type == "keypress")); var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress")); var stopped = false; inCallStackIfNecessary("handleKeyEvent", function() { if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ )) { // in IE, special keys don't send keypress, the keydown does the action if (!outsideKeyPress(evt)) { evt.preventDefault(); stopped = true; } } else if (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 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 "

one

two

three

", // which becomes "one\ntwo\nthree". We can get the correct text // from the clipboard directly, but we still have to let the paste // happen to get the style information. var clipText = window.clipboardData && window.clipboardData.getData("Text"); if (clipText && doc.selection) { // this "paste" event seems to mess with the selection whether we try to // stop it or not, so can't really do document-level manipulation now // or in an idle call-stack. instead, use IE native manipulation //function escapeLine(txt) { //return processSpaces(escapeHTML(textify(txt))); //} //var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('
'); //doc.selection.createRange().pasteHTML(newHTML); //evt.preventDefault(); //iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify); } } var inInternationalComposition = false; function handleCompositionEvent(evt) { // international input events, fired in FF3, at least; allow e.g. Japanese input if (evt.type == "compositionstart") { inInternationalComposition = true; } else if (evt.type == "compositionend") { inInternationalComposition = false; } } editorInfo.ace_getInInternationalComposition = function () { return inInternationalComposition; } function bindTheEventHandlers() { $(document).on("keydown", handleKeyEvent); $(document).on("keypress", handleKeyEvent); $(document).on("keyup", handleKeyEvent); $(document).on("click", handleClick); // 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