pad.libre-service.eu-etherpad/src/static/js/ace2_inner.js

4377 lines
149 KiB
JavaScript
Raw Normal View History

/**
2013-06-14 19:37:41 +02:00
* 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
*/
2011-03-26 14:10:41 +01:00
/**
* Copyright 2009 Google Inc.
2011-07-07 19:59:34 +02:00
*
2011-03-26 14:10:41 +01:00
* 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
2011-07-07 19:59:34 +02:00
*
2011-03-26 14:10:41 +01:00
* http://www.apache.org/licenses/LICENSE-2.0
2011-07-07 19:59:34 +02:00
*
2011-03-26 14:10:41 +01:00
* 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.
*/
2020-11-23 19:24:19 +01:00
let _, $, jQuery, plugins, Ace2Common;
const browser = require('./browser');
if (browser.msie) {
2015-01-21 15:25:24 +01:00
// Honestly fuck IE royally.
// Basically every hack we have since V11 causes a problem
2020-11-23 19:24:19 +01:00
if (parseInt(browser.version) >= 11) {
2015-01-21 16:01:39 +01:00
delete browser.msie;
browser.chrome = true;
2015-01-21 16:21:31 +01:00
browser.modernIE = true;
2015-01-21 15:25:24 +01:00
}
}
2015-01-21 15:55:29 +01:00
Ace2Common = require('./ace2_common');
plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
$ = jQuery = require('./rjquery').$;
2020-11-23 19:24:19 +01:00
_ = require('./underscore');
const isNodeText = Ace2Common.isNodeText;
const getAssoc = Ace2Common.getAssoc;
const setAssoc = Ace2Common.setAssoc;
const isTextNode = Ace2Common.isTextNode;
const binarySearchInfinite = Ace2Common.binarySearchInfinite;
const htmlPrettyEscape = Ace2Common.htmlPrettyEscape;
const noop = Ace2Common.noop;
const hooks = require('./pluginfw/hooks');
function Ace2Inner() {
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
const colorutils = require('./colorutils').colorutils;
const makeContentCollector = require('./contentcollector').makeContentCollector;
const makeCSSManager = require('./cssmanager').makeCSSManager;
const domline = require('./domline').domline;
const AttribPool = require('./AttributePool');
const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils');
const linestylefilter = require('./linestylefilter').linestylefilter;
const SkipList = require('./skiplist');
const undoModule = require('./undomodule').undoModule;
const AttributeManager = require('./AttributeManager');
const Scroll = require('./scroll');
const DEBUG = false; // $$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
2013-06-14 19:37:41 +02:00
// changed to false
2020-11-23 19:24:19 +01:00
let isSetUp = false;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const THE_TAB = ' '; // 4
const MAX_LIST_LEVEL = 16;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough'];
const SELECT_BUTTON_CLASS = 'selected';
2020-11-23 19:24:19 +01:00
const caughtErrors = [];
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let thisAuthor = '';
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let disposed = false;
const editorInfo = parent.editorInfo;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const iframe = window.frameElement;
const outerWin = iframe.ace_outerWin;
2011-03-26 14:10:41 +01:00
iframe.ace_outerWin = null; // prevent IE 6 memory leak
2020-11-23 19:24:19 +01:00
const sideDiv = iframe.nextSibling;
const lineMetricsDiv = sideDiv.nextSibling;
let lineNumbersShown;
let sideDivInner;
2011-03-26 14:10:41 +01:00
initLineNumbers();
2020-11-23 19:24:19 +01:00
const scroll = Scroll.init(outerWin);
2020-11-23 19:24:19 +01:00
let outsideKeyDown = noop;
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
let outsideKeyPress = function () { return true; };
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
let outsideNotifyDirty = noop;
2011-03-26 14:10:41 +01:00
// 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!
2020-11-23 19:24:19 +01:00
const rep = {
2012-03-18 11:34:56 +01:00
lines: new SkipList(),
2011-07-07 19:59:34 +02:00
selStart: null,
selEnd: null,
selFocusAtStart: false,
2020-11-23 19:24:19 +01:00
alltext: '',
2011-07-07 19:59:34 +02:00
alines: [],
2020-11-23 19:24:19 +01:00
apool: new AttribPool(),
2011-07-07 19:59:34 +02:00
};
2013-06-14 19:37:41 +02:00
// lines, alltext, alines, and DOM are set up in init()
2020-11-23 19:24:19 +01:00
if (undoModule.enabled) {
2011-03-26 14:10:41 +01:00
undoModule.apool = rep.apool;
}
2020-11-23 19:24:19 +01:00
let root, doc; // set in init()
let isEditable = true;
let doesWrap = true;
let hasLineNumbers = true;
let isStyled = true;
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
let console = (DEBUG && window.console);
let documentAttributeManager;
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
if (!window.console) {
const names = ['log', 'debug', 'info', 'warn', 'error', 'assert', 'dir', 'dirxml', 'group', 'groupEnd', 'time', 'timeEnd', 'count', 'trace', 'profile', 'profileEnd'];
2011-03-26 14:10:41 +01:00
console = {};
2020-11-23 19:24:19 +01:00
for (let i = 0; i < names.length; ++i) console[names[i]] = noop;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
let PROFILER = window.PROFILER;
if (!PROFILER) {
PROFILER = function () {
2011-07-07 19:59:34 +02:00
return {
start: noop,
mark: noop,
literal: noop,
end: noop,
2020-11-23 19:24:19 +01:00
cancel: noop,
2011-07-07 19:59:34 +02:00
};
};
}
2011-03-26 14:10:41 +01:00
// "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.
2020-11-23 19:24:19 +01:00
let dmesg = noop;
2011-03-26 14:10:41 +01:00
window.dmesg = noop;
2020-11-23 19:24:19 +01:00
const scheduler = parent; // hack for opera required
2020-11-23 19:24:19 +01:00
let dynamicCSS = null;
let outerDynamicCSS = null;
let parentDynamicCSS = null;
2011-07-07 19:59:34 +02:00
function initDynamicCSS() {
2020-11-23 19:24:19 +01:00
dynamicCSS = makeCSSManager('dynamicsyntax');
outerDynamicCSS = makeCSSManager('dynamicsyntax', 'outer');
parentDynamicCSS = makeCSSManager('dynamicsyntax', 'parent');
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const changesetTracker = makeChangesetTracker(scheduler, rep.apool, {
withCallbacks(operationName, f) {
inCallStackIfNecessary(operationName, () => {
2011-03-26 14:10:41 +01:00
fastIncorp(1);
2011-07-07 19:59:34 +02:00
f(
2020-11-23 19:24:19 +01:00
{
setDocumentAttributedText(atext) {
setDocAText(atext);
},
applyChangesetToDocument(changeset, preferInsertionAfterCaret) {
const oldEventType = currentCallStack.editEvent.eventType;
currentCallStack.startNewEvent('nonundoable');
performDocumentApplyChangeset(changeset, preferInsertionAfterCaret);
currentCallStack.startNewEvent(oldEventType);
},
});
2011-03-26 14:10:41 +01:00
});
2020-11-23 19:24:19 +01:00
},
2011-03-26 14:10:41 +01:00
});
2020-11-23 19:24:19 +01:00
const authorInfos = {}; // presence of key determines if author is present in doc
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
function getAuthorInfos() {
return authorInfos;
2020-11-23 19:24:19 +01:00
}
editorInfo.ace_getAuthorInfos = getAuthorInfos;
function setAuthorStyle(author, info) {
2013-06-06 06:30:48 +02:00
if (!dynamicCSS) {
return;
}
2020-11-23 19:24:19 +01:00
const authorSelector = getAuthorColorClassSelector(getAuthorClassName(author));
2013-06-06 06:59:56 +02:00
2020-11-23 19:24:19 +01:00
const authorStyleSet = hooks.callAll('aceSetAuthorStyle', {
dynamicCSS,
parentDynamicCSS,
outerDynamicCSS,
info,
author,
authorSelector,
2013-06-06 06:59:56 +02:00
});
// Prevent default behaviour if any hook says so
2020-11-23 19:24:19 +01:00
if (_.any(authorStyleSet, (it) => it)) {
return;
2013-06-06 06:59:56 +02:00
}
2020-11-23 19:24:19 +01:00
if (!info) {
2013-06-06 06:30:48 +02:00
dynamicCSS.removeSelectorStyle(authorSelector);
parentDynamicCSS.removeSelectorStyle(authorSelector);
2020-11-23 19:24:19 +01:00
} else if (info.bgcolor) {
let bgcolor = info.bgcolor;
if ((typeof info.fade) === 'number') {
bgcolor = fadeColor(bgcolor, info.fade);
}
2013-06-06 06:30:48 +02:00
2020-11-23 19:24:19 +01:00
const authorStyle = dynamicCSS.selectorStyle(authorSelector);
const parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector);
2013-06-06 06:30:48 +02:00
2020-11-23 19:24:19 +01:00
// author color
authorStyle.backgroundColor = bgcolor;
parentAuthorStyle.backgroundColor = bgcolor;
2013-06-06 06:30:48 +02:00
2020-11-23 19:24:19 +01:00
const textColor = colorutils.textColorFromBackgroundColor(bgcolor, parent.parent.clientVars.skinName);
authorStyle.color = textColor;
parentAuthorStyle.color = textColor;
2013-06-06 06:30:48 +02:00
}
}
function setAuthorInfo(author, info) {
2020-11-23 19:24:19 +01:00
if ((typeof author) !== 'string') {
// Potentially caused by: https://github.com/ether/etherpad-lite/issues/2802");
2020-11-23 19:24:19 +01:00
throw new Error(`setAuthorInfo: author (${author}) is not a string`);
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (!info) {
2011-03-26 14:10:41 +01:00
delete authorInfos[author];
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
authorInfos[author] = info;
}
2013-06-06 06:30:48 +02:00
setAuthorStyle(author, info);
2011-03-26 14:10:41 +01:00
}
function getAuthorClassName(author) {
2020-11-23 19:24:19 +01:00
return `author-${author.replace(/[^a-y0-9]/g, (c) => {
if (c == '.') return '-';
return `z${c.charCodeAt(0)}z`;
})}`;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function className2Author(className) {
2020-11-23 19:24:19 +01:00
if (className.substring(0, 7) == 'author-') {
return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
if (cc == '-') { return '.'; } else if (cc.charAt(0) == 'z') {
2011-07-07 19:59:34 +02:00
return String.fromCharCode(Number(cc.slice(1, -1)));
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
return cc;
}
});
}
return null;
}
2011-07-07 19:59:34 +02:00
function getAuthorColorClassSelector(oneClassName) {
2020-11-23 19:24:19 +01:00
return `.authorColors .${oneClassName}`;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function fadeColor(colorCSS, fadeFrac) {
2020-11-23 19:24:19 +01:00
let color = colorutils.css2triple(colorCSS);
2011-07-07 19:59:34 +02:00
color = colorutils.blend(color, [1, 1, 1], fadeFrac);
2011-03-26 14:10:41 +01:00
return colorutils.triple2css(color);
}
2020-11-23 19:24:19 +01:00
editorInfo.ace_getRep = function () {
2011-03-26 14:10:41 +01:00
return rep;
2012-02-19 14:52:24 +01:00
};
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
editorInfo.ace_getAuthor = function () {
2012-05-30 17:18:43 +02:00
return thisAuthor;
2020-11-23 19:24:19 +01:00
};
2012-05-30 17:18:43 +02:00
2020-11-23 19:24:19 +01:00
const _nonScrollableEditEvents = {
applyChangesToBase: 1,
};
2020-11-23 19:24:19 +01:00
_.each(hooks.callAll('aceRegisterNonScrollableEditEvents'), (eventType) => {
_nonScrollableEditEvents[eventType] = 1;
});
function isScrollableEditEvent(eventType) {
return !_nonScrollableEditEvents[eventType];
}
2011-03-26 14:10:41 +01:00
var currentCallStack = null;
2011-07-07 19:59:34 +02:00
function inCallStack(type, action) {
2011-03-26 14:10:41 +01:00
if (disposed) return;
2020-11-23 19:24:19 +01:00
if (currentCallStack) {
// Do not uncomment this in production. It will break Etherpad being provided in iFrames. I'm leaving this in for testing usefulness.
// top.console.error("Can't enter callstack " + type + ", already in " + currentCallStack.type);
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
let profiling = false;
2011-07-07 19:59:34 +02:00
function profileRest() {
2011-03-26 14:10:41 +01:00
profiling = true;
}
function newEditEvent(eventType) {
2011-07-07 19:59:34 +02:00
return {
2020-11-23 19:24:19 +01:00
eventType,
backset: null,
2011-07-07 19:59:34 +02:00
};
}
function submitOldEvent(evt) {
2020-11-23 19:24:19 +01:00
if (rep.selStart && rep.selEnd) {
const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
2011-07-07 19:59:34 +02:00
evt.selStart = selStartChar;
evt.selEnd = selEndChar;
evt.selFocusAtStart = rep.selFocusAtStart;
}
2020-11-23 19:24:19 +01:00
if (undoModule.enabled) {
let undoWorked = false;
try {
if (isPadLoading(evt.eventType)) {
2011-07-07 19:59:34 +02:00
undoModule.clearHistory();
2020-11-23 19:24:19 +01:00
} else if (evt.eventType == 'nonundoable') {
if (evt.changeset) {
2011-07-07 19:59:34 +02:00
undoModule.reportExternalChange(evt.changeset);
}
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
undoModule.reportEvent(evt);
}
undoWorked = true;
2020-11-23 19:24:19 +01:00
} finally {
if (!undoWorked) {
2011-07-07 19:59:34 +02:00
undoModule.enabled = false; // for safety
}
}
}
}
function startNewEvent(eventType, dontSubmitOld) {
2020-11-23 19:24:19 +01:00
const oldEvent = currentCallStack.editEvent;
if (!dontSubmitOld) {
2011-07-07 19:59:34 +02:00
submitOldEvent(oldEvent);
2011-03-26 14:10:41 +01:00
}
currentCallStack.editEvent = newEditEvent(eventType);
return oldEvent;
}
2011-07-07 19:59:34 +02:00
currentCallStack = {
2020-11-23 19:24:19 +01:00
type,
2011-07-07 19:59:34 +02:00
docTextChanged: false,
selectionAffected: false,
userChangedSelection: false,
domClean: false,
2020-11-23 19:24:19 +01:00
profileRest,
2011-07-07 19:59:34 +02:00
isUserChange: false,
// is this a "user change" type of call-stack
repChanged: false,
editEvent: newEditEvent(type),
2020-11-23 19:24:19 +01:00
startNewEvent,
2011-07-07 19:59:34 +02:00
};
2020-11-23 19:24:19 +01:00
let cleanExit = false;
let result;
try {
2011-03-26 14:10:41 +01:00
result = action();
2012-05-30 17:18:43 +02:00
hooks.callAll('aceEditEvent', {
callstack: currentCallStack,
2020-11-23 19:24:19 +01:00
editorInfo,
rep,
documentAttributeManager,
2012-05-30 17:18:43 +02:00
});
2011-03-26 14:10:41 +01:00
cleanExit = true;
2020-11-23 19:24:19 +01:00
} catch (e) {
2011-07-07 19:59:34 +02:00
caughtErrors.push(
2020-11-23 19:24:19 +01:00
{
error: e,
time: +new Date(),
});
2011-03-26 14:10:41 +01:00
dmesg(e.toString());
throw e;
2020-11-23 19:24:19 +01:00
} finally {
const cs = currentCallStack;
if (cleanExit) {
2011-07-07 19:59:34 +02:00
submitOldEvent(cs.editEvent);
2020-11-23 19:24:19 +01:00
if (cs.domClean && cs.type != 'setup') {
// if (cs.isUserChange)
// {
// if (cs.repChanged) parenModule.notifyChange();
// else parenModule.notifyTick();
// }
2020-11-23 19:24:19 +01:00
if (cs.selectionAffected) {
2011-07-07 19:59:34 +02:00
updateBrowserSelectionFromRep();
}
2020-11-23 19:24:19 +01:00
if ((cs.docTextChanged || cs.userChangedSelection) && isScrollableEditEvent(cs.type)) {
2011-07-07 19:59:34 +02:00
scrollSelectionIntoView();
}
2020-11-23 19:24:19 +01:00
if (cs.docTextChanged && cs.type.indexOf('importText') < 0) {
2011-07-07 19:59:34 +02:00
outsideNotifyDirty();
}
}
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
// non-clean exit
2020-11-23 19:24:19 +01:00
if (currentCallStack.type == 'idleWorkTimer') {
2011-07-07 19:59:34 +02:00
idleWorkTimer.atLeast(1000);
}
2011-03-26 14:10:41 +01:00
}
currentCallStack = null;
}
return result;
}
editorInfo.ace_inCallStack = inCallStack;
function inCallStackIfNecessary(type, action) {
2020-11-23 19:24:19 +01:00
if (!currentCallStack) {
2011-03-26 14:10:41 +01:00
inCallStack(type, action);
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
action();
}
}
editorInfo.ace_inCallStackIfNecessary = inCallStackIfNecessary;
function dispose() {
2011-03-26 14:10:41 +01:00
disposed = true;
if (idleWorkTimer) idleWorkTimer.never();
teardown();
}
function setWraps(newVal) {
2011-03-26 14:10:41 +01:00
doesWrap = newVal;
2020-11-23 19:24:19 +01:00
const dwClass = 'doesWrap';
root.classList.toggle('doesWrap', doesWrap);
2020-11-23 19:24:19 +01:00
scheduler.setTimeout(() => {
inCallStackIfNecessary('setWraps', () => {
2011-07-07 19:59:34 +02:00
fastIncorp(7);
recreateDOM();
fixView();
2011-03-26 14:10:41 +01:00
});
}, 0);
}
function setStyled(newVal) {
2020-11-23 19:24:19 +01:00
const oldVal = isStyled;
isStyled = !!newVal;
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
if (newVal != oldVal) {
if (!newVal) {
2011-07-07 19:59:34 +02:00
// clear styles
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary('setStyled', () => {
2011-07-07 19:59:34 +02:00
fastIncorp(12);
2020-11-23 19:24:19 +01:00
const clearStyles = [];
for (const k in STYLE_ATTRIBS) {
2011-07-07 19:59:34 +02:00
clearStyles.push([k, '']);
}
performDocumentApplyAttributesToCharRange(0, rep.alltext.length, clearStyles);
});
2011-03-26 14:10:41 +01:00
}
}
}
function setTextFace(face) {
2020-06-02 11:25:43 +02:00
root.style.fontFamily = face;
lineMetricsDiv.style.fontFamily = face;
2011-03-26 14:10:41 +01:00
}
function recreateDOM() {
2011-03-26 14:10:41 +01:00
// precond: normalized
recolorLinesInRange(0, rep.alltext.length);
}
function setEditable(newVal) {
2011-03-26 14:10:41 +01:00
isEditable = newVal;
root.contentEditable = isEditable ? 'true' : 'false';
root.classList.toggle('static', !isEditable);
2011-03-26 14:10:41 +01:00
}
function enforceEditability() {
2011-03-26 14:10:41 +01:00
setEditable(isEditable);
}
function importText(text, undoable, dontProcess) {
2020-11-23 19:24:19 +01:00
let lines;
if (dontProcess) {
if (text.charAt(text.length - 1) != '\n') {
throw new Error('new raw text must end with newline');
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (/[\r\t\xa0]/.exec(text)) {
throw new Error('new raw text must not contain CR, tab, or nbsp');
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
lines = text.substring(0, text.length - 1).split('\n');
2020-11-23 19:24:19 +01:00
} else {
2012-03-17 13:36:42 +01:00
lines = _.map(text.split('\n'), textify);
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
let newText = '\n';
if (lines.length > 0) {
newText = `${lines.join('\n')}\n`;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
2011-03-26 14:10:41 +01:00
setDocText(newText);
});
2020-11-23 19:24:19 +01:00
if (dontProcess && rep.alltext != text) {
throw new Error('mismatch error setting raw text in importText');
2011-03-26 14:10:41 +01:00
}
}
function importAText(atext, apoolJsonObj, undoable) {
2011-03-26 14:10:41 +01:00
atext = Changeset.cloneAText(atext);
2020-11-23 19:24:19 +01:00
if (apoolJsonObj) {
const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
2011-03-26 14:10:41 +01:00
atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
}
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
2011-03-26 14:10:41 +01:00
setDocAText(atext);
});
}
function setDocAText(atext) {
2020-11-23 19:24:19 +01:00
if (atext.text === '') {
/*
* The server is fine with atext.text being an empty string, but the front
* end is not, and crashes.
*
* It is not clear if this is a problem in the server or in the client
* code, and this is a client-side hack fix. The underlying problem needs
* to be investigated.
*
* See for reference:
* - https://github.com/ether/etherpad-lite/issues/3861
*/
2020-11-23 19:24:19 +01:00
atext.text = '\n';
}
2011-03-26 14:10:41 +01:00
fastIncorp(8);
2020-11-23 19:24:19 +01:00
const oldLen = rep.lines.totalWidth();
const numLines = rep.lines.length();
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
const assem = Changeset.smartOpAssembler();
const o = Changeset.newOp('-');
2011-03-26 14:10:41 +01:00
o.chars = upToLastLine;
2011-07-07 19:59:34 +02:00
o.lines = numLines - 1;
2011-03-26 14:10:41 +01:00
assem.append(o);
o.chars = lastLineLength;
o.lines = 0;
assem.append(o);
Changeset.appendATextToAssembler(atext, assem);
2020-11-23 19:24:19 +01:00
const newLen = oldLen + assem.getLengthChange();
const changeset = Changeset.checkRep(
Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
2011-03-26 14:10:41 +01:00
performDocumentApplyChangeset(changeset);
2011-07-07 19:59:34 +02:00
performSelectionChange([0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]);
2011-03-26 14:10:41 +01:00
idleWorkTimer.atMost(100);
2020-11-23 19:24:19 +01:00
if (rep.alltext != atext.text) {
2011-03-26 14:10:41 +01:00
dmesg(htmlPrettyEscape(rep.alltext));
dmesg(htmlPrettyEscape(atext.text));
2020-11-23 19:24:19 +01:00
throw new Error('mismatch error setting raw text in setDocAText');
2011-03-26 14:10:41 +01:00
}
}
function setDocText(text) {
2011-03-26 14:10:41 +01:00
setDocAText(Changeset.makeAText(text));
}
function getDocText() {
2020-11-23 19:24:19 +01:00
const alltext = rep.alltext;
let len = alltext.length;
2011-03-26 14:10:41 +01:00
if (len > 0) len--; // final extra newline
return alltext.substring(0, len);
}
function exportText() {
2020-11-23 19:24:19 +01:00
if (currentCallStack && !currentCallStack.domClean) {
inCallStackIfNecessary('exportText', () => {
2011-07-07 19:59:34 +02:00
fastIncorp(2);
});
2011-03-26 14:10:41 +01:00
}
return getDocText();
}
function editorChangedSize() {
2011-03-26 14:10:41 +01:00
fixView();
}
function setOnKeyPress(handler) {
2011-03-26 14:10:41 +01:00
outsideKeyPress = handler;
}
function setOnKeyDown(handler) {
2011-03-26 14:10:41 +01:00
outsideKeyDown = handler;
}
function setNotifyDirty(handler) {
2011-03-26 14:10:41 +01:00
outsideNotifyDirty = handler;
}
function getFormattedCode() {
2020-11-23 19:24:19 +01:00
if (currentCallStack && !currentCallStack.domClean) {
inCallStackIfNecessary('getFormattedCode', incorporateUserChanges);
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const buf = [];
if (rep.lines.length() > 0) {
2011-03-26 14:10:41 +01:00
// should be the case, even for empty file
2020-11-23 19:24:19 +01:00
let entry = rep.lines.atIndex(0);
while (entry) {
const domInfo = entry.domInfo;
buf.push((domInfo && domInfo.getInnerHTML()) || domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || '&nbsp;' /* empty line*/);
2011-07-07 19:59:34 +02:00
entry = rep.lines.next(entry);
2011-03-26 14:10:41 +01:00
}
}
2020-11-23 19:24:19 +01:00
return `<div class="syntax"><div>${buf.join('</div>\n<div>')}</div></div>`;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const CMDS = {
clearauthorship(prompt) {
if ((!(rep.selStart && rep.selEnd)) || isCaret()) {
if (prompt) {
2011-03-26 14:10:41 +01:00
prompt();
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
performDocumentApplyAttributesToCharRange(0, rep.alltext.length, [
2020-11-23 19:24:19 +01:00
['author', ''],
2011-07-07 19:59:34 +02:00
]);
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
setAttributeOnSelection('author', '');
}
2020-11-23 19:24:19 +01:00
},
2011-03-26 14:10:41 +01:00
};
function execCommand(cmd) {
2011-03-26 14:10:41 +01:00
cmd = cmd.toLowerCase();
2020-11-23 19:24:19 +01:00
const cmdArgs = Array.prototype.slice.call(arguments, 1);
if (CMDS[cmd]) {
inCallStackIfNecessary(cmd, () => {
2011-07-07 19:59:34 +02:00
fastIncorp(9);
CMDS[cmd].apply(CMDS, cmdArgs);
2011-03-26 14:10:41 +01:00
});
}
}
function replaceRange(start, end, text) {
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary('replaceRange', () => {
2011-03-26 14:10:41 +01:00
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;
2020-11-23 19:24:19 +01:00
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;
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
editorInfo.ace_callWithAce = function (fn, callStack, normalize) {
let wrapper = function () {
2012-02-19 14:52:24 +01:00
return fn(editorInfo);
};
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
if (normalize !== undefined) {
const wrapper1 = wrapper;
wrapper = function () {
2011-03-26 14:10:41 +01:00
editorInfo.ace_fastIncorp(9);
2011-07-07 19:59:34 +02:00
wrapper1();
2012-02-19 14:52:24 +01:00
};
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (callStack !== undefined) {
2011-03-26 14:10:41 +01:00
return editorInfo.ace_inCallStack(callStack, wrapper);
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
return wrapper();
}
2012-02-19 14:52:24 +01:00
};
2011-03-26 14:10:41 +01:00
2012-02-21 22:29:40 +01:00
// This methed exposes a setter for some ace properties
// @param key the name of the parameter
// @param value the value to set to
2020-11-23 19:24:19 +01:00
editorInfo.ace_setProperty = function (key, value) {
2012-02-21 22:29:40 +01:00
// These properties are exposed
2020-11-23 19:24:19 +01:00
const setters = {
2012-02-21 22:29:40 +01:00
wraps: setWraps,
showsauthorcolors: (val) => root.classList.toggle('authorColors', !!val),
showsuserselections: (val) => root.classList.toggle('userSelections', !!val),
2020-11-23 19:24:19 +01:00
showslinenumbers(value) {
hasLineNumbers = !!value;
sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers);
2012-02-21 22:29:40 +01:00
fixView();
},
grayedout: (val) => outerWin.document.body.classList.toggle('grayedout', !!val),
2020-11-23 19:24:19 +01:00
dmesg() { dmesg = window.dmesg = value; },
userauthor(value) {
2012-04-05 00:50:04 +02:00
thisAuthor = String(value);
documentAttributeManager.author = thisAuthor;
},
2012-02-21 22:29:40 +01:00
styled: setStyled,
textface: setTextFace,
2020-11-23 19:24:19 +01:00
rtlistrue(value) {
root.classList.toggle('rtl', value);
root.classList.toggle('ltr', !value);
2020-11-23 19:24:19 +01:00
document.documentElement.dir = value ? 'rtl' : 'ltr';
},
2012-02-21 22:29:40 +01:00
};
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
const setter = setters[key.toLowerCase()];
2013-06-14 19:37:41 +02:00
// check if setter is present
2020-11-23 19:24:19 +01:00
if (setter !== undefined) {
setter(value);
2011-12-04 19:55:35 +01:00
}
2012-02-19 14:52:24 +01:00
};
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
editorInfo.ace_setBaseText = function (txt) {
2011-03-26 14:10:41 +01:00
changesetTracker.setBaseText(txt);
};
2020-11-23 19:24:19 +01:00
editorInfo.ace_setBaseAttributedText = function (atxt, apoolJsonObj) {
2011-03-26 14:10:41 +01:00
changesetTracker.setBaseAttributedText(atxt, apoolJsonObj);
};
2020-11-23 19:24:19 +01:00
editorInfo.ace_applyChangesToBase = function (c, optAuthor, apoolJsonObj) {
2011-03-26 14:10:41 +01:00
changesetTracker.applyChangesToBase(c, optAuthor, apoolJsonObj);
};
2020-11-23 19:24:19 +01:00
editorInfo.ace_prepareUserChangeset = function () {
2011-03-26 14:10:41 +01:00
return changesetTracker.prepareUserChangeset();
};
2020-11-23 19:24:19 +01:00
editorInfo.ace_applyPreparedChangesetToBase = function () {
2011-03-26 14:10:41 +01:00
changesetTracker.applyPreparedChangesetToBase();
};
2020-11-23 19:24:19 +01:00
editorInfo.ace_setUserChangeNotificationCallback = function (f) {
2011-03-26 14:10:41 +01:00
changesetTracker.setUserChangeNotificationCallback(f);
};
2020-11-23 19:24:19 +01:00
editorInfo.ace_setAuthorInfo = function (author, info) {
2011-03-26 14:10:41 +01:00
setAuthorInfo(author, info);
};
2020-11-23 19:24:19 +01:00
editorInfo.ace_setAuthorSelectionRange = function (author, start, end) {
2011-03-26 14:10:41 +01:00
changesetTracker.setAuthorSelectionRange(author, start, end);
};
2020-11-23 19:24:19 +01:00
editorInfo.ace_getUnhandledErrors = function () {
2011-03-26 14:10:41 +01:00
return caughtErrors.slice();
};
2020-11-23 19:24:19 +01:00
editorInfo.ace_getDocument = function () {
return doc;
};
2020-11-23 19:24:19 +01:00
editorInfo.ace_getDebugProperty = function (prop) {
if (prop == 'debugger') {
2011-03-26 14:10:41 +01:00
// obfuscate "eval" so as not to scare yuicompressor
2020-11-23 19:24:19 +01:00
window['ev' + 'al']('debugger');
} else if (prop == 'rep') {
2011-03-26 14:10:41 +01:00
return rep;
2020-11-23 19:24:19 +01:00
} else if (prop == 'window') {
2011-03-26 14:10:41 +01:00
return window;
2020-11-23 19:24:19 +01:00
} else if (prop == 'document') {
2011-03-26 14:10:41 +01:00
return document;
}
return undefined;
};
function now() {
return Date.now();
2011-07-07 19:59:34 +02:00
}
2011-03-26 14:10:41 +01:00
function newTimeLimit(ms) {
2020-11-23 19:24:19 +01:00
const startTime = now();
let lastElapsed = 0;
let exceededAlready = false;
let printedTrace = false;
const isTimeUp = function () {
if (exceededAlready) {
if ((!printedTrace)) { // && now() - startTime - ms > 300) {
printedTrace = true;
2011-07-07 19:59:34 +02:00
}
2020-11-23 19:24:19 +01:00
return true;
}
const elapsed = now() - startTime;
if (elapsed > ms) {
exceededAlready = true;
return true;
} else {
lastElapsed = elapsed;
return false;
}
};
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
isTimeUp.elapsed = function () {
2011-07-07 19:59:34 +02:00
return now() - startTime;
2012-02-19 14:52:24 +01:00
};
2011-03-26 14:10:41 +01:00
return isTimeUp;
}
function makeIdleAction(func) {
2020-11-23 19:24:19 +01:00
let scheduledTimeout = null;
let scheduledTime = 0;
2011-07-07 19:59:34 +02:00
function unschedule() {
2020-11-23 19:24:19 +01:00
if (scheduledTimeout) {
scheduler.clearTimeout(scheduledTimeout);
2011-07-07 19:59:34 +02:00
scheduledTimeout = null;
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
function reschedule(time) {
2011-03-26 14:10:41 +01:00
unschedule();
scheduledTime = time;
2020-11-23 19:24:19 +01:00
let delay = time - now();
2011-03-26 14:10:41 +01:00
if (delay < 0) delay = 0;
scheduledTimeout = scheduler.setTimeout(callback, delay);
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function callback() {
2011-03-26 14:10:41 +01:00
scheduledTimeout = null;
// func may reschedule the action
func();
}
return {
2020-11-23 19:24:19 +01:00
atMost(ms) {
const latestTime = now() + ms;
if ((!scheduledTimeout) || scheduledTime > latestTime) {
2011-07-07 19:59:34 +02:00
reschedule(latestTime);
}
2011-03-26 14:10:41 +01:00
},
// atLeast(ms) will schedule the action if not scheduled yet.
// In other words, "infinity" is replaced by ms, even though
// it is technically larger.
2020-11-23 19:24:19 +01:00
atLeast(ms) {
const earliestTime = now() + ms;
if ((!scheduledTimeout) || scheduledTime < earliestTime) {
2011-07-07 19:59:34 +02:00
reschedule(earliestTime);
}
2011-03-26 14:10:41 +01:00
},
2020-11-23 19:24:19 +01:00
never() {
2011-07-07 19:59:34 +02:00
unschedule();
2020-11-23 19:24:19 +01:00
},
2012-02-19 14:52:24 +01:00
};
2011-03-26 14:10:41 +01:00
}
function fastIncorp(n) {
2011-03-26 14:10:41 +01:00
// normalize but don't do any lexing or anything
incorporateUserChanges();
2011-03-26 14:10:41 +01:00
}
editorInfo.ace_fastIncorp = fastIncorp;
2020-11-23 19:24:19 +01:00
var idleWorkTimer = makeIdleAction(() => {
if (inInternationalComposition) {
2011-03-26 14:10:41 +01:00
// don't do idle input incorporation during international input composition
idleWorkTimer.atLeast(500);
return;
}
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary('idleWorkTimer', () => {
const isTimeUp = newTimeLimit(250);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let finishedImportantWork = false;
let finishedWork = false;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
try {
incorporateUserChanges();
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
if (isTimeUp()) return;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
updateLineNumbers(); // update line numbers if any time left
if (isTimeUp()) return;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const visibleRange = scroll.getVisibleCharRange(rep);
const docRange = [0, rep.lines.totalWidth()];
2011-07-07 19:59:34 +02:00
finishedImportantWork = true;
finishedWork = true;
2020-11-23 19:24:19 +01:00
} finally {
if (finishedWork) {
2011-07-07 19:59:34 +02:00
idleWorkTimer.atMost(1000);
2020-11-23 19:24:19 +01:00
} else if (finishedImportantWork) {
2011-07-07 19:59:34 +02:00
// 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);
2020-11-23 19:24:19 +01:00
} else {
let timeToWait = Math.round(isTimeUp.elapsed() / 2);
2011-07-07 19:59:34 +02:00
if (timeToWait < 100) timeToWait = 100;
idleWorkTimer.atMost(timeToWait);
}
2011-03-26 14:10:41 +01:00
}
});
});
2020-11-23 19:24:19 +01:00
let _nextId = 1;
2011-07-07 19:59:34 +02:00
function uniqueId(n) {
2011-03-26 14:10:41 +01:00
// not actually guaranteed to be unique, e.g. if user copy-pastes
// nodes with ids
2020-11-23 19:24:19 +01:00
const nid = n.id;
2011-03-26 14:10:41 +01:00
if (nid) return nid;
2020-11-23 19:24:19 +01:00
return (n.id = `magicdomid${_nextId++}`);
2011-03-26 14:10:41 +01:00
}
function recolorLinesInRange(startChar, endChar) {
2011-03-26 14:10:41 +01:00
if (endChar <= startChar) return;
if (startChar < 0 || startChar >= rep.lines.totalWidth()) return;
2020-11-23 19:24:19 +01:00
let lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary
let lineStart = rep.lines.offsetOfEntry(lineEntry);
let lineIndex = rep.lines.indexOfEntry(lineEntry);
let selectionNeedsResetting = false;
let firstLine = null;
let lastLine = null;
2011-03-26 14:10:41 +01:00
// tokenFunc function; accesses current value of lineEntry and curDocChar,
// also mutates curDocChar
2020-11-23 19:24:19 +01:00
let curDocChar;
const tokenFunc = function (tokenText, tokenClass) {
2020-11-23 19:24:19 +01:00
lineEntry.domInfo.appendSpan(tokenText, tokenClass);
};
2011-03-26 14:10:41 +01:00
while (lineEntry && lineStart < endChar) {
2020-11-23 19:24:19 +01:00
const lineEnd = lineStart + lineEntry.width;
2011-03-26 14:10:41 +01:00
curDocChar = lineStart;
lineEntry.domInfo.clearSpans();
getSpansForLine(lineEntry, tokenFunc, lineStart);
lineEntry.domInfo.finishUpdate();
markNodeClean(lineEntry.lineNode);
2020-11-23 19:24:19 +01:00
if (rep.selStart && rep.selStart[0] == lineIndex || rep.selEnd && rep.selEnd[0] == lineIndex) {
2011-07-07 19:59:34 +02:00
selectionNeedsResetting = true;
2011-03-26 14:10:41 +01:00
}
if (firstLine === null) firstLine = lineIndex;
lastLine = lineIndex;
lineStart = lineEnd;
lineEntry = rep.lines.next(lineEntry);
lineIndex++;
}
2020-11-23 19:24:19 +01:00
if (selectionNeedsResetting) {
2011-03-26 14:10:41 +01:00
currentCallStack.selectionAffected = true;
}
}
// like getSpansForRange, but for a line, and the func takes (text,class)
// instead of (width,class); excludes the trailing '\n' from
// consideration by func
2011-07-07 19:59:34 +02:00
function getSpansForLine(lineEntry, textAndClassFunc, lineEntryOffsetHint) {
2020-11-23 19:24:19 +01:00
let lineEntryOffset = lineEntryOffsetHint;
if ((typeof lineEntryOffset) !== 'number') {
2011-03-26 14:10:41 +01:00
lineEntryOffset = rep.lines.offsetOfEntry(lineEntry);
}
2020-11-23 19:24:19 +01:00
const text = lineEntry.text;
const width = lineEntry.width; // text.length+1
if (text.length === 0) {
2011-03-26 14:10:41 +01:00
// allow getLineStyleFilter to set line-div styles
2020-11-23 19:24:19 +01:00
const func = linestylefilter.getLineStyleFilter(
0, '', textAndClassFunc, rep.apool);
2011-03-26 14:10:41 +01:00
func('', '');
2020-11-23 19:24:19 +01:00
} else {
const offsetIntoLine = 0;
let filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser);
const lineNum = rep.lines.indexOfEntry(lineEntry);
const aline = rep.alines[lineNum];
2011-03-26 14:10:41 +01:00
filteredFunc = linestylefilter.getLineStyleFilter(
2020-11-23 19:24:19 +01:00
text.length, aline, filteredFunc, rep.apool);
2011-03-26 14:10:41 +01:00
filteredFunc(text, '');
}
}
2020-11-23 19:24:19 +01:00
let observedChanges;
2011-07-07 19:59:34 +02:00
function clearObservedChanges() {
2011-07-07 19:59:34 +02:00
observedChanges = {
2020-11-23 19:24:19 +01:00
cleanNodesNearChanges: {},
2011-07-07 19:59:34 +02:00
};
2011-03-26 14:10:41 +01:00
}
clearObservedChanges();
function getCleanNodeByKey(key) {
2020-11-23 19:24:19 +01:00
const p = PROFILER('getCleanNodeByKey', false);
2011-03-26 14:10:41 +01:00
p.extra = 0;
2020-11-23 19:24:19 +01:00
let n = doc.getElementById(key);
2011-03-26 14:10:41 +01:00
// copying and pasting can lead to duplicate ids
2020-11-23 19:24:19 +01:00
while (n && isNodeDirty(n)) {
2011-03-26 14:10:41 +01:00
p.extra++;
2020-11-23 19:24:19 +01:00
n.id = '';
2011-03-26 14:10:41 +01:00
n = doc.getElementById(key);
}
2020-11-23 19:24:19 +01:00
p.literal(p.extra, 'extra');
2011-03-26 14:10:41 +01:00
p.end();
return n;
}
function observeChangesAroundNode(node) {
2011-03-26 14:10:41 +01:00
// 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).
2020-11-23 19:24:19 +01:00
let cleanNode;
let hasAdjacentDirtyness;
if (!isNodeDirty(node)) {
2011-03-26 14:10:41 +01:00
cleanNode = node;
var prevSib = cleanNode.previousSibling;
var nextSib = cleanNode.nextSibling;
2011-07-07 19:59:34 +02:00
hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) || (nextSib && isNodeDirty(nextSib)));
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
// node is dirty, look for clean node above
2020-11-23 19:24:19 +01:00
let upNode = node.previousSibling;
while (upNode && isNodeDirty(upNode)) {
2011-07-07 19:59:34 +02:00
upNode = upNode.previousSibling;
}
2020-11-23 19:24:19 +01:00
if (upNode) {
2011-07-07 19:59:34 +02:00
cleanNode = upNode;
2020-11-23 19:24:19 +01:00
} else {
let downNode = node.nextSibling;
while (downNode && isNodeDirty(downNode)) {
2011-07-07 19:59:34 +02:00
downNode = downNode.nextSibling;
}
2020-11-23 19:24:19 +01:00
if (downNode) {
2011-07-07 19:59:34 +02:00
cleanNode = downNode;
}
}
2020-11-23 19:24:19 +01:00
if (!cleanNode) {
2011-07-07 19:59:34 +02:00
// Couldn't find any adjacent clean nodes!
// Since top and bottom of doc is dirty, the dirty area will be detected.
return;
2011-03-26 14:10:41 +01:00
}
hasAdjacentDirtyness = true;
}
2020-11-23 19:24:19 +01:00
if (hasAdjacentDirtyness) {
2011-03-26 14:10:41 +01:00
// previous or next line is dirty
2020-11-23 19:24:19 +01:00
observedChanges.cleanNodesNearChanges[`$${uniqueId(cleanNode)}`] = true;
} else {
2011-03-26 14:10:41 +01:00
// next and prev lines are clean (if they exist)
2020-11-23 19:24:19 +01:00
const lineKey = uniqueId(cleanNode);
2011-03-26 14:10:41 +01:00
var prevSib = cleanNode.previousSibling;
var nextSib = cleanNode.nextSibling;
2020-11-23 19:24:19 +01:00
const actualPrevKey = ((prevSib && uniqueId(prevSib)) || null);
const actualNextKey = ((nextSib && uniqueId(nextSib)) || null);
const repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey));
const repNextEntry = rep.lines.next(rep.lines.atKey(lineKey));
const repPrevKey = ((repPrevEntry && repPrevEntry.key) || null);
const repNextKey = ((repNextEntry && repNextEntry.key) || null);
if (actualPrevKey != repPrevKey || actualNextKey != repNextKey) {
observedChanges.cleanNodesNearChanges[`$${uniqueId(cleanNode)}`] = true;
2011-03-26 14:10:41 +01:00
}
}
}
function observeChangesAroundSelection() {
2011-03-26 14:10:41 +01:00
if (currentCallStack.observedSelection) return;
currentCallStack.observedSelection = true;
2020-11-23 19:24:19 +01:00
const p = PROFILER('getSelection', false);
const selection = getSelection();
2011-03-26 14:10:41 +01:00
p.end();
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
if (selection) {
const node1 = topLevel(selection.startPoint.node);
const node2 = topLevel(selection.endPoint.node);
2011-03-26 14:10:41 +01:00
if (node1) observeChangesAroundNode(node1);
2020-11-23 19:24:19 +01:00
if (node2 && node1 != node2) {
2011-07-07 19:59:34 +02:00
observeChangesAroundNode(node2);
2011-03-26 14:10:41 +01:00
}
}
}
function observeSuspiciousNodes() {
2011-03-26 14:10:41 +01:00
// inspired by Firefox bug #473255, where pasting formatted text
// causes the cursor to jump away, making the new HTML never found.
2020-11-23 19:24:19 +01:00
if (root.getElementsByTagName) {
const nds = root.getElementsByTagName('style');
for (let i = 0; i < nds.length; i++) {
const n = topLevel(nds[i]);
if (n && n.parentNode == root) {
2011-07-07 19:59:34 +02:00
observeChangesAroundNode(n);
}
2011-03-26 14:10:41 +01:00
}
}
}
function incorporateUserChanges() {
2011-03-26 14:10:41 +01:00
if (currentCallStack.domClean) return false;
currentCallStack.isUserChange = true;
2011-03-27 12:46:45 +02:00
if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const p = PROFILER('incorp', false);
2011-03-26 14:10:41 +01:00
// returns true if dom changes were made
2020-11-23 19:24:19 +01:00
if (!root.firstChild) {
root.innerHTML = '<div><!-- --></div>';
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
p.mark('obs');
2011-03-26 14:10:41 +01:00
observeChangesAroundSelection();
observeSuspiciousNodes();
2020-11-23 19:24:19 +01:00
p.mark('dirty');
let dirtyRanges = getDirtyRanges();
let dirtyRangesCheckOut = true;
let j = 0;
let a, b;
while (j < dirtyRanges.length) {
2011-03-26 14:10:41 +01:00
a = dirtyRanges[j][0];
b = dirtyRanges[j][1];
2020-11-23 19:24:19 +01:00
if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) {
2011-03-26 14:10:41 +01:00
dirtyRangesCheckOut = false;
break;
}
j++;
}
2020-11-23 19:24:19 +01:00
if (!dirtyRangesCheckOut) {
const numBodyNodes = root.childNodes.length;
for (var k = 0; k < numBodyNodes; k++) {
const bodyNode = root.childNodes.item(k);
if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) {
2011-03-26 14:10:41 +01:00
observeChangesAroundNode(bodyNode);
}
}
dirtyRanges = getDirtyRanges();
}
clearObservedChanges();
2020-11-23 19:24:19 +01:00
p.mark('getsel');
const selection = getSelection();
let selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection
let i = 0;
const splicesToDo = [];
let netNumLinesChangeSoFar = 0;
const toDeleteAtEnd = [];
p.mark('ranges');
p.literal(dirtyRanges.length, 'numdirt');
const domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]]
while (i < dirtyRanges.length) {
const range = dirtyRanges[i];
2011-03-26 14:10:41 +01:00
a = range[0];
b = range[1];
2020-11-23 19:24:19 +01:00
let firstDirtyNode = (((a === 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling);
2011-03-26 14:10:41 +01:00
firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode);
2020-11-23 19:24:19 +01:00
let lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling);
2011-03-26 14:10:41 +01:00
lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
2020-11-23 19:24:19 +01:00
if (firstDirtyNode && lastDirtyNode) {
const cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author);
2011-07-07 19:59:34 +02:00
cc.notifySelection(selection);
2020-11-23 19:24:19 +01:00
const dirtyNodes = [];
for (let n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode);
n = n.nextSibling) {
if (browser.msie) {
2011-03-26 14:10:41 +01:00
// try to undo IE's pesky and overzealous linkification
2020-11-23 19:24:19 +01:00
try {
n.createTextRange().execCommand('unlink', false, null);
} catch (e) {}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
cc.collectContent(n);
dirtyNodes.push(n);
}
cc.notifyNextNode(lastDirtyNode.nextSibling);
2020-11-23 19:24:19 +01:00
let lines = cc.getLines();
if ((lines.length <= 1 || lines[lines.length - 1] !== '') && lastDirtyNode.nextSibling) {
2011-07-07 19:59:34 +02:00
// 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.
b++;
2020-11-23 19:24:19 +01:00
const cleanLine = lastDirtyNode.nextSibling;
2011-07-07 19:59:34 +02:00
cc.collectContent(cleanLine);
toDeleteAtEnd.push(cleanLine);
cc.notifyNextNode(cleanLine.nextSibling);
}
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const ccData = cc.finish();
const ss = ccData.selStart;
const se = ccData.selEnd;
2011-03-26 14:10:41 +01:00
lines = ccData.lines;
2020-11-23 19:24:19 +01:00
const lineAttribs = ccData.lineAttribs;
const linesWrapped = ccData.linesWrapped;
var scrollToTheLeftNeeded = false;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
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
2013-02-17 22:19:15 +01:00
// Firefox isn't quite so bad, but it's still pretty quirky.
var scrollToTheLeftNeeded = true;
}
2011-07-07 19:59:34 +02:00
}
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
if (ss[0] >= 0) selStart = [ss[0] + a + netNumLinesChangeSoFar, ss[1]];
if (se[0] >= 0) selEnd = [se[0] + a + netNumLinesChangeSoFar, se[1]];
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const entries = [];
const nodeToAddAfter = lastDirtyNode;
const lineNodeInfos = new Array(lines.length);
for (var k = 0; k < lines.length; k++) {
const lineString = lines[k];
const newEntry = createDomLineEntry(lineString);
2011-07-07 19:59:34 +02:00
entries.push(newEntry);
lineNodeInfos[k] = newEntry.domInfo;
}
2020-11-23 19:24:19 +01:00
// var fragment = magicdom.wrapDom(document.createDocumentFragment());
2011-07-07 19:59:34 +02:00
domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]);
2020-11-23 19:24:19 +01:00
_.each(dirtyNodes, (n) => {
2011-07-07 19:59:34 +02:00
toDeleteAtEnd.push(n);
});
2020-11-23 19:24:19 +01:00
const spliceHints = {};
2011-07-07 19:59:34 +02:00
if (selStart) spliceHints.selStart = selStart;
if (selEnd) spliceHints.selEnd = selEnd;
splicesToDo.push([a + netNumLinesChangeSoFar, b - a, entries, lineAttribs, spliceHints]);
netNumLinesChangeSoFar += (lines.length - (b - a));
2020-11-23 19:24:19 +01:00
} else if (b > a) {
splicesToDo.push([a + netNumLinesChangeSoFar,
b - a,
[],
[]]);
2011-03-26 14:10:41 +01:00
}
i++;
}
2020-11-23 19:24:19 +01:00
const domChanges = (splicesToDo.length > 0);
2011-03-26 14:10:41 +01:00
// update the representation
2020-11-23 19:24:19 +01:00
p.mark('splice');
_.each(splicesToDo, (splice) => {
2011-03-26 14:10:41 +01:00
doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]);
});
// do DOM inserts
2020-11-23 19:24:19 +01:00
p.mark('insert');
_.each(domInsertsNeeded, (ins) => {
insertDomLines(ins[0], ins[1]);
2011-03-26 14:10:41 +01:00
});
2020-11-23 19:24:19 +01:00
p.mark('del');
2011-03-26 14:10:41 +01:00
// delete old dom nodes
2020-11-23 19:24:19 +01:00
_.each(toDeleteAtEnd, (n) => {
// var id = n.uniqueId();
2011-03-26 14:10:41 +01:00
// parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf)
2020-11-23 19:24:19 +01:00
if (n.parentNode) n.parentNode.removeChild(n);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
// dmesg(htmlPrettyEscape(htmlForRemovedChild(n)));
2011-03-26 14:10:41 +01:00
});
2020-11-23 19:24:19 +01:00
if (scrollToTheLeftNeeded) { // needed to stop chrome from breaking the ui when long strings without spaces are pasted
$('#innerdocbody').scrollLeft(0);
}
2020-11-23 19:24:19 +01:00
p.mark('findsel');
2011-03-26 14:10:41 +01:00
// if the nodes that define the selection weren't encountered during
// content collection, figure out where those nodes are now.
2020-11-23 19:24:19 +01:00
if (selection && !selStart) {
// if (domChanges) dmesg("selection not collected");
const selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', {
callstack: currentCallStack,
2020-11-23 19:24:19 +01:00
editorInfo,
rep,
root,
point: selection.startPoint,
documentAttributeManager,
2013-06-14 19:37:41 +02:00
});
2020-11-23 19:24:19 +01:00
selStart = (selStartFromHook == null || selStartFromHook.length == 0) ? getLineAndCharForPoint(selection.startPoint) : selStartFromHook;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (selection && !selEnd) {
const selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', {
callstack: currentCallStack,
2020-11-23 19:24:19 +01:00
editorInfo,
rep,
root,
point: selection.endPoint,
documentAttributeManager,
});
2020-11-23 19:24:19 +01:00
selEnd = (selEndFromHook == null || selEndFromHook.length == 0) ? getLineAndCharForPoint(selection.endPoint) : selEndFromHook;
2011-03-26 14:10:41 +01:00
}
// selection from content collection can, in various ways, extend past final
// BR in firefox DOM, so cap the line
2020-11-23 19:24:19 +01:00
const numLines = rep.lines.length();
if (selStart && selStart[0] >= numLines) {
2011-07-07 19:59:34 +02:00
selStart[0] = numLines - 1;
2011-03-26 14:10:41 +01:00
selStart[1] = rep.lines.atIndex(selStart[0]).text.length;
}
2020-11-23 19:24:19 +01:00
if (selEnd && selEnd[0] >= numLines) {
2011-07-07 19:59:34 +02:00
selEnd[0] = numLines - 1;
2011-03-26 14:10:41 +01:00
selEnd[1] = rep.lines.atIndex(selEnd[0]).text.length;
}
2020-11-23 19:24:19 +01:00
p.mark('repsel');
2011-03-26 14:10:41 +01:00
// 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
2013-06-14 19:37:41 +02:00
// idea.
2011-07-07 19:59:34 +02:00
if (selection) repSelectionChange(selStart, selEnd, selection && selection.focusAtStart);
2011-03-26 14:10:41 +01:00
// update browser selection
2020-11-23 19:24:19 +01:00
p.mark('browsel');
if (selection && (domChanges || isCaret())) {
2011-03-26 14:10:41 +01:00
// 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;
2020-11-23 19:24:19 +01:00
p.mark('fixview');
2011-03-26 14:10:41 +01:00
fixView();
2020-11-23 19:24:19 +01:00
p.end('END');
2011-03-26 14:10:41 +01:00
return domChanges;
}
2011-07-07 19:59:34 +02:00
var STYLE_ATTRIBS = {
bold: true,
italic: true,
underline: true,
strikethrough: true,
2020-11-23 19:24:19 +01:00
list: true,
2011-07-07 19:59:34 +02:00
};
2011-03-26 14:10:41 +01:00
function isStyleAttribute(aname) {
2011-07-07 19:59:34 +02:00
return !!STYLE_ATTRIBS[aname];
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function isDefaultLineAttribute(aname) {
return AttributeManager.DEFAULT_LINE_ATTRIBUTES.indexOf(aname) !== -1;
2011-03-26 14:10:41 +01:00
}
function insertDomLines(nodeToAddAfter, infoStructs) {
2020-11-23 19:24:19 +01:00
let lastEntry;
let lineStartOffset;
2011-03-26 14:10:41 +01:00
if (infoStructs.length < 1) return;
2020-11-23 19:24:19 +01:00
const startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node));
const endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node));
const charStart = rep.lines.offsetOfEntry(startEntry);
const charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width;
_.each(infoStructs, (info) => {
const p2 = PROFILER('insertLine', false);
const node = info.node;
const key = uniqueId(node);
let entry;
p2.mark('findEntry');
if (lastEntry) {
2011-07-07 19:59:34 +02:00
// optimization to avoid recalculation
2020-11-23 19:24:19 +01:00
const next = rep.lines.next(lastEntry);
if (next && next.key == key) {
2011-07-07 19:59:34 +02:00
entry = next;
lineStartOffset += lastEntry.width;
}
}
2020-11-23 19:24:19 +01:00
if (!entry) {
p2.literal(1, 'nonopt');
2011-07-07 19:59:34 +02:00
entry = rep.lines.atKey(key);
lineStartOffset = rep.lines.offsetOfKey(key);
2020-11-23 19:24:19 +01:00
} else { p2.literal(0, 'nonopt'); }
2011-03-26 14:10:41 +01:00
lastEntry = entry;
2020-11-23 19:24:19 +01:00
p2.mark('spans');
getSpansForLine(entry, (tokenText, tokenClass) => {
2011-07-07 19:59:34 +02:00
info.appendSpan(tokenText, tokenClass);
}, lineStartOffset);
2020-11-23 19:24:19 +01:00
p2.mark('addLine');
2011-03-26 14:10:41 +01:00
info.prepareForAdd();
entry.lineMarker = info.lineMarker;
2020-11-23 19:24:19 +01:00
if (!nodeToAddAfter) {
2011-07-07 19:59:34 +02:00
root.insertBefore(node, root.firstChild);
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
root.insertBefore(node, nodeToAddAfter.nextSibling);
2011-03-26 14:10:41 +01:00
}
nodeToAddAfter = node;
info.notifyAdded();
2020-11-23 19:24:19 +01:00
p2.mark('markClean');
2011-03-26 14:10:41 +01:00
markNodeClean(node);
p2.end();
});
}
function isCaret() {
2011-07-07 19:59:34 +02:00
return (rep.selStart && rep.selEnd && rep.selStart[0] == rep.selEnd[0] && rep.selStart[1] == rep.selEnd[1]);
2011-03-26 14:10:41 +01:00
}
editorInfo.ace_isCaret = isCaret;
// prereq: isCaret()
2011-07-07 19:59:34 +02:00
function caretLine() {
2011-07-07 19:59:34 +02:00
return rep.selStart[0];
}
editorInfo.ace_caretLine = caretLine;
2013-06-14 19:37:41 +02:00
function caretColumn() {
2011-07-07 19:59:34 +02:00
return rep.selStart[1];
}
editorInfo.ace_caretColumn = caretColumn;
2013-06-14 19:37:41 +02:00
function caretDocChar() {
2011-03-26 14:10:41 +01:00
return rep.lines.offsetOfIndex(caretLine()) + caretColumn();
}
editorInfo.ace_caretDocChar = caretDocChar;
2013-06-14 19:37:41 +02:00
function handleReturnIndentation() {
2011-03-26 14:10:41 +01:00
// on return, indent to level of previous line
2020-11-23 19:24:19 +01:00
if (isCaret() && caretColumn() === 0 && caretLine() > 0) {
const lineNum = caretLine();
const thisLine = rep.lines.atIndex(lineNum);
const prevLine = rep.lines.prev(thisLine);
const prevLineText = prevLine.text;
let theIndent = /^ *(?:)/.exec(prevLineText)[0];
const shouldIndent = parent.parent.clientVars.indentationOnNewLine;
if (shouldIndent && /[\[\(\:\{]\s*$/.exec(prevLineText)) {
theIndent += THE_TAB;
}
2020-11-23 19:24:19 +01:00
const cs = Changeset.builder(rep.lines.totalWidth()).keep(
rep.lines.offsetOfIndex(lineNum), lineNum).insert(
theIndent, [
['author', thisAuthor],
], rep.apool).toString();
2011-03-26 14:10:41 +01:00
performDocumentApplyChangeset(cs);
performSelectionChange([lineNum, theIndent.length], [lineNum, theIndent.length]);
}
}
function getPointForLineAndChar(lineAndChar) {
2020-11-23 19:24:19 +01:00
const line = lineAndChar[0];
let charsLeft = lineAndChar[1];
// Do not uncomment this in production it will break iFrames.
2020-11-23 19:24:19 +01:00
// top.console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key,
// getCleanNodeByKey(rep.lines.atIndex(line).key));
const lineEntry = rep.lines.atIndex(line);
2011-03-26 14:10:41 +01:00
charsLeft -= lineEntry.lineMarker;
2020-11-23 19:24:19 +01:00
if (charsLeft < 0) {
2011-03-26 14:10:41 +01:00
charsLeft = 0;
}
2020-11-23 19:24:19 +01:00
const lineNode = lineEntry.lineNode;
let n = lineNode;
let after = false;
if (charsLeft === 0) {
let index = 0;
2015-04-12 14:00:01 +02:00
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..
}
2020-11-23 19:24:19 +01:00
if (browser.msie && line == (rep.lines.length() - 1) && lineNode.childNodes.length === 0) {
2011-07-07 19:59:34 +02:00
// best to stay at end of last empty div in IE
index = 1;
}
return {
node: lineNode,
2020-11-23 19:24:19 +01:00
index,
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
}
2020-11-23 19:24:19 +01:00
while (!(n == lineNode && after)) {
if (after) {
if (n.nextSibling) {
2011-07-07 19:59:34 +02:00
n = n.nextSibling;
after = false;
2020-11-23 19:24:19 +01:00
} else { n = n.parentNode; }
} else if (isNodeText(n)) {
const len = n.nodeValue.length;
if (charsLeft <= len) {
return {
node: n,
index: charsLeft,
maxIndex: len,
};
2011-07-07 19:59:34 +02:00
}
2020-11-23 19:24:19 +01:00
charsLeft -= len;
after = true;
} else if (n.firstChild) { n = n.firstChild; } else { after = true; }
2011-07-07 19:59:34 +02:00
}
return {
node: lineNode,
index: 1,
2020-11-23 19:24:19 +01:00
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
}
function nodeText(n) {
2020-11-23 19:24:19 +01:00
if (browser.msie) {
return n.innerText;
2020-11-23 19:24:19 +01:00
} else {
return n.textContent || n.nodeValue || '';
2020-11-23 19:24:19 +01:00
}
2011-03-26 14:10:41 +01:00
}
function getLineAndCharForPoint(point) {
2011-03-26 14:10:41 +01:00
// 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.
2020-11-23 19:24:19 +01:00
if (point.node == root) {
if (point.index === 0) {
2011-07-07 19:59:34 +02:00
return [0, 0];
2020-11-23 19:24:19 +01:00
} else {
const N = rep.lines.length();
const ln = rep.lines.atIndex(N - 1);
2011-07-07 19:59:34 +02:00
return [N - 1, ln.text.length];
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
} else {
let n = point.node;
let col = 0;
2011-03-26 14:10:41 +01:00
// if this part fails, it probably means the selection node
// was dirty, and we didn't see it when collecting dirty nodes.
2020-11-23 19:24:19 +01:00
if (isNodeText(n)) {
2011-07-07 19:59:34 +02:00
col = point.index;
2020-11-23 19:24:19 +01:00
} else if (point.index > 0) {
2011-07-07 19:59:34 +02:00
col = nodeText(n).length;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
let parNode, prevSib;
while ((parNode = n.parentNode) != root) {
if ((prevSib = n.previousSibling)) {
2011-07-07 19:59:34 +02:00
n = prevSib;
col += nodeText(n).length;
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
n = parNode;
}
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (n.firstChild && isBlockElement(n.firstChild)) {
2011-03-26 14:10:41 +01:00
col += 1; // lineMarker
}
2020-11-23 19:24:19 +01:00
const lineEntry = rep.lines.atKey(n.id);
const lineNum = rep.lines.indexOfEntry(lineEntry);
2011-03-26 14:10:41 +01:00
return [lineNum, col];
}
}
2012-03-27 22:24:16 +02:00
editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint;
2011-03-26 14:10:41 +01:00
function createDomLineEntry(lineString) {
2020-11-23 19:24:19 +01:00
const info = doCreateDomLine(lineString.length > 0);
const newNode = info.node;
2011-07-07 19:59:34 +02:00
return {
key: uniqueId(newNode),
text: lineString,
lineNode: newNode,
domInfo: info,
2020-11-23 19:24:19 +01:00
lineMarker: 0,
2011-07-07 19:59:34 +02:00
};
2011-03-26 14:10:41 +01:00
}
function canApplyChangesetToDocument(changes) {
2011-03-26 14:10:41 +01:00
return Changeset.oldLen(changes) == rep.alltext.length;
}
function performDocumentApplyChangeset(changes, insertsAfterSelection) {
2011-03-26 14:10:41 +01:00
doRepApplyChangeset(changes, insertsAfterSelection);
2020-11-23 19:24:19 +01:00
let requiredSelectionSetting = null;
if (rep.selStart && rep.selEnd) {
const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
const result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
2011-03-26 14:10:41 +01:00
requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
}
2020-11-23 19:24:19 +01:00
const linesMutatee = {
splice(start, numRemoved, newLinesVA) {
const args = Array.prototype.slice.call(arguments, 2);
domAndRepSplice(start, numRemoved, _.map(args, (s) => s.slice(0, -1)));
2011-07-07 19:59:34 +02:00
},
2020-11-23 19:24:19 +01:00
get(i) {
return `${rep.lines.atIndex(i).text}\n`;
2011-07-07 19:59:34 +02:00
},
2020-11-23 19:24:19 +01:00
length() {
2011-07-07 19:59:34 +02:00
return rep.lines.length();
2011-03-26 14:10:41 +01:00
},
2020-11-23 19:24:19 +01:00
slice_notused(start, end) {
return _.map(rep.lines.slice(start, end), (e) => `${e.text}\n`);
},
2011-03-26 14:10:41 +01:00
};
Changeset.mutateTextLines(changes, linesMutatee);
2020-11-23 19:24:19 +01:00
if (requiredSelectionSetting) {
2011-07-07 19:59:34 +02:00
performSelectionChange(lineAndColumnFromChar(requiredSelectionSetting[0]), lineAndColumnFromChar(requiredSelectionSetting[1]), requiredSelectionSetting[2]);
2011-03-26 14:10:41 +01:00
}
function domAndRepSplice(startLine, deleteCount, newLineStrings) {
2020-11-23 19:24:19 +01:00
const keysToDelete = [];
if (deleteCount > 0) {
let entryToDelete = rep.lines.atIndex(startLine);
for (let i = 0; i < deleteCount; i++) {
2011-07-07 19:59:34 +02:00
keysToDelete.push(entryToDelete.key);
entryToDelete = rep.lines.next(entryToDelete);
}
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const lineEntries = _.map(newLineStrings, createDomLineEntry);
2011-03-26 14:10:41 +01:00
doRepLineSplice(startLine, deleteCount, lineEntries);
2020-11-23 19:24:19 +01:00
let nodeToAddAfter;
if (startLine > 0) {
2011-07-07 19:59:34 +02:00
nodeToAddAfter = getCleanNodeByKey(rep.lines.atIndex(startLine - 1).key);
2020-11-23 19:24:19 +01:00
} else { nodeToAddAfter = null; }
2011-03-26 14:10:41 +01:00
insertDomLines(nodeToAddAfter, _.map(lineEntries, (entry) => entry.domInfo));
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
_.each(keysToDelete, (k) => {
const n = doc.getElementById(k);
2011-07-07 19:59:34 +02:00
n.parentNode.removeChild(n);
2011-03-26 14:10:41 +01:00
});
2020-11-23 19:24:19 +01:00
if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine + deleteCount) || (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount)) {
2011-07-07 19:59:34 +02:00
currentCallStack.selectionAffected = true;
2011-03-26 14:10:41 +01:00
}
}
}
function doRepApplyChangeset(changes, insertsAfterSelection) {
2011-03-26 14:10:41 +01:00
Changeset.checkRep(changes);
2020-11-23 19:24:19 +01:00
if (Changeset.oldLen(changes) != rep.alltext.length) throw new Error(`doRepApplyChangeset length mismatch: ${Changeset.oldLen(changes)}/${rep.alltext.length}`);
2011-03-26 14:10:41 +01:00
(function doRecordUndoInformation(changes) {
2020-11-23 19:24:19 +01:00
const editEvent = currentCallStack.editEvent;
if (editEvent.eventType == 'nonundoable') {
if (!editEvent.changeset) {
2011-07-07 19:59:34 +02:00
editEvent.changeset = changes;
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
}
2020-11-23 19:24:19 +01:00
} else {
const inverseChangeset = Changeset.inverse(changes, {
get(i) {
return `${rep.lines.atIndex(i).text}\n`;
2011-07-07 19:59:34 +02:00
},
2020-11-23 19:24:19 +01:00
length() {
2011-07-07 19:59:34 +02:00
return rep.lines.length();
2020-11-23 19:24:19 +01:00
},
2011-07-07 19:59:34 +02:00
}, rep.alines, rep.apool);
2020-11-23 19:24:19 +01:00
if (!editEvent.backset) {
2011-07-07 19:59:34 +02:00
editEvent.backset = inverseChangeset;
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool);
}
2011-03-26 14:10:41 +01:00
}
})(changes);
2020-11-23 19:24:19 +01:00
// rep.alltext = Changeset.applyToText(changes, rep.alltext);
2011-03-26 14:10:41 +01:00
Changeset.mutateAttributionLines(changes, rep.alines, rep.apool);
2020-11-23 19:24:19 +01:00
if (changesetTracker.isTracking()) {
2011-03-26 14:10:41 +01:00
changesetTracker.composeUserChangeset(changes);
}
}
2012-04-05 15:25:17 +02:00
/*
Converts the position of a char (index in String) into a [row, col] tuple
*/
function lineAndColumnFromChar(x) {
2020-11-23 19:24:19 +01:00
const lineEntry = rep.lines.atOffset(x);
const lineStart = rep.lines.offsetOfEntry(lineEntry);
const lineNum = rep.lines.indexOfEntry(lineEntry);
2011-03-26 14:10:41 +01:00
return [lineNum, x - lineStart];
}
function performDocumentReplaceCharRange(startChar, endChar, newText) {
2020-11-23 19:24:19 +01:00
if (startChar == endChar && newText.length === 0) {
2011-03-26 14:10:41 +01:00
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.
2020-11-23 19:24:19 +01:00
if (endChar == rep.alltext.length) {
if (startChar == endChar) {
2011-07-07 19:59:34 +02:00
// an insert at end
startChar--;
endChar--;
2020-11-23 19:24:19 +01:00
newText = `\n${newText.substring(0, newText.length - 1)}`;
} else if (newText.length === 0) {
2011-07-07 19:59:34 +02:00
// a delete at end
startChar--;
endChar--;
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
// a replace at end
endChar--;
newText = newText.substring(0, newText.length - 1);
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
performDocumentReplaceRange(lineAndColumnFromChar(startChar), lineAndColumnFromChar(endChar), newText);
2011-03-26 14:10:41 +01:00
}
function performDocumentReplaceRange(start, end, newText) {
2012-02-19 14:52:24 +01:00
if (start === undefined) start = rep.selStart;
if (end === undefined) end = rep.selEnd;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
// dmesg(String([start.toSource(),end.toSource(),newText.toSource()]));
2011-03-26 14:10:41 +01:00
// start[0]: <--- start[1] --->CCCCCCCCCCC\n
// CCCCCCCCCCCCCCCCCCCC\n
// CCCC\n
// end[0]: <CCC end[1] CCC>-------\n
2020-11-23 19:24:19 +01:00
const builder = Changeset.builder(rep.lines.totalWidth());
2012-04-05 00:50:04 +02:00
ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
ChangesetUtils.buildRemoveRange(rep, builder, start, end);
2011-07-07 19:59:34 +02:00
builder.insert(newText, [
2020-11-23 19:24:19 +01:00
['author', thisAuthor],
2011-07-07 19:59:34 +02:00
], rep.apool);
2020-11-23 19:24:19 +01:00
const cs = builder.toString();
2011-03-26 14:10:41 +01:00
performDocumentApplyChangeset(cs);
}
function performDocumentApplyAttributesToCharRange(start, end, attribs) {
end = Math.min(end, rep.alltext.length - 1);
documentAttributeManager.setAttributesOnRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs);
2011-03-26 14:10:41 +01:00
}
editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange;
2013-06-14 19:37:41 +02:00
function setAttributeOnSelection(attributeName, attributeValue) {
2011-03-26 14:10:41 +01:00
if (!(rep.selStart && rep.selEnd)) return;
documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
2020-11-23 19:24:19 +01:00
[attributeName, attributeValue],
2011-07-07 19:59:34 +02:00
]);
2011-03-26 14:10:41 +01:00
}
editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection;
2020-11-23 19:24:19 +01:00
function getAttributeOnSelection(attributeName, prevChar) {
if (!(rep.selStart && rep.selEnd)) return;
const isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]);
if (isNotSelection) {
if (prevChar) {
2016-03-26 15:01:26 +01:00
// If it's not the start of the line
2020-11-23 19:24:19 +01:00
if (rep.selStart[1] !== 0) {
2016-03-26 15:01:26 +01:00
rep.selStart[1]--;
}
}
}
2020-11-23 19:24:19 +01:00
const withIt = Changeset.makeAttribsString('+', [
[attributeName, 'true'],
2013-11-28 18:27:52 +01:00
], rep.apool);
2020-11-23 19:24:19 +01:00
const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`);
function hasIt(attribs) {
2013-11-28 18:27:52 +01:00
return withItRegex.test(attribs);
}
2020-11-23 19:24:19 +01:00
return rangeHasAttrib(rep.selStart, rep.selEnd);
function rangeHasAttrib(selStart, selEnd) {
// if range is collapsed -> no attribs in range
2020-11-23 19:24:19 +01:00
if (selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false;
2020-11-23 19:24:19 +01:00
if (selStart[0] != selEnd[0]) { // -> More than one line selected
var hasAttrib = true;
// from selStart to the end of the first line
2020-11-23 19:24:19 +01:00
hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]);
// for all lines in between
2020-11-23 19:24:19 +01:00
for (let 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
2020-11-23 19:24:19 +01:00
hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]);
2020-11-23 19:24:19 +01:00
return hasAttrib;
}
// Logic tells us we now have a range on a single line
2020-11-23 19:24:19 +01:00
const lineNum = selStart[0];
const start = selStart[1];
const end = selEnd[1];
var hasAttrib = true;
// Iterate over attribs on this line
2020-11-23 19:24:19 +01:00
const opIter = Changeset.opIterator(rep.alines[lineNum]);
let indexIntoLine = 0;
while (opIter.hasNext()) {
2020-11-23 19:24:19 +01:00
const op = opIter.next();
const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) {
2013-11-28 18:27:52 +01:00
// 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
2013-11-28 18:27:52 +01:00
break;
}
}
indexIntoLine = opEndInLine;
}
2020-11-23 19:24:19 +01:00
return hasAttrib;
2013-11-28 18:27:52 +01:00
}
}
2013-11-28 18:27:52 +01:00
editorInfo.ace_getAttributeOnSelection = getAttributeOnSelection;
function toggleAttributeOnSelection(attributeName) {
2011-03-26 14:10:41 +01:00
if (!(rep.selStart && rep.selEnd)) return;
2020-11-23 19:24:19 +01:00
let selectionAllHasIt = true;
const withIt = Changeset.makeAttribsString('+', [
[attributeName, 'true'],
2011-07-07 19:59:34 +02:00
], rep.apool);
2020-11-23 19:24:19 +01:00
const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`);
2011-07-07 19:59:34 +02:00
function hasIt(attribs) {
2011-07-07 19:59:34 +02:00
return withItRegex.test(attribs);
}
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const selStartLine = rep.selStart[0];
const selEndLine = rep.selEnd[0];
for (let n = selStartLine; n <= selEndLine; n++) {
const opIter = Changeset.opIterator(rep.alines[n]);
let indexIntoLine = 0;
let selectionStartInLine = 0;
if (documentAttributeManager.lineHasMarker(n)) {
selectionStartInLine = 1; // ignore "*" used as line marker
}
2020-11-23 19:24:19 +01:00
let selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline
if (n == selStartLine) {
2011-07-07 19:59:34 +02:00
selectionStartInLine = rep.selStart[1];
}
2020-11-23 19:24:19 +01:00
if (n == selEndLine) {
2011-07-07 19:59:34 +02:00
selectionEndInLine = rep.selEnd[1];
}
2020-11-23 19:24:19 +01:00
while (opIter.hasNext()) {
const op = opIter.next();
const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) {
2011-07-07 19:59:34 +02:00
// does op overlap selection?
2020-11-23 19:24:19 +01:00
if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine)) {
2011-07-07 19:59:34 +02:00
selectionAllHasIt = false;
break;
}
}
indexIntoLine = opEndInLine;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (!selectionAllHasIt) {
2011-07-07 19:59:34 +02:00
break;
2011-03-26 14:10:41 +01:00
}
}
2020-11-23 19:24:19 +01:00
const attributeValue = selectionAllHasIt ? '' : 'true';
documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [[attributeName, attributeValue]]);
if (attribIsFormattingStyle(attributeName)) {
updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ...
2011-03-26 14:10:41 +01:00
}
}
editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection;
function performDocumentReplaceSelection(newText) {
2011-03-26 14:10:41 +01:00
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.
2011-07-07 19:59:34 +02:00
function doRepLineSplice(startLine, deleteCount, newLineEntries) {
2020-11-23 19:24:19 +01:00
_.each(newLineEntries, (entry) => {
2011-07-07 19:59:34 +02:00
entry.width = entry.text.length + 1;
});
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const startOldChar = rep.lines.offsetOfIndex(startLine);
const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const oldRegionStart = rep.lines.offsetOfIndex(startLine);
const oldRegionEnd = rep.lines.offsetOfIndex(startLine + deleteCount);
2011-03-26 14:10:41 +01:00
rep.lines.splice(startLine, deleteCount, newLineEntries);
currentCallStack.docTextChanged = true;
currentCallStack.repChanged = true;
2020-11-23 19:24:19 +01:00
const newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const newText = _.map(newLineEntries, (e) => `${e.text}\n`).join('');
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
rep.alltext = rep.alltext.substring(0, startOldChar) + newText + rep.alltext.substring(endOldChar, rep.alltext.length);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
// var newTotalLength = rep.alltext.length;
// rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart,
// newRegionEnd - oldRegionStart);
2011-03-26 14:10:41 +01:00
}
function doIncorpLineSplice(startLine, deleteCount, newLineEntries, lineAttribs, hints) {
2020-11-23 19:24:19 +01:00
const startOldChar = rep.lines.offsetOfIndex(startLine);
const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const oldRegionStart = rep.lines.offsetOfIndex(startLine);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let selStartHintChar, selEndHintChar;
if (hints && hints.selStart) {
2011-07-07 19:59:34 +02:00
selStartHintChar = rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - oldRegionStart;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (hints && hints.selEnd) {
2011-07-07 19:59:34 +02:00
selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const newText = _.map(newLineEntries, (e) => `${e.text}\n`).join('');
const oldText = rep.alltext.substring(startOldChar, endOldChar);
const oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join('');
const newAttribs = `${lineAttribs.join('|1+1')}|1+1`; // not valid in a changeset
const analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar);
const commonStart = analysis[0];
let commonEnd = analysis[1];
let shortOldText = oldText.substring(commonStart, oldText.length - commonEnd);
let shortNewText = newText.substring(commonStart, newText.length - commonEnd);
let spliceStart = startOldChar + commonStart;
let spliceEnd = endOldChar - commonEnd;
let shiftFinalNewlineToBeforeNewText = false;
2011-03-26 14:10:41 +01:00
// adjust the splice to not involve the final newline of the document;
// be very defensive
2020-11-23 19:24:19 +01:00
if (shortOldText.charAt(shortOldText.length - 1) == '\n' && shortNewText.charAt(shortNewText.length - 1) == '\n') {
2011-03-26 14:10:41 +01:00
// replacing text that ends in newline with text that also ends in newline
// (still, after analysis, somehow)
2011-07-07 19:59:34 +02:00
shortOldText = shortOldText.slice(0, -1);
shortNewText = shortNewText.slice(0, -1);
2011-03-26 14:10:41 +01:00
spliceEnd--;
commonEnd++;
}
2020-11-23 19:24:19 +01:00
if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0) {
2011-03-26 14:10:41 +01:00
// inserting after final newline, bad
spliceStart--;
spliceEnd--;
2020-11-23 19:24:19 +01:00
shortNewText = `\n${shortNewText.slice(0, -1)}`;
2011-03-26 14:10:41 +01:00
shiftFinalNewlineToBeforeNewText = true;
}
2020-11-23 19:24:19 +01:00
if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0) {
2011-03-26 14:10:41 +01:00
// deletion at end of rep.alltext
2020-11-23 19:24:19 +01:00
if (rep.alltext.charAt(spliceStart - 1) == '\n') {
2011-07-07 19:59:34 +02:00
// (if not then what the heck? it will definitely lead
// to a rep.alltext without a final newline)
spliceStart--;
spliceEnd--;
2011-03-26 14:10:41 +01:00
}
}
2020-11-23 19:24:19 +01:00
if (!(shortOldText.length === 0 && shortNewText.length === 0)) {
const oldDocText = rep.alltext;
const oldLen = oldDocText.length;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const spliceStartLine = rep.lines.indexOfOffset(spliceStart);
const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
const startBuilder = function () {
const builder = Changeset.builder(oldLen);
2011-07-07 19:59:34 +02:00
builder.keep(spliceStartLineStart, spliceStartLine);
builder.keep(spliceStart - spliceStartLineStart);
return builder;
2012-02-19 14:52:24 +01:00
};
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
const eachAttribRun = function (attribs, func /* (startInNewText, endInNewText, attribs)*/) {
const attribsIter = Changeset.opIterator(attribs);
let textIndex = 0;
const newTextStart = commonStart;
const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
while (attribsIter.hasNext()) {
const op = attribsIter.next();
const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
2011-07-07 19:59:34 +02:00
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
}
textIndex = nextIndex;
}
2012-02-19 14:52:24 +01:00
};
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const justApplyStyles = (shortNewText == shortOldText);
let theChangeset;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
if (justApplyStyles) {
2011-07-07 19:59:34 +02:00
// 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".
2020-11-23 19:24:19 +01:00
const incorpedAttribClearer = cachedStrFunc((oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => {
const k = rep.apool.getAttribKey(n);
if (isStyleAttribute(k)) {
return rep.apool.putAttrib([k, '']);
}
return false;
}));
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
const builder1 = startBuilder();
if (shiftFinalNewlineToBeforeNewText) {
2011-07-07 19:59:34 +02:00
builder1.keep(1, 1);
}
2020-11-23 19:24:19 +01:00
eachAttribRun(oldAttribs, (start, end, attribs) => {
2011-07-07 19:59:34 +02:00
builder1.keepText(newText.substring(start, end), incorpedAttribClearer(attribs));
});
2020-11-23 19:24:19 +01:00
const clearer = builder1.toString();
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
const builder2 = startBuilder();
if (shiftFinalNewlineToBeforeNewText) {
2011-07-07 19:59:34 +02:00
builder2.keep(1, 1);
}
2020-11-23 19:24:19 +01:00
eachAttribRun(newAttribs, (start, end, attribs) => {
2011-07-07 19:59:34 +02:00
builder2.keepText(newText.substring(start, end), attribs);
});
2020-11-23 19:24:19 +01:00
const styler = builder2.toString();
2011-07-07 19:59:34 +02:00
theChangeset = Changeset.compose(clearer, styler, rep.apool);
2020-11-23 19:24:19 +01:00
} else {
const builder = startBuilder();
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
const spliceEndLine = rep.lines.indexOfOffset(spliceEnd);
const spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine);
if (spliceEndLineStart > spliceStart) {
2011-07-07 19:59:34 +02:00
builder.remove(spliceEndLineStart - spliceStart, spliceEndLine - spliceStartLine);
builder.remove(spliceEnd - spliceEndLineStart);
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
builder.remove(spliceEnd - spliceStart);
}
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let isNewTextMultiauthor = false;
const authorAtt = Changeset.makeAttribsString('+', (thisAuthor ? [
['author', thisAuthor],
2011-07-07 19:59:34 +02:00
] : []), rep.apool);
2020-11-23 19:24:19 +01:00
const authorizer = cachedStrFunc((oldAtts) => {
if (isNewTextMultiauthor) {
2011-03-26 14:10:41 +01:00
// prefer colors from DOM
2011-07-07 19:59:34 +02:00
return Changeset.composeAttributes(authorAtt, oldAtts, true, rep.apool);
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
// use this author's color
2011-07-07 19:59:34 +02:00
return Changeset.composeAttributes(oldAtts, authorAtt, true, rep.apool);
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
});
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let foundDomAuthor = '';
eachAttribRun(newAttribs, (start, end, attribs) => {
const a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool);
if (a && a != foundDomAuthor) {
if (!foundDomAuthor) {
2011-03-26 14:10:41 +01:00
foundDomAuthor = a;
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
isNewTextMultiauthor = true; // multiple authors in DOM!
}
}
2011-07-07 19:59:34 +02:00
});
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
if (shiftFinalNewlineToBeforeNewText) {
2011-07-07 19:59:34 +02:00
builder.insert('\n', authorizer(''));
}
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
eachAttribRun(newAttribs, (start, end, attribs) => {
2011-07-07 19:59:34 +02:00
builder.insert(newText.substring(start, end), authorizer(attribs));
});
theChangeset = builder.toString();
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
// dmesg(htmlPrettyEscape(theChangeset));
2011-03-26 14:10:41 +01:00
doRepApplyChangeset(theChangeset);
}
// do this no matter what, because we need to get the right
// line keys into the rep.
doRepLineSplice(startLine, deleteCount, newLineEntries);
}
function cachedStrFunc(func) {
2020-11-23 19:24:19 +01:00
const cache = {};
return function (s) {
if (!cache[s]) {
2011-07-07 19:59:34 +02:00
cache[s] = func(s);
2011-03-26 14:10:41 +01:00
}
return cache[s];
};
}
function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint) {
// we need to take into account both the styles attributes & attributes defined by
// the plugins, so basically we can ignore only the default line attribs used by
// Etherpad
function incorpedAttribFilter(anum) {
return !isDefaultLineAttribute(rep.apool.getAttribKey(anum));
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function attribRuns(attribs) {
2020-11-23 19:24:19 +01:00
const lengs = [];
const atts = [];
const iter = Changeset.opIterator(attribs);
while (iter.hasNext()) {
const op = iter.next();
2011-07-07 19:59:34 +02:00
lengs.push(op.chars);
atts.push(op.attribs);
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
return [lengs, atts];
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function attribIterator(runs, backward) {
2020-11-23 19:24:19 +01:00
const lengs = runs[0];
const atts = runs[1];
let i = (backward ? lengs.length - 1 : 0);
let j = 0;
return function next() {
2020-11-23 19:24:19 +01:00
while (j >= lengs[i]) {
2011-07-07 19:59:34 +02:00
if (backward) i--;
else i++;
j = 0;
}
2020-11-23 19:24:19 +01:00
const a = atts[i];
2011-07-07 19:59:34 +02:00
j++;
return a;
2011-03-26 14:10:41 +01:00
};
}
2020-11-23 19:24:19 +01:00
const oldLen = oldText.length;
const newLen = newText.length;
const minLen = Math.min(oldLen, newLen);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter));
const newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter));
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let commonStart = 0;
const oldStartIter = attribIterator(oldARuns, false);
const newStartIter = attribIterator(newARuns, false);
while (commonStart < minLen) {
if (oldText.charAt(commonStart) == newText.charAt(commonStart) && oldStartIter() == newStartIter()) {
2011-07-07 19:59:34 +02:00
commonStart++;
2020-11-23 19:24:19 +01:00
} else { break; }
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
let commonEnd = 0;
const oldEndIter = attribIterator(oldARuns, true);
const newEndIter = attribIterator(newARuns, true);
while (commonEnd < minLen) {
if (commonEnd === 0) {
2011-07-07 19:59:34 +02:00
// assume newline in common
oldEndIter();
newEndIter();
commonEnd++;
2020-11-23 19:24:19 +01:00
} else if (oldText.charAt(oldLen - 1 - commonEnd) == newText.charAt(newLen - 1 - commonEnd) && oldEndIter() == newEndIter()) {
2011-07-07 19:59:34 +02:00
commonEnd++;
2020-11-23 19:24:19 +01:00
} else { break; }
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
let hintedCommonEnd = -1;
if ((typeof optSelEndHint) === 'number') {
2011-03-26 14:10:41 +01:00
hintedCommonEnd = newLen - optSelEndHint;
}
2020-11-23 19:24:19 +01:00
if (commonStart + commonEnd > oldLen) {
2011-03-26 14:10:41 +01:00
// ambiguous insertion
var minCommonEnd = oldLen - commonStart;
var maxCommonEnd = commonEnd;
2020-11-23 19:24:19 +01:00
if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) {
2011-07-07 19:59:34 +02:00
commonEnd = hintedCommonEnd;
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
commonEnd = minCommonEnd;
2011-03-26 14:10:41 +01:00
}
commonStart = oldLen - commonEnd;
}
2020-11-23 19:24:19 +01:00
if (commonStart + commonEnd > newLen) {
2011-03-26 14:10:41 +01:00
// ambiguous deletion
var minCommonEnd = newLen - commonStart;
var maxCommonEnd = commonEnd;
2020-11-23 19:24:19 +01:00
if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) {
2011-07-07 19:59:34 +02:00
commonEnd = hintedCommonEnd;
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
commonEnd = minCommonEnd;
2011-03-26 14:10:41 +01:00
}
commonStart = newLen - commonEnd;
}
return [commonStart, commonEnd];
}
function equalLineAndChars(a, b) {
2011-03-26 14:10:41 +01:00
if (!a) return !b;
if (!b) return !a;
return (a[0] == b[0] && a[1] == b[1]);
}
function performSelectionChange(selectStart, selectEnd, focusAtStart) {
2020-11-23 19:24:19 +01:00
if (repSelectionChange(selectStart, selectEnd, focusAtStart)) {
2011-03-26 14:10:41 +01:00
currentCallStack.selectionAffected = true;
}
}
editorInfo.ace_performSelectionChange = performSelectionChange;
2011-03-26 14:10:41 +01:00
// Change the abstract representation of the document to have a different selection.
// Should not rely on the line representation. Should not affect the DOM.
2011-07-07 19:59:34 +02:00
function repSelectionChange(selectStart, selectEnd, focusAtStart) {
2020-11-23 19:24:19 +01:00
focusAtStart = !!focusAtStart;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const newSelFocusAtStart = (focusAtStart && ((!selectStart) || (!selectEnd) || (selectStart[0] != selectEnd[0]) || (selectStart[1] != selectEnd[1])));
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
if ((!equalLineAndChars(rep.selStart, selectStart)) || (!equalLineAndChars(rep.selEnd, selectEnd)) || (rep.selFocusAtStart != newSelFocusAtStart)) {
2011-03-26 14:10:41 +01:00
rep.selStart = selectStart;
rep.selEnd = selectEnd;
rep.selFocusAtStart = newSelFocusAtStart;
currentCallStack.repChanged = true;
// select the formatting buttons when there is the style applied on selection
selectFormattingButtonIfLineHasStyleApplied(rep);
hooks.callAll('aceSelectionChanged', {
2020-11-23 19:24:19 +01:00
rep,
callstack: currentCallStack,
2020-11-23 19:24:19 +01:00
documentAttributeManager,
});
// we scroll when user places the caret at the last line of the pad
// when this settings is enabled
2020-11-23 19:24:19 +01:00
const docTextChanged = currentCallStack.docTextChanged;
if (!docTextChanged) {
const isScrollableEvent = !isPadLoading(currentCallStack.type) && isScrollableEditEvent(currentCallStack.type);
const innerHeight = getInnerHeight();
scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight);
}
2011-03-26 14:10:41 +01:00
return true;
// Do not uncomment this in production it will break iFrames.
2020-11-23 19:24:19 +01:00
// top.console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd,
// String(!!rep.selFocusAtStart));
2011-03-26 14:10:41 +01:00
}
return false;
// Do not uncomment this in production it will break iFrames.
2020-11-23 19:24:19 +01:00
// top.console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart);
2011-03-26 14:10:41 +01:00
}
function isPadLoading(eventType) {
return (eventType === 'setup') || (eventType === 'setBaseText') || (eventType === 'importText');
}
function updateStyleButtonState(attribName, hasStyleOnRepSelection) {
2020-11-23 19:24:19 +01:00
const $formattingButton = parent.parent.$(`[data-key="${attribName}"]`).find('a');
$formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection);
}
function attribIsFormattingStyle(attributeName) {
return _.contains(FORMATTING_STYLES, attributeName);
}
2020-11-23 19:24:19 +01:00
function selectFormattingButtonIfLineHasStyleApplied(rep) {
_.each(FORMATTING_STYLES, (style) => {
const hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style);
updateStyleButtonState(style, hasStyleOnRepSelection);
2020-11-23 19:24:19 +01:00
});
}
function doCreateDomLine(nonEmpty) {
2020-11-23 19:24:19 +01:00
if (browser.msie && (!nonEmpty)) {
const result = {
2011-07-07 19:59:34 +02:00
node: null,
appendSpan: noop,
prepareForAdd: noop,
notifyAdded: noop,
clearSpans: noop,
finishUpdate: noop,
2020-11-23 19:24:19 +01:00
lineMarker: 0,
2011-07-07 19:59:34 +02:00
};
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const lineElem = doc.createElement('div');
2011-03-26 14:10:41 +01:00
result.node = lineElem;
2020-11-23 19:24:19 +01:00
result.notifyAdded = function () {
2011-07-07 19:59:34 +02:00
// 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.
2020-11-23 19:24:19 +01:00
lineElem.innerHTML = ' '; // Frist we set a value that isnt blank
2011-07-07 19:59:34 +02:00
// a primitive-valued property survives copy-and-paste
2020-11-23 19:24:19 +01:00
setAssoc(lineElem, 'shouldBeEmpty', true);
2011-07-07 19:59:34 +02:00
// an object property doesn't
2020-11-23 19:24:19 +01:00
setAssoc(lineElem, 'unpasted', {});
lineElem.innerHTML = ''; // Then we make it blank.. New line and no space = Awesome :)
2011-03-26 14:10:41 +01:00
};
2020-11-23 19:24:19 +01:00
let lineClass = 'ace-line';
result.appendSpan = function (txt, cls) {
if ((!txt) && cls) {
2011-07-07 19:59:34 +02:00
// 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
2011-03-26 14:10:41 +01:00
};
2020-11-23 19:24:19 +01:00
result.clearSpans = function () {
2011-07-07 19:59:34 +02:00
lineClass = ''; // non-null to cause update
2011-03-26 14:10:41 +01:00
};
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
const writeClass = function () {
2011-07-07 19:59:34 +02:00
if (lineClass !== null) lineElem.className = lineClass;
2012-02-19 14:52:24 +01:00
};
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
result.prepareForAdd = writeClass;
result.finishUpdate = writeClass;
2020-11-23 19:24:19 +01:00
result.getInnerHTML = function () {
return '';
2011-07-07 19:59:34 +02:00
};
2011-03-26 14:10:41 +01:00
return result;
2020-11-23 19:24:19 +01:00
} else {
2015-01-21 16:01:39 +01:00
return domline.createDomLine(nonEmpty, doesWrap, browser, doc);
2011-03-26 14:10:41 +01:00
}
}
function textify(str) {
2011-03-26 14:10:41 +01:00
return str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' ');
}
2020-11-23 19:24:19 +01:00
const _blockElems = {
div: 1,
p: 1,
pre: 1,
li: 1,
ol: 1,
ul: 1,
2011-07-07 19:59:34 +02:00
};
2020-11-23 19:24:19 +01:00
_.each(hooks.callAll('aceRegisterBlockElements'), (element) => {
_blockElems[element] = 1;
2012-04-07 02:13:26 +02:00
});
function isBlockElement(n) {
2020-11-23 19:24:19 +01:00
return !!_blockElems[(n.tagName || '').toLowerCase()];
2011-03-26 14:10:41 +01:00
}
function getDirtyRanges() {
2011-03-26 14:10:41 +01:00
// 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.
2020-11-23 19:24:19 +01:00
const p = PROFILER('getDirtyRanges', false);
2011-03-26 14:10:41 +01:00
p.forIndices = 0;
p.consecutives = 0;
p.corrections = 0;
2020-11-23 19:24:19 +01:00
const cleanNodeForIndexCache = {};
const N = rep.lines.length(); // old number of lines
2011-07-07 19:59:34 +02:00
function cleanNodeForIndex(i) {
2011-03-26 14:10:41 +01:00
// 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.
2020-11-23 19:24:19 +01:00
if (cleanNodeForIndexCache[i] === undefined) {
2011-07-07 19:59:34 +02:00
p.forIndices++;
2020-11-23 19:24:19 +01:00
let result;
if (i < 0 || i >= N) {
2011-07-07 19:59:34 +02:00
result = true; // truthy, but no actual node
2020-11-23 19:24:19 +01:00
} else {
const key = rep.lines.atIndex(i).key;
2011-07-07 19:59:34 +02:00
result = (getCleanNodeByKey(key) || false);
}
cleanNodeForIndexCache[i] = result;
2011-03-26 14:10:41 +01:00
}
return cleanNodeForIndexCache[i];
}
2020-11-23 19:24:19 +01:00
const isConsecutiveCache = {};
2011-07-07 19:59:34 +02:00
function isConsecutive(i) {
2020-11-23 19:24:19 +01:00
if (isConsecutiveCache[i] === undefined) {
2011-07-07 19:59:34 +02:00
p.consecutives++;
2020-11-23 19:24:19 +01:00
isConsecutiveCache[i] = (function () {
2011-07-07 19:59:34 +02:00
// 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
2020-11-23 19:24:19 +01:00
const a = cleanNodeForIndex(i - 1);
const b = cleanNodeForIndex(i);
2011-07-07 19:59:34 +02:00
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;
})();
2011-03-26 14:10:41 +01:00
}
return isConsecutiveCache[i];
}
2011-07-07 19:59:34 +02:00
function isClean(i) {
2011-03-26 14:10:41 +01:00
// returns whether line (i) in the un-updated representation maps to a clean node,
// or is outside the bounds of the document
2011-07-07 19:59:34 +02:00
return !!cleanNodeForIndex(i);
2011-03-26 14:10:41 +01:00
}
// 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.
2020-11-23 19:24:19 +01:00
const cleanRanges = [
[-1, N + 1],
2011-07-07 19:59:34 +02:00
];
function rangeForLine(i) {
2011-03-26 14:10:41 +01:00
// returns index of cleanRange containing i, or -1 if none
2020-11-23 19:24:19 +01:00
let answer = -1;
_.each(cleanRanges, (r, idx) => {
2011-07-07 19:59:34 +02:00
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
2011-03-26 14:10:41 +01:00
});
return answer;
}
2011-07-07 19:59:34 +02:00
function removeLineFromRange(rng, line) {
2011-03-26 14:10:41 +01:00
// rng is index into cleanRanges, line is line number
// precond: line is in rng
2020-11-23 19:24:19 +01:00
const a = cleanRanges[rng][0];
const b = cleanRanges[rng][1];
2011-07-07 19:59:34 +02:00
if ((a + 1) == b) cleanRanges.splice(rng, 1);
2011-03-26 14:10:41 +01:00
else if (line == a) cleanRanges[rng][0]++;
2011-07-07 19:59:34 +02:00
else if (line == (b - 1)) cleanRanges[rng][1]--;
else cleanRanges.splice(rng, 1, [a, line], [line + 1, b]);
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function splitRange(rng, pt) {
2011-03-26 14:10:41 +01:00
// precond: pt splits cleanRanges[rng] into two non-empty ranges
2020-11-23 19:24:19 +01:00
const a = cleanRanges[rng][0];
const b = cleanRanges[rng][1];
2011-07-07 19:59:34 +02:00
cleanRanges.splice(rng, 1, [a, pt], [pt, b]);
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const correctedLines = {};
2011-07-07 19:59:34 +02:00
function correctlyAssignLine(line) {
2011-03-26 14:10:41 +01:00
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).
2020-11-23 19:24:19 +01:00
const rng = rangeForLine(line);
const lineClean = isClean(line);
if (rng < 0) {
if (lineClean) {
// somehow lost clean line
2011-07-07 19:59:34 +02:00
}
return true;
}
2020-11-23 19:24:19 +01:00
if (!lineClean) {
2011-07-07 19:59:34 +02:00
// a clean-range includes this dirty line, fix it
removeLineFromRange(rng, line);
return false;
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
// line is clean, but could be wrongly connected to a clean line
// above or below
2020-11-23 19:24:19 +01:00
const a = cleanRanges[rng][0];
const b = cleanRanges[rng][1];
let didSomething = false;
2011-07-07 19:59:34 +02:00
// 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.
2020-11-23 19:24:19 +01:00
if (a < line && isClean(line - 1) && !isConsecutive(line)) {
2011-07-07 19:59:34 +02:00
splitRange(rng, line);
didSomething = true;
}
2020-11-23 19:24:19 +01:00
if (b > (line + 1) && isClean(line + 1) && !isConsecutive(line + 1)) {
2011-07-07 19:59:34 +02:00
splitRange(rng, line + 1);
didSomething = true;
}
return !didSomething;
}
}
function detectChangesAroundLine(line, reqInARow) {
2011-03-26 14:10:41 +01:00
// 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.
2020-11-23 19:24:19 +01:00
let correctInARow = 0;
let currentIndex = line;
while (correctInARow < reqInARow && currentIndex >= 0) {
if (correctlyAssignLine(currentIndex)) {
2011-07-07 19:59:34 +02:00
correctInARow++;
2020-11-23 19:24:19 +01:00
} else { correctInARow = 0; }
2011-07-07 19:59:34 +02:00
currentIndex--;
2011-03-26 14:10:41 +01:00
}
correctInARow = 0;
currentIndex = line;
2020-11-23 19:24:19 +01:00
while (correctInARow < reqInARow && currentIndex < N) {
if (correctlyAssignLine(currentIndex)) {
2011-07-07 19:59:34 +02:00
correctInARow++;
2020-11-23 19:24:19 +01:00
} else { correctInARow = 0; }
2011-07-07 19:59:34 +02:00
currentIndex++;
2011-03-26 14:10:41 +01:00
}
}
2020-11-23 19:24:19 +01:00
if (N === 0) {
2011-03-26 14:10:41 +01:00
p.cancel();
2020-11-23 19:24:19 +01:00
if (!isConsecutive(0)) {
2011-07-07 19:59:34 +02:00
splitRange(0, 0);
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
} else {
p.mark('topbot');
2011-07-07 19:59:34 +02:00
detectChangesAroundLine(0, 1);
detectChangesAroundLine(N - 1, 1);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
p.mark('obs');
for (const k in observedChanges.cleanNodesNearChanges) {
const key = k.substring(1);
if (rep.lines.containsKey(key)) {
const line = rep.lines.indexOfKey(key);
2011-07-07 19:59:34 +02:00
detectChangesAroundLine(line, 2);
}
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
p.mark('stats&calc');
p.literal(p.forIndices, 'byidx');
p.literal(p.consecutives, 'cons');
p.literal(p.corrections, 'corr');
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const dirtyRanges = [];
for (let r = 0; r < cleanRanges.length - 1; r++) {
2011-07-07 19:59:34 +02:00
dirtyRanges.push([cleanRanges[r][1], cleanRanges[r + 1][0]]);
2011-03-26 14:10:41 +01:00
}
p.end();
return dirtyRanges;
}
function markNodeClean(n) {
2011-03-26 14:10:41 +01:00
// clean nodes have knownHTML that matches their innerHTML
2020-11-23 19:24:19 +01:00
const dirtiness = {};
2011-03-26 14:10:41 +01:00
dirtiness.nodeId = uniqueId(n);
dirtiness.knownHTML = n.innerHTML;
2020-11-23 19:24:19 +01:00
if (browser.msie) {
2011-03-26 14:10:41 +01:00
// 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;
}
2020-11-23 19:24:19 +01:00
setAssoc(n, 'dirtiness', dirtiness);
2011-03-26 14:10:41 +01:00
}
function isNodeDirty(n) {
2020-11-23 19:24:19 +01:00
const p = PROFILER('cleanCheck', false);
2011-03-26 14:10:41 +01:00
if (n.parentNode != root) return true;
2020-11-23 19:24:19 +01:00
const data = getAssoc(n, 'dirtiness');
2011-03-26 14:10:41 +01:00
if (!data) return true;
if (n.id !== data.nodeId) return true;
2020-11-23 19:24:19 +01:00
if (browser.msie) {
2011-03-26 14:10:41 +01:00
if (n.innerText !== data.knownText) return true;
}
if (n.innerHTML !== data.knownHTML) return true;
p.end();
return false;
}
function getViewPortTopBottom() {
2020-11-23 19:24:19 +01:00
const theTop = scroll.getScrollY();
const doc = outerWin.document;
const 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)
2020-11-23 19:24:19 +01:00
const viewportExtraSpacesAndPosition = getEditorPositionTop() + getPaddingTopAddedWhenPageViewIsEnable();
2011-07-07 19:59:34 +02:00
return {
top: theTop,
2020-11-23 19:24:19 +01:00
bottom: (theTop + height - viewportExtraSpacesAndPosition),
2011-07-07 19:59:34 +02:00
};
2011-03-26 14:10:41 +01:00
}
function getEditorPositionTop() {
2020-11-23 19:24:19 +01:00
const editor = parent.document.getElementsByTagName('iframe');
const editorPositionTop = editor[0].offsetTop;
return editorPositionTop;
2011-03-26 14:10:41 +01:00
}
// ep_page_view adds padding-top, which makes the viewport smaller
function getPaddingTopAddedWhenPageViewIsEnable() {
2020-11-23 19:24:19 +01:00
const rootDocument = parent.parent.document;
const aceOuter = rootDocument.getElementsByName('ace_outer');
const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top'));
return aceOuterPaddingTop;
2011-03-26 14:10:41 +01:00
}
function handleCut(evt) {
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary('handleCut', () => {
doDeleteKey(evt);
});
2015-03-13 06:01:18 +01:00
return true;
}
function handleClick(evt) {
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary('handleClick', () => {
2011-03-26 14:10:41 +01:00
idleWorkTimer.atMost(200);
});
function isLink(n) {
2020-11-23 19:24:19 +01:00
return (n.tagName || '').toLowerCase() == 'a' && n.href;
2012-02-19 14:52:24 +01:00
}
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
// only want to catch left-click
2020-11-23 19:24:19 +01:00
if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3)) {
2011-03-26 14:10:41 +01:00
// find A tag with HREF
2020-11-23 19:24:19 +01:00
let n = evt.target;
while (n && n.parentNode && !isLink(n)) {
2011-07-07 19:59:34 +02:00
n = n.parentNode;
}
2020-11-23 19:24:19 +01:00
if (n && isLink(n)) {
try {
window.open(n.href, '_blank', 'noopener,noreferrer');
2020-11-23 19:24:19 +01:00
} catch (e) {
2011-03-26 14:10:41 +01:00
// absorb "user canceled" error in IE for certain prompts
}
2011-07-07 19:59:34 +02:00
evt.preventDefault();
2011-03-26 14:10:41 +01:00
}
}
hideEditBarDropdowns();
}
function hideEditBarDropdowns() {
2020-11-23 19:24:19 +01:00
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');
2012-02-26 13:38:52 +01:00
}
2011-03-26 14:10:41 +01:00
}
function doReturnKey() {
2020-11-23 19:24:19 +01:00
if (!(rep.selStart && rep.selEnd)) {
2011-03-26 14:10:41 +01:00
return;
}
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
const lineNum = rep.selStart[0];
let listType = getLineListType(lineNum);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
if (listType) {
const text = rep.lines.atIndex(lineNum).text;
listType = /([a-z]+)([0-9]+)/.exec(listType);
2020-11-23 19:24:19 +01:00
const type = listType[1];
const 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
2012-01-15 18:20:20 +01:00
}
2020-11-23 19:24:19 +01:00
} else if (lineNum + 1 <= rep.lines.length()) {
2012-01-15 18:20:20 +01:00
performDocumentReplaceSelection('\n');
2020-11-23 19:24:19 +01:00
setLineListType(lineNum + 1, type + level);
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
} else {
2012-01-15 18:20:20 +01:00
performDocumentReplaceSelection('\n');
2011-03-26 14:10:41 +01:00
handleReturnIndentation();
}
}
function doIndentOutdent(isOut) {
2013-02-18 02:40:34 +01:00
if (!((rep.selStart && rep.selEnd) ||
2020-11-23 19:24:19 +01:00
((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) && rep.selEnd[1] > 1)) &&
2013-02-18 02:40:34 +01:00
(isOut != true)
2020-11-23 19:24:19 +01:00
) {
2011-03-26 14:10:41 +01:00
return false;
}
2020-11-23 19:24:19 +01:00
let firstLine, lastLine;
2011-03-26 14:10:41 +01:00
firstLine = rep.selStart[0];
2012-02-19 14:52:24 +01:00
lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0));
2020-11-23 19:24:19 +01:00
const mods = [];
for (let n = firstLine; n <= lastLine; n++) {
let listType = getLineListType(n);
let t = 'indent';
let level = 0;
if (listType) {
listType = /([a-z]+)([0-9]+)/.exec(listType);
2020-11-23 19:24:19 +01:00
if (listType) {
t = listType[1];
level = Number(listType[2]);
2011-03-26 14:10:41 +01:00
}
}
2020-11-23 19:24:19 +01:00
const newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1)));
if (level != newLevel) {
mods.push([n, (newLevel > 0) ? t + newLevel : '']);
}
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
_.each(mods, (mod) => {
2012-04-05 01:07:47 +02:00
setLineListType(mod[0], mod[1]);
2012-04-05 00:50:04 +02:00
});
return true;
2011-03-26 14:10:41 +01:00
}
editorInfo.ace_doIndentOutdent = doIndentOutdent;
function doTabKey(shiftDown) {
2020-11-23 19:24:19 +01:00
if (!doIndentOutdent(shiftDown)) {
2011-03-26 14:10:41 +01:00
performDocumentReplaceSelection(THE_TAB);
}
}
function doDeleteKey(optEvt) {
2020-11-23 19:24:19 +01:00
const evt = optEvt || {};
let handled = false;
if (rep.selStart) {
if (isCaret()) {
const lineNum = caretLine();
const col = caretColumn();
2011-03-26 14:10:41 +01:00
var lineEntry = rep.lines.atIndex(lineNum);
2020-11-23 19:24:19 +01:00
const lineText = lineEntry.text;
const lineMarker = lineEntry.lineMarker;
if (/^ +$/.exec(lineText.substring(lineMarker, col))) {
const col2 = col - lineMarker;
const tabSize = THE_TAB.length;
const toDelete = ((col2 - 1) % tabSize) + 1;
2011-07-07 19:59:34 +02:00
performDocumentReplaceRange([lineNum, col - toDelete], [lineNum, col], '');
2020-11-23 19:24:19 +01:00
// scrollSelectionIntoView();
2011-07-07 19:59:34 +02:00
handled = true;
}
}
2020-11-23 19:24:19 +01:00
if (!handled) {
if (isCaret()) {
const theLine = caretLine();
2011-03-26 14:10:41 +01:00
var lineEntry = rep.lines.atIndex(theLine);
2020-11-23 19:24:19 +01:00
if (caretColumn() <= lineEntry.lineMarker) {
2011-03-26 14:10:41 +01:00
// delete at beginning of line
2020-11-23 19:24:19 +01:00
const action = 'delete_newline';
const prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : '');
const thisLineListType = getLineListType(theLine);
const prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1));
const prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker);
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
const thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine);
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
if (thisLineListType) {
2011-03-26 14:10:41 +01:00
// this line is a list
2020-11-23 19:24:19 +01:00
if (prevLineBlank && !prevLineListType) {
2011-07-07 19:59:34 +02:00
// previous line is blank, remove it
performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], '');
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
// delistify
2011-07-07 19:59:34 +02:00
performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], '');
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
} else if (thisLineHasMarker && prevLineEntry) {
// If the line has any attributes assigned, remove them by removing the marker '*'
2020-11-23 19:24:19 +01:00
performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, lineEntry.lineMarker], '');
} else if (theLine > 0) {
2011-03-26 14:10:41 +01:00
// remove newline
2011-07-07 19:59:34 +02:00
performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], '');
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
} else {
const docChar = caretDocChar();
if (docChar > 0) {
if (evt.metaKey || evt.ctrlKey || evt.altKey) {
2011-07-07 19:59:34 +02:00
// 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.
2020-11-23 19:24:19 +01:00
let deleteBackTo = docChar - 1;
while (deleteBackTo > lineEntry.lineMarker && isWordChar(rep.alltext.charAt(deleteBackTo - 1))) {
2011-07-07 19:59:34 +02:00
deleteBackTo--;
}
performDocumentReplaceCharRange(deleteBackTo, docChar, '');
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
// normal delete
performDocumentReplaceCharRange(docChar - 1, docChar, '');
}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
}
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
performDocumentReplaceSelection('');
}
2011-03-26 14:10:41 +01:00
}
}
2020-11-23 19:24:19 +01:00
// 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.
const line = caretLine();
if (line != -1 && renumberList(line + 1) === null) {
2012-01-15 18:20:20 +01:00
renumberList(line);
}
2011-03-26 14:10:41 +01:00
}
// set of "letter or digit" chars is based on section 20.5.16 of the original Java Language Spec
2020-11-23 19:24:19 +01:00
const 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]/;
const REGEX_SPACE = /\s/;
2011-03-26 14:10:41 +01:00
function isWordChar(c) {
2011-07-07 19:59:34 +02:00
return !!REGEX_WORDCHAR.exec(c);
2011-03-26 14:10:41 +01:00
}
editorInfo.ace_isWordChar = isWordChar;
2013-06-14 19:37:41 +02:00
function isSpaceChar(c) {
2011-07-07 19:59:34 +02:00
return !!REGEX_SPACE.exec(c);
2011-03-26 14:10:41 +01:00
}
function moveByWordInLine(lineText, initialIndex, forwardNotBack) {
2020-11-23 19:24:19 +01:00
let i = initialIndex;
2011-07-07 19:59:34 +02:00
function nextChar() {
2011-03-26 14:10:41 +01:00
if (forwardNotBack) return lineText.charAt(i);
2011-07-07 19:59:34 +02:00
else return lineText.charAt(i - 1);
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function advance() {
2011-07-07 19:59:34 +02:00
if (forwardNotBack) i++;
else i--;
}
function isDone() {
2011-03-26 14:10:41 +01:00
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).
2020-11-23 19:24:19 +01:00
if (browser.msie && forwardNotBack) {
while ((!isDone()) && isWordChar(nextChar())) {
2011-07-07 19:59:34 +02:00
advance();
}
2020-11-23 19:24:19 +01:00
while ((!isDone()) && !isWordChar(nextChar())) {
2011-07-07 19:59:34 +02:00
advance();
}
2020-11-23 19:24:19 +01:00
} else {
while ((!isDone()) && !isWordChar(nextChar())) {
2011-07-07 19:59:34 +02:00
advance();
}
2020-11-23 19:24:19 +01:00
while ((!isDone()) && isWordChar(nextChar())) {
2011-07-07 19:59:34 +02:00
advance();
}
2011-03-26 14:10:41 +01:00
}
return i;
}
function handleKeyEvent(evt) {
2011-07-07 19:59:34 +02:00
if (!isEditable) return;
2020-11-23 19:24:19 +01:00
const type = evt.type;
const charCode = evt.charCode;
const keyCode = evt.keyCode;
const which = evt.which;
const altKey = evt.altKey;
const shiftKey = evt.shiftKey;
2011-03-26 14:10:41 +01:00
// Is caret potentially hidden by the chat button?
2020-11-23 19:24:19 +01:00
const myselection = document.getSelection(); // get the current caret selection
const caretOffsetTop = myselection.focusNode.parentNode.offsetTop | myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214
2020-11-23 19:24:19 +01:00
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
2020-11-23 19:24:19 +01:00
} else {
var lineHeight = myselection.focusNode.offsetHeight; // line height of blank lines
}
2015-05-06 01:32:36 +02:00
2020-11-23 19:24:19 +01:00
// dmesg("keyevent type: "+type+", which: "+which);
2011-03-26 14:10:41 +01:00
// 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.
2020-11-23 19:24:19 +01:00
const isModKey = ((!charCode) && ((type == 'keyup') || (type == 'keydown')) && (keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 20 || keyCode == 224 || keyCode == 91));
2011-03-26 14:10:41 +01:00
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.
2020-11-23 19:24:19 +01:00
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
}
2020-11-23 19:24:19 +01:00
let specialHandled = false;
const isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == 'keydown') : (type == 'keypress'));
const isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == 'keydown') : (type == 'keypress'));
let stopped = false;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary('handleKeyEvent', function () {
if (type == 'keypress' || (isTypeForSpecialKey && keyCode == 13 /* return*/)) {
2011-07-07 19:59:34 +02:00
// in IE, special keys don't send keypress, the keydown does the action
2020-11-23 19:24:19 +01:00
if (!outsideKeyPress(evt)) {
2011-07-07 19:59:34 +02:00
evt.preventDefault();
stopped = true;
}
2020-11-23 19:24:19 +01:00
} else if (evt.key === 'Dead') {
// If it's a dead key we don't want to do any Etherpad behavior.
stopped = true;
return true;
2020-11-23 19:24:19 +01:00
} else if (type == 'keydown') {
2011-07-07 19:59:34 +02:00
outsideKeyDown(evt);
}
2020-11-23 19:24:19 +01:00
if (!stopped) {
const specialHandledInHook = hooks.callAll('aceKeyEvent', {
callstack: currentCallStack,
2020-11-23 19:24:19 +01:00
editorInfo,
rep,
documentAttributeManager,
evt,
});
// if any hook returned true, set specialHandled with true
if (specialHandledInHook) {
specialHandled = _.contains(specialHandledInHook, true);
}
2020-11-23 19:24:19 +01:00
const 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)
2020-11-23 19:24:19 +01:00
const firstEditbarElement = parent.parent.$('#editbar').children('ul').first().children().first().children().first().children().first();
$(this).blur();
2015-03-25 16:49:41 +01:00
firstEditbarElement.focus();
evt.preventDefault();
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && altKey && keyCode == 67 && type === 'keydown' && padShortcutEnabled.altC) {
// Alt c focuses on the Chat window
$(this).blur();
parent.parent.chat.show();
2020-11-23 19:24:19 +01:00
parent.parent.$('#chatinput').focus();
evt.preventDefault();
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === 'keydown' && padShortcutEnabled.cmdShift2) {
2015-04-05 14:42:26 +02:00
// Control-Shift-2 shows a gritter popup showing a line author
2020-11-23 19:24:19 +01:00
const lineNumber = rep.selEnd[0];
const alineAttrs = rep.alines[lineNumber];
const 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.
2020-11-23 19:24:19 +01:00
let author = null;
if (alineAttrs) {
var authors = [];
var authorNames = [];
2020-11-23 19:24:19 +01:00
const opIter = Changeset.opIterator(alineAttrs);
2020-11-23 19:24:19 +01:00
while (opIter.hasNext()) {
const op = opIter.next();
authorId = Changeset.opAttributeValue(op, 'author', apool);
// Only push unique authors and ones with values
2020-11-23 19:24:19 +01:00
if (authors.indexOf(authorId) === -1 && authorId !== '') {
authors.push(authorId);
}
}
}
// No author information is available IE on a new pad.
2020-11-23 19:24:19 +01:00
if (authors.length === 0) {
var authorString = 'No author information is available';
} else {
// Known authors info, both current and historical
2020-11-23 19:24:19 +01:00
const padAuthors = parent.parent.pad.userList();
let authorObj = {};
authors.forEach((authorId) => {
padAuthors.forEach((padAuthor) => {
// If the person doing the lookup is the author..
2020-11-23 19:24:19 +01:00
if (padAuthor.userId === authorId) {
if (parent.parent.clientVars.userId === authorId) {
authorObj = {
2020-11-23 19:24:19 +01:00
name: 'Me',
};
} else {
authorObj = padAuthor;
}
}
});
2020-11-23 19:24:19 +01:00
if (!authorObj) {
author = 'Unknown';
return;
}
author = authorObj.name;
2020-11-23 19:24:19 +01:00
if (!author) author = 'Unknown';
authorNames.push(author);
2020-11-23 19:24:19 +01:00
});
}
2020-11-23 19:24:19 +01:00
if (authors.length === 1) {
var authorString = `The author of this line is ${authorNames}`;
}
2020-11-23 19:24:19 +01:00
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
2020-11-23 19:24:19 +01:00
time: '4000',
});
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForSpecialKey && keyCode == 8 && padShortcutEnabled.delete) {
2011-07-07 19:59:34 +02:00
// "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();
2011-03-26 14:10:41 +01:00
doDeleteKey(evt);
specialHandled = true;
2011-07-07 19:59:34 +02:00
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForSpecialKey && keyCode == 13 && padShortcutEnabled.return) {
2011-07-07 19:59:34 +02:00
// return key, handle specially;
// note that in mozilla we need to do an incorporation for proper return behavior anyway.
fastIncorp(4);
evt.preventDefault();
doReturnKey();
2020-11-23 19:24:19 +01:00
// scrollSelectionIntoView();
scheduler.setTimeout(() => {
2011-07-07 19:59:34 +02:00
outerWin.scrollBy(-100, 0);
}, 0);
specialHandled = true;
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForSpecialKey && keyCode == 27 && padShortcutEnabled.esc) {
2015-10-29 18:54:21 +01:00
// prevent esc key;
// in mozilla versions 14-19 avoid reconnecting pad.
fastIncorp(4);
evt.preventDefault();
specialHandled = true;
// close all gritters when the user hits escape key
parent.parent.$.gritter.removeAll();
2015-10-29 18:54:21 +01:00
}
2020-11-23 19:24:19 +01:00
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();
2020-11-23 19:24:19 +01:00
const originalBackground = parent.parent.$('#revisionlink').css('background');
parent.parent.$('#revisionlink').css({background: 'lightyellow'});
scheduler.setTimeout(() => {
parent.parent.$('#revisionlink').css({background: originalBackground});
}, 1000);
2020-11-23 19:24:19 +01:00
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;
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && !(evt.metaKey || evt.ctrlKey) && padShortcutEnabled.tab) {
2011-07-07 19:59:34 +02:00
// tab
fastIncorp(5);
evt.preventDefault();
2011-03-26 14:10:41 +01:00
doTabKey(evt.shiftKey);
2020-11-23 19:24:19 +01:00
// scrollSelectionIntoView();
2011-07-07 19:59:34 +02:00
specialHandled = true;
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'z' && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdZ) {
2014-11-22 20:13:23 +01:00
// cmd-Z (undo)
2011-07-07 19:59:34 +02:00
fastIncorp(6);
evt.preventDefault();
2020-11-23 19:24:19 +01:00
if (evt.shiftKey) {
doUndoRedo('redo');
} else {
doUndoRedo('undo');
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
specialHandled = true;
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'y' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdY) {
2011-07-07 19:59:34 +02:00
// cmd-Y (redo)
fastIncorp(10);
evt.preventDefault();
2020-11-23 19:24:19 +01:00
doUndoRedo('redo');
2011-07-07 19:59:34 +02:00
specialHandled = true;
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'b' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdB) {
2011-07-07 19:59:34 +02:00
// cmd-B (bold)
fastIncorp(13);
evt.preventDefault();
2011-03-26 14:10:41 +01:00
toggleAttributeOnSelection('bold');
2011-07-07 19:59:34 +02:00
specialHandled = true;
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'i' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdI) {
2011-07-07 19:59:34 +02:00
// cmd-I (italic)
fastIncorp(14);
evt.preventDefault();
2011-03-26 14:10:41 +01:00
toggleAttributeOnSelection('italic');
2011-07-07 19:59:34 +02:00
specialHandled = true;
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'u' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdU) {
2011-07-07 19:59:34 +02:00
// cmd-U (underline)
fastIncorp(15);
evt.preventDefault();
2011-03-26 14:10:41 +01:00
toggleAttributeOnSelection('underline');
2011-07-07 19:59:34 +02:00
specialHandled = true;
}
2020-11-23 19:24:19 +01:00
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;
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'l' && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftL) {
// cmd-shift-L (unorderedlist)
fastIncorp(9);
evt.preventDefault();
2020-11-23 19:24:19 +01:00
doInsertUnorderedList();
specialHandled = true;
2020-11-23 19:24:19 +01:00
}
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();
2020-11-23 19:24:19 +01:00
doInsertOrderedList();
specialHandled = true;
2020-11-23 19:24:19 +01:00
}
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();
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'h' && (evt.ctrlKey) && padShortcutEnabled.cmdH) {
2011-07-07 19:59:34 +02:00
// cmd-H (backspace)
fastIncorp(20);
evt.preventDefault();
2011-03-26 14:10:41 +01:00
doDeleteKey();
2011-07-07 19:59:34 +02:00
specialHandled = true;
}
2020-11-23 19:24:19 +01:00
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
2020-11-23 19:24:19 +01:00
const oldVisibleLineRange = scroll.getVisibleLineRange(rep);
let topOffset = rep.selStart[0] - oldVisibleLineRange[0];
if (topOffset < 0) {
2013-02-03 18:39:49 +01:00
topOffset = 0;
}
2020-11-23 19:24:19 +01:00
const isPageDown = evt.which === 34;
const isPageUp = evt.which === 33;
2013-02-03 18:39:49 +01:00
2020-11-23 19:24:19 +01:00
scheduler.setTimeout(() => {
const newVisibleLineRange = scroll.getVisibleLineRange(rep); // the visible lines IE 1,10
const linesCount = rep.lines.length(); // total count of lines in pad IE 10
const numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now?
2013-02-03 18:39:49 +01:00
2020-11-23 19:24:19 +01:00
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)
2013-02-03 18:39:49 +01:00
}
2020-11-23 19:24:19 +01:00
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
}
2013-02-03 18:39:49 +01:00
}
2020-11-23 19:24:19 +01:00
// ensure min and max
if (rep.selEnd[0] < 0) {
rep.selEnd[0] = 0;
2013-02-03 18:39:49 +01:00
}
2020-11-23 19:24:19 +01:00
if (rep.selStart[0] < 0) {
2013-03-18 19:44:01 +01:00
rep.selStart[0] = 0;
}
2020-11-23 19:24:19 +01:00
if (rep.selEnd[0] >= linesCount) {
rep.selEnd[0] = linesCount - 1;
2013-02-03 18:39:49 +01:00
}
updateBrowserSelectionFromRep();
2020-11-23 19:24:19 +01:00
const myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current
let caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214
2015-01-18 20:58:38 +01:00
// sometimes the first selection is -1 which causes problems (Especially with ep_page_view)
// so use focusNode.offsetTop value.
2020-11-23 19:24:19 +01:00
if (caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop;
scroll.setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document
2013-02-03 18:39:49 +01:00
}, 200);
}
// scroll to viewport when user presses arrow keys and caret is out of the viewport
2020-11-23 19:24:19 +01:00
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
2020-11-23 19:24:19 +01:00
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.
2020-11-23 19:24:19 +01:00
const selection = getSelection();
2020-11-23 19:24:19 +01:00
if (selection) {
const arrowUp = evt.which === 38;
const innerHeight = getInnerHeight();
scroll.scrollWhenPressArrowKeys(arrowUp, rep, innerHeight);
}
}
}
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (type == 'keydown') {
2011-07-07 19:59:34 +02:00
idleWorkTimer.atLeast(500);
2020-11-23 19:24:19 +01:00
} else if (type == 'keypress') {
if ((!specialHandled) && false /* parenModule.shouldNormalizeOnChar(charCode)*/) {
2011-07-07 19:59:34 +02:00
idleWorkTimer.atMost(0);
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
idleWorkTimer.atLeast(500);
}
2020-11-23 19:24:19 +01:00
} else if (type == 'keyup') {
const wait = 0;
2011-07-07 19:59:34 +02:00
idleWorkTimer.atLeast(wait);
idleWorkTimer.atMost(wait);
2011-03-26 14:10:41 +01:00
}
// Is part of multi-keystroke international character on Firefox Mac
2020-11-23 19:24:19 +01:00
const isFirefoxHalfCharacter = (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0);
2011-03-26 14:10:41 +01:00
// Is part of multi-keystroke international character on Safari Mac
2020-11-23 19:24:19 +01:00
const isSafariHalfCharacter = (browser.safari && evt.altKey && keyCode == 229);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter) {
2011-07-07 19:59:34 +02:00
idleWorkTimer.atLeast(3000); // give user time to type
// if this is a keydown, e.g., the keyup shouldn't trigger a normalize
thisKeyDoesntTriggerNormalize = true;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) {
if (type != 'keyup') {
2011-07-07 19:59:34 +02:00
observeChangesAroundSelection();
}
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (type == 'keyup') {
2011-07-07 19:59:34 +02:00
thisKeyDoesntTriggerNormalize = false;
2011-03-26 14:10:41 +01:00
}
});
}
var thisKeyDoesntTriggerNormalize = false;
2020-11-23 19:24:19 +01:00
let arrowKeyWasReleased = true;
function continuouslyPressingArrowKey(type) {
2020-11-23 19:24:19 +01:00
let firstTimeKeyIsContinuouslyPressed = false;
2020-11-23 19:24:19 +01:00
if (type == 'keyup') { arrowKeyWasReleased = true; } else if (type == 'keydown' && arrowKeyWasReleased) {
firstTimeKeyIsContinuouslyPressed = true;
arrowKeyWasReleased = false;
}
return !firstTimeKeyIsContinuouslyPressed;
}
function doUndoRedo(which) {
2011-03-26 14:10:41 +01:00
// precond: normalized DOM
2020-11-23 19:24:19 +01:00
if (undoModule.enabled) {
let whichMethod;
if (which == 'undo') whichMethod = 'performUndo';
if (which == 'redo') whichMethod = 'performRedo';
if (whichMethod) {
const oldEventType = currentCallStack.editEvent.eventType;
2011-07-07 19:59:34 +02:00
currentCallStack.startNewEvent(which);
2020-11-23 19:24:19 +01:00
undoModule[whichMethod]((backset, selectionInfo) => {
if (backset) {
2011-07-07 19:59:34 +02:00
performDocumentApplyChangeset(backset);
}
2020-11-23 19:24:19 +01:00
if (selectionInfo) {
2011-07-07 19:59:34 +02:00
performSelectionChange(lineAndColumnFromChar(selectionInfo.selStart), lineAndColumnFromChar(selectionInfo.selEnd), selectionInfo.selFocusAtStart);
}
2020-11-23 19:24:19 +01:00
const oldEvent = currentCallStack.startNewEvent(oldEventType, true);
2011-07-07 19:59:34 +02:00
return oldEvent;
});
2011-03-26 14:10:41 +01:00
}
}
}
editorInfo.ace_doUndoRedo = doUndoRedo;
function updateBrowserSelectionFromRep() {
2011-03-26 14:10:41 +01:00
// requires normalized DOM!
2020-11-23 19:24:19 +01:00
const selStart = rep.selStart;
const selEnd = rep.selEnd;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
if (!(selStart && selEnd)) {
2011-03-26 14:10:41 +01:00
setSelection(null);
return;
}
2020-11-23 19:24:19 +01:00
const selection = {};
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const ss = [selStart[0], selStart[1]];
2011-03-26 14:10:41 +01:00
selection.startPoint = getPointForLineAndChar(ss);
2020-11-23 19:24:19 +01:00
const se = [selEnd[0], selEnd[1]];
2011-03-26 14:10:41 +01:00
selection.endPoint = getPointForLineAndChar(se);
2020-11-23 19:24:19 +01:00
selection.focusAtStart = !!rep.selFocusAtStart;
2011-03-26 14:10:41 +01:00
setSelection(selection);
}
editorInfo.ace_updateBrowserSelectionFromRep = updateBrowserSelectionFromRep;
2011-03-26 14:10:41 +01:00
function nodeMaxIndex(nd) {
2011-03-26 14:10:41 +01:00
if (isNodeText(nd)) return nd.nodeValue.length;
else return 1;
}
function hasIESelection() {
2020-11-23 19:24:19 +01:00
let browserSelection;
try {
2011-07-07 19:59:34 +02:00
browserSelection = doc.selection;
2020-11-23 19:24:19 +01:00
} catch (e) {}
2011-07-07 19:59:34 +02:00
if (!browserSelection) return false;
2020-11-23 19:24:19 +01:00
let origSelectionRange;
try {
2011-07-07 19:59:34 +02:00
origSelectionRange = browserSelection.createRange();
2020-11-23 19:24:19 +01:00
} catch (e) {}
2011-07-07 19:59:34 +02:00
if (!origSelectionRange) return false;
2011-03-26 14:10:41 +01:00
return true;
}
function getSelection() {
2011-03-26 14:10:41 +01:00
// 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.
2020-11-23 19:24:19 +01:00
if (browser.msie) {
2011-03-26 14:10:41 +01:00
var browserSelection;
2020-11-23 19:24:19 +01:00
try {
2011-07-07 19:59:34 +02:00
browserSelection = doc.selection;
2020-11-23 19:24:19 +01:00
} catch (e) {}
2011-07-07 19:59:34 +02:00
if (!browserSelection) return null;
2020-11-23 19:24:19 +01:00
let origSelectionRange;
try {
2011-07-07 19:59:34 +02:00
origSelectionRange = browserSelection.createRange();
2020-11-23 19:24:19 +01:00
} catch (e) {}
2011-07-07 19:59:34 +02:00
if (!origSelectionRange) return null;
2020-11-23 19:24:19 +01:00
const selectionParent = origSelectionRange.parentElement();
2011-03-26 14:10:41 +01:00
if (selectionParent.ownerDocument != doc) return null;
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
const newRange = function () {
2011-07-07 19:59:34 +02:00
return doc.body.createTextRange();
2012-02-19 14:52:24 +01:00
};
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
const rangeForElementNode = function (nd) {
const rng = newRange();
2011-07-07 19:59:34 +02:00
// doesn't work on text nodes
rng.moveToElementText(nd);
return rng;
2012-02-19 14:52:24 +01:00
};
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
const pointFromCollapsedRange = function (rng) {
const parNode = rng.parentElement();
let elemBelow = -1;
let elemAbove = parNode.childNodes.length;
const rangeWithin = rangeForElementNode(parNode);
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
if (rng.compareEndPoints('StartToStart', rangeWithin) === 0) {
2011-07-07 19:59:34 +02:00
return {
node: parNode,
index: 0,
2020-11-23 19:24:19 +01:00
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
2020-11-23 19:24:19 +01:00
} else if (rng.compareEndPoints('EndToEnd', rangeWithin) === 0) {
if (isBlockElement(parNode) && parNode.nextSibling) {
2011-07-07 19:59:34 +02:00
// caret after block is not consistent across browsers
// (same line vs next) so put caret before next node
return {
node: parNode.nextSibling,
index: 0,
2020-11-23 19:24:19 +01:00
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
}
return {
node: parNode,
index: 1,
2020-11-23 19:24:19 +01:00
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
2020-11-23 19:24:19 +01:00
} else if (parNode.childNodes.length === 0) {
2011-07-07 19:59:34 +02:00
return {
node: parNode,
index: 0,
2020-11-23 19:24:19 +01:00
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
}
2020-11-23 19:24:19 +01:00
for (let i = 0; i < parNode.childNodes.length; i++) {
const n = parNode.childNodes.item(i);
if (!isNodeText(n)) {
const nodeRange = rangeForElementNode(n);
const startComp = rng.compareEndPoints('StartToStart', nodeRange);
const endComp = rng.compareEndPoints('EndToEnd', nodeRange);
if (startComp >= 0 && endComp <= 0) {
let index = 0;
if (startComp > 0) {
2011-07-07 19:59:34 +02:00
index = 1;
}
return {
node: n,
2020-11-23 19:24:19 +01:00
index,
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
2020-11-23 19:24:19 +01:00
} else if (endComp > 0) {
if (i > elemBelow) {
2011-07-07 19:59:34 +02:00
elemBelow = i;
2020-11-23 19:24:19 +01:00
rangeWithin.setEndPoint('StartToEnd', nodeRange);
2011-07-07 19:59:34 +02:00
}
2020-11-23 19:24:19 +01:00
} else if (startComp < 0) {
if (i < elemAbove) {
2011-07-07 19:59:34 +02:00
elemAbove = i;
2020-11-23 19:24:19 +01:00
rangeWithin.setEndPoint('EndToStart', nodeRange);
2011-07-07 19:59:34 +02:00
}
}
}
}
2020-11-23 19:24:19 +01:00
if ((elemAbove - elemBelow) == 1) {
if (elemBelow >= 0) {
2011-07-07 19:59:34 +02:00
return {
node: parNode.childNodes.item(elemBelow),
index: 1,
2020-11-23 19:24:19 +01:00
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
return {
node: parNode.childNodes.item(elemAbove),
index: 0,
2020-11-23 19:24:19 +01:00
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
}
}
2020-11-23 19:24:19 +01:00
let idx = 0;
const r = rng.duplicate();
2011-07-07 19:59:34 +02:00
// infinite stateful binary search! call function for values 0 to inf,
// expecting the answer to be about 40. return index of smallest
// true value.
2020-11-23 19:24:19 +01:00
const indexIntoRange = binarySearchInfinite(40, (i) => {
2011-07-07 19:59:34 +02:00
// the search algorithm whips the caret back and forth,
// though it has to be moved relatively and may hit
// the end of the buffer
2020-11-23 19:24:19 +01:00
const delta = i - idx;
const moved = Math.abs(r.move('character', -delta));
2011-07-07 19:59:34 +02:00
// 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:
2020-11-23 19:24:19 +01:00
if (r.move('character', -1)) r.move('character', 1);
2011-07-07 19:59:34 +02:00
if (delta < 0) idx -= moved;
else idx += moved;
2020-11-23 19:24:19 +01:00
return (r.compareEndPoints('StartToStart', rangeWithin) <= 0);
2011-07-07 19:59:34 +02:00
});
// iterate over consecutive text nodes, point is in one of them
2020-11-23 19:24:19 +01:00
let textNode = elemBelow + 1;
let indexLeft = indexIntoRange;
while (textNode < elemAbove) {
2011-07-07 19:59:34 +02:00
var tn = parNode.childNodes.item(textNode);
2020-11-23 19:24:19 +01:00
if (indexLeft <= tn.nodeValue.length) {
2011-07-07 19:59:34 +02:00
return {
node: tn,
index: indexLeft,
2020-11-23 19:24:19 +01:00
maxIndex: tn.nodeValue.length,
2011-07-07 19:59:34 +02:00
};
}
indexLeft -= tn.nodeValue.length;
textNode++;
}
var tn = parNode.childNodes.item(textNode - 1);
return {
node: tn,
index: tn.nodeValue.length,
2020-11-23 19:24:19 +01:00
maxIndex: tn.nodeValue.length,
2011-07-07 19:59:34 +02:00
};
2012-02-19 14:52:24 +01:00
};
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
var selection = {};
2020-11-23 19:24:19 +01:00
if (origSelectionRange.compareEndPoints('StartToEnd', origSelectionRange) === 0) {
2011-07-07 19:59:34 +02:00
// collapsed
2020-11-23 19:24:19 +01:00
const pnt = pointFromCollapsedRange(origSelectionRange);
2011-07-07 19:59:34 +02:00
selection.startPoint = pnt;
selection.endPoint = {
node: pnt.node,
index: pnt.index,
2020-11-23 19:24:19 +01:00
maxIndex: pnt.maxIndex,
2011-07-07 19:59:34 +02:00
};
2020-11-23 19:24:19 +01:00
} else {
const start = origSelectionRange.duplicate();
2011-07-07 19:59:34 +02:00
start.collapse(true);
2020-11-23 19:24:19 +01:00
const end = origSelectionRange.duplicate();
2011-07-07 19:59:34 +02:00
end.collapse(false);
selection.startPoint = pointFromCollapsedRange(start);
selection.endPoint = pointFromCollapsedRange(end);
2011-03-26 14:10:41 +01:00
}
return selection;
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
// non-IE browser
var browserSelection = window.getSelection();
2020-11-23 19:24:19 +01:00
if (browserSelection && browserSelection.type != 'None' && browserSelection.rangeCount !== 0) {
const range = browserSelection.getRangeAt(0);
2011-07-07 19:59:34 +02:00
function isInBody(n) {
2020-11-23 19:24:19 +01:00
while (n && !(n.tagName && n.tagName.toLowerCase() == 'body')) {
2011-07-07 19:59:34 +02:00
n = n.parentNode;
}
return !!n;
}
function pointFromRangeBound(container, offset) {
2020-11-23 19:24:19 +01:00
if (!isInBody(container)) {
2011-07-07 19:59:34 +02:00
// command-click in Firefox selects whole document, HEAD and BODY!
return {
node: root,
index: 0,
2020-11-23 19:24:19 +01:00
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
}
2020-11-23 19:24:19 +01:00
const n = container;
const childCount = n.childNodes.length;
if (isNodeText(n)) {
2011-07-07 19:59:34 +02:00
return {
node: n,
index: offset,
2020-11-23 19:24:19 +01:00
maxIndex: n.nodeValue.length,
2011-07-07 19:59:34 +02:00
};
2020-11-23 19:24:19 +01:00
} else if (childCount === 0) {
2011-07-07 19:59:34 +02:00
return {
node: n,
index: 0,
2020-11-23 19:24:19 +01:00
maxIndex: 1,
2011-07-07 19:59:34 +02:00
};
}
// 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
2020-11-23 19:24:19 +01:00
else if (offset == childCount) {
2011-07-07 19:59:34 +02:00
var nd = n.childNodes.item(childCount - 1);
var max = nodeMaxIndex(nd);
return {
node: nd,
index: max,
2020-11-23 19:24:19 +01:00
maxIndex: max,
2011-07-07 19:59:34 +02:00
};
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
var nd = n.childNodes.item(offset);
var max = nodeMaxIndex(nd);
return {
node: nd,
index: 0,
2020-11-23 19:24:19 +01:00
maxIndex: max,
2011-07-07 19:59:34 +02:00
};
}
}
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));
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
if (selection.startPoint.node.ownerDocument !== window.document) {
return null;
}
2011-07-07 19:59:34 +02:00
return selection;
2020-11-23 19:24:19 +01:00
} else { return null; }
2011-03-26 14:10:41 +01:00
}
}
function setSelection(selection) {
function copyPoint(pt) {
2011-07-07 19:59:34 +02:00
return {
node: pt.node,
index: pt.index,
2020-11-23 19:24:19 +01:00
maxIndex: pt.maxIndex,
2011-07-07 19:59:34 +02:00
};
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (browser.msie) {
2011-03-26 14:10:41 +01:00
// 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) {
2020-11-23 19:24:19 +01:00
while (n.firstChild && !isNodeText(n.firstChild)) {
2011-03-26 14:10:41 +01:00
n = n.firstChild;
}
s.moveToElementText(n);
}
2011-07-07 19:59:34 +02:00
function newRange() {
2011-07-07 19:59:34 +02:00
return doc.body.createTextRange();
}
function setCollapsedBefore(s, n) {
2011-07-07 19:59:34 +02:00
// s is an IE TextRange, n is a dom node
2020-11-23 19:24:19 +01:00
if (isNodeText(n)) {
2011-07-07 19:59:34 +02:00
// previous node should not also be text, but prevent inf recurs
2020-11-23 19:24:19 +01:00
if (n.previousSibling && !isNodeText(n.previousSibling)) {
2011-07-07 19:59:34 +02:00
setCollapsedAfter(s, n.previousSibling);
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
setCollapsedBefore(s, n.parentNode);
}
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
moveToElementText(s, n);
2011-03-26 14:10:41 +01:00
// work around for issue that caret at beginning of line
// somehow ends up at end of previous line
2020-11-23 19:24:19 +01:00
if (s.move('character', 1)) {
2011-03-26 14:10:41 +01:00
s.move('character', -1);
}
2011-07-07 19:59:34 +02:00
s.collapse(true); // to start
}
}
function setCollapsedAfter(s, n) {
2011-07-07 19:59:34 +02:00
// s is an IE TextRange, n is a magicdom node
2020-11-23 19:24:19 +01:00
if (isNodeText(n)) {
2011-07-07 19:59:34 +02:00
// 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);
2020-11-23 19:24:19 +01:00
s.move('character', n.nodeValue.length);
} else {
2011-07-07 19:59:34 +02:00
moveToElementText(s, n);
s.collapse(false); // to end
}
}
function getPointRange(point) {
2020-11-23 19:24:19 +01:00
const s = newRange();
const n = point.node;
if (isNodeText(n)) {
2011-07-07 19:59:34 +02:00
setCollapsedBefore(s, n);
2020-11-23 19:24:19 +01:00
s.move('character', point.index);
} else if (point.index === 0) {
2011-07-07 19:59:34 +02:00
setCollapsedBefore(s, n);
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
setCollapsedAfter(s, n);
}
return s;
}
2020-11-23 19:24:19 +01:00
if (selection) {
if (!hasIESelection()) {
2011-07-07 19:59:34 +02:00
return; // don't steal focus
}
2020-11-23 19:24:19 +01:00
const startPoint = copyPoint(selection.startPoint);
const endPoint = copyPoint(selection.endPoint);
2011-07-07 19:59:34 +02:00
// fix issue where selection can't be extended past end of line
// with shift-rightarrow or shift-downarrow
2020-11-23 19:24:19 +01:00
if (endPoint.index == endPoint.maxIndex && endPoint.node.nextSibling) {
2011-07-07 19:59:34 +02:00
endPoint.node = endPoint.node.nextSibling;
endPoint.index = 0;
endPoint.maxIndex = nodeMaxIndex(endPoint.node);
}
var range = getPointRange(startPoint);
2020-11-23 19:24:19 +01:00
range.setEndPoint('EndToEnd', getPointRange(endPoint));
2011-07-07 19:59:34 +02:00
// 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) {
2020-11-23 19:24:19 +01:00
let browserSelection;
try {
2011-07-07 19:59:34 +02:00
browserSelection = doc.selection;
2020-11-23 19:24:19 +01:00
} catch (e) {}
2011-07-07 19:59:34 +02:00
if (!browserSelection) return false;
2020-11-23 19:24:19 +01:00
const rng2 = browserSelection.createRange();
2011-07-07 19:59:34 +02:00
if (rng2.parentElement().ownerDocument != doc) return false;
2020-11-23 19:24:19 +01:00
if (rng.compareEndPoints('StartToStart', rng2) !== 0) return false;
if (rng.compareEndPoints('EndToEnd', rng2) !== 0) return false;
2011-07-07 19:59:34 +02:00
return true;
}
2020-11-23 19:24:19 +01:00
if (!isEqualToDocumentSelection(range)) {
// dmesg(toSource(selection));
// dmesg(escapeHTML(doc.body.innerHTML));
2011-07-07 19:59:34 +02:00
range.select();
}
2020-11-23 19:24:19 +01:00
} else {
try {
2011-07-07 19:59:34 +02:00
doc.selection.empty();
2020-11-23 19:24:19 +01:00
} catch (e) {}
2011-07-07 19:59:34 +02:00
}
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
// non-IE browser
2020-11-23 19:24:19 +01:00
let isCollapsed;
2011-07-07 19:59:34 +02:00
function pointToRangeBound(pt) {
2020-11-23 19:24:19 +01:00
const p = copyPoint(pt);
2011-07-07 19:59:34 +02:00
// 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.
2020-11-23 19:24:19 +01:00
if (isCollapsed) {
function diveDeep() {
2020-11-23 19:24:19 +01:00
while (p.node.childNodes.length > 0) {
// && (p.node == root || p.node.parentNode == root)) {
if (p.index === 0) {
2011-07-07 19:59:34 +02:00
p.node = p.node.firstChild;
p.maxIndex = nodeMaxIndex(p.node);
2020-11-23 19:24:19 +01:00
} else if (p.index == p.maxIndex) {
2011-07-07 19:59:34 +02:00
p.node = p.node.lastChild;
p.maxIndex = nodeMaxIndex(p.node);
p.index = p.maxIndex;
2020-11-23 19:24:19 +01:00
} else { break; }
2011-07-07 19:59:34 +02:00
}
}
// now fix problem where cursor at end of text node at end of span-like element
// with background doesn't seem to show up...
2020-11-23 19:24:19 +01:00
if (isNodeText(p.node) && p.index == p.maxIndex) {
let n = p.node;
while ((!n.nextSibling) && (n != root) && (n.parentNode != root)) {
2011-07-07 19:59:34 +02:00
n = n.parentNode;
}
2020-11-23 19:24:19 +01:00
if (n.nextSibling && (!((typeof n.nextSibling.tagName) === 'string' && n.nextSibling.tagName.toLowerCase() == 'br')) && (n != p.node) && (n != root) && (n.parentNode != root)) {
2011-07-07 19:59:34 +02:00
// 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;
2011-03-26 14:10:41 +01:00
// also fixes other FF problems
2020-11-23 19:24:19 +01:00
if (!isNodeText(p.node)) {
2011-07-07 19:59:34 +02:00
diveDeep();
}
}
2020-11-23 19:24:19 +01:00
if (isNodeText(p.node)) {
2011-07-07 19:59:34 +02:00
return {
container: p.node,
2020-11-23 19:24:19 +01:00
offset: p.index,
2011-07-07 19:59:34 +02:00
};
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
// p.index in {0,1}
return {
container: p.node.parentNode,
2020-11-23 19:24:19 +01:00
offset: childIndex(p.node) + p.index,
2011-07-07 19:59:34 +02:00
};
}
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const browserSelection = window.getSelection();
if (browserSelection) {
2011-07-07 19:59:34 +02:00
browserSelection.removeAllRanges();
2020-11-23 19:24:19 +01:00
if (selection) {
2011-07-07 19:59:34 +02:00
isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index);
2020-11-23 19:24:19 +01:00
const start = pointToRangeBound(selection.startPoint);
const end = pointToRangeBound(selection.endPoint);
2011-07-07 19:59:34 +02:00
2020-11-23 19:24:19 +01:00
if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend) {
2011-07-07 19:59:34 +02:00
// can handle "backwards"-oriented selection, shift-arrow-keys move start
// of selection
browserSelection.collapse(end.container, end.offset);
browserSelection.extend(start.container, start.offset);
2020-11-23 19:24:19 +01:00
} else {
2011-07-07 19:59:34 +02:00
var range = doc.createRange();
range.setStart(start.container, start.offset);
range.setEnd(end.container, end.offset);
browserSelection.removeAllRanges();
browserSelection.addRange(range);
}
}
}
}
}
function childIndex(n) {
2020-11-23 19:24:19 +01:00
let idx = 0;
while (n.previousSibling) {
2011-03-26 14:10:41 +01:00
idx++;
n = n.previousSibling;
}
return idx;
}
function fixView() {
2011-03-26 14:10:41 +01:00
// calling this method repeatedly should be fast
2020-11-23 19:24:19 +01:00
if (getInnerWidth() === 0 || getInnerHeight() === 0) {
2011-03-26 14:10:41 +01:00
return;
}
2020-11-23 19:24:19 +01:00
const win = outerWin;
2011-03-26 14:10:41 +01:00
enforceEditability();
$(sideDiv).addClass('sidedivdelayed');
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const _teardownActions = [];
2011-07-07 19:59:34 +02:00
function teardown() {
2020-11-23 19:24:19 +01:00
_.each(_teardownActions, (a) => {
2011-07-07 19:59:34 +02:00
a();
});
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const iePastedLines = null;
2011-07-07 19:59:34 +02:00
function handleIEPaste(evt) {
2011-03-26 14:10:41 +01:00
// Pasting in IE loses blank lines in a way that loses information;
// "one\n\ntwo\nthree" becomes "<p>one</p><p>two</p><p>three</p>",
// which becomes "one\ntwo\nthree". We can get the correct text
// from the clipboard directly, but we still have to let the paste
// happen to get the style information.
2020-11-23 19:24:19 +01:00
const clipText = window.clipboardData && window.clipboardData.getData('Text');
if (clipText && doc.selection) {
2011-03-26 14:10:41 +01:00
// 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
2020-11-23 19:24:19 +01:00
// function escapeLine(txt) {
// return processSpaces(escapeHTML(textify(txt)));
// }
// var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('<br>');
// doc.selection.createRange().pasteHTML(newHTML);
// evt.preventDefault();
// iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify);
2011-03-26 14:10:41 +01:00
}
}
var inInternationalComposition = false;
function handleCompositionEvent(evt) {
// international input events, fired in FF3, at least; allow e.g. Japanese input
2020-11-23 19:24:19 +01:00
if (evt.type == 'compositionstart') {
inInternationalComposition = true;
2020-11-23 19:24:19 +01:00
} else if (evt.type == 'compositionend') {
inInternationalComposition = false;
}
}
editorInfo.ace_getInInternationalComposition = function () {
return inInternationalComposition;
2020-11-23 19:24:19 +01:00
};
2011-03-26 14:10:41 +01:00
function bindTheEventHandlers() {
2020-11-23 19:24:19 +01:00
$(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
2020-11-23 19:24:19 +01:00
$(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);
2020-11-23 19:24:19 +01:00
$(root).on('blur', handleBlur);
if (browser.msie) {
$(document).on('click', handleIEOuterClick);
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
if (browser.msie) $(root).on('paste', handleIEPaste);
2015-01-19 00:58:47 +01:00
// If non-nullish, pasting on a link should be suppressed.
let suppressPasteOnLink = null;
2020-11-23 19:24:19 +01:00
$(root).on('auxclick', (e) => {
if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) {
// The user middle-clicked on a link. Usually users do this to open a link in a new tab, but
// in X11 (Linux) this will instead paste the contents of the primary selection at the mouse
// cursor. Users almost certainly do not want to paste when middle-clicking on a link, so
// tell the 'paste' event handler to suppress the paste. This is done by starting a
// short-lived timer that suppresses paste (when the target is a link) until either the
// paste event arrives or the timer fires.
//
// Why it is implemented this way:
// * Users want to be able to paste on a link via Ctrl-V, the Edit menu, or the context
// menu (https://github.com/ether/etherpad-lite/issues/2775) so we cannot simply
// suppress all paste actions when the target is a link.
// * Non-X11 systems do not paste when the user middle-clicks, so the paste suppression
// must be self-resetting.
// * On non-X11 systems, middle click should continue to open the link in a new tab.
// Suppressing the middle click here in the 'auxclick' handler (via e.preventDefault())
// would break that behavior.
suppressPasteOnLink = scheduler.setTimeout(() => { suppressPasteOnLink = null; }, 0);
}
});
2020-11-23 19:24:19 +01:00
$(root).on('paste', (e) => {
if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) {
scheduler.clearTimeout(suppressPasteOnLink);
suppressPasteOnLink = null;
e.preventDefault();
return;
2015-01-19 00:58:47 +01:00
}
// Call paste hook
hooks.callAll('acePaste', {
2020-11-23 19:24:19 +01:00
editorInfo,
rep,
documentAttributeManager,
e,
});
2020-11-23 19:24:19 +01:00
});
2015-01-19 00:58:47 +01:00
2016-01-17 16:11:54 +01:00
// 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..
2020-11-23 19:24:19 +01:00
$(document).on('drop', (e) => {
if (e.target.a || e.target.localName === 'a') {
2015-12-05 19:50:51 +01:00
e.preventDefault();
}
// Bug fix: when user drags some content and drop it far from its origin, we
// need to merge the changes into a single changeset. So mark origin with <style>,
// in order to make content be observed by incorporateUserChanges() (see
// observeSuspiciousNodes() for more info)
2020-11-23 19:24:19 +01:00
const selection = getSelection();
if (selection) {
const firstLineSelected = topLevel(selection.startPoint.node);
const lastLineSelected = topLevel(selection.endPoint.node);
2020-11-23 19:24:19 +01:00
const lineBeforeSelection = firstLineSelected.previousSibling;
const lineAfterSelection = lastLineSelected.nextSibling;
2020-11-23 19:24:19 +01:00
const neighbor = lineBeforeSelection || lineAfterSelection;
neighbor.appendChild(document.createElement('style'));
}
// Call drop hook
2015-12-05 19:50:51 +01:00
hooks.callAll('aceDrop', {
2020-11-23 19:24:19 +01:00
editorInfo,
rep,
documentAttributeManager,
e,
2015-12-05 19:50:51 +01:00
});
});
// CompositionEvent is not implemented below IE version 8
2020-11-23 19:24:19 +01:00
if (!(browser.msie && parseInt(browser.version <= 9)) && document.documentElement) {
$(document.documentElement).on('compositionstart', handleCompositionEvent);
$(document.documentElement).on('compositionend', handleCompositionEvent);
2011-03-26 14:10:41 +01:00
}
}
function topLevel(n) {
if ((!n) || n == root) return null;
2020-11-23 19:24:19 +01:00
while (n.parentNode != root) {
n = n.parentNode;
}
return n;
}
function handleIEOuterClick(evt) {
2020-11-23 19:24:19 +01:00
if ((evt.target.tagName || '').toLowerCase() != 'html') {
2011-03-26 14:10:41 +01:00
return;
}
2020-11-23 19:24:19 +01:00
if (!(evt.pageY > root.clientHeight)) {
2011-03-26 14:10:41 +01:00
return;
}
// click below the body
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary('handleOuterClick', () => {
2011-03-26 14:10:41 +01:00
// put caret at bottom of doc
fastIncorp(11);
2020-11-23 19:24:19 +01:00
if (isCaret()) { // don't interfere with drag
const lastLine = rep.lines.length() - 1;
const lastCol = rep.lines.atIndex(lastLine).text.length;
2011-07-07 19:59:34 +02:00
performSelectionChange([lastLine, lastCol], [lastLine, lastCol]);
2011-03-26 14:10:41 +01:00
}
});
}
function getClassArray(elem, optFilter) {
2020-11-23 19:24:19 +01:00
const bodyClasses = [];
(elem.className || '').replace(/\S+/g, (c) => {
if ((!optFilter) || (optFilter(c))) {
2011-07-07 19:59:34 +02:00
bodyClasses.push(c);
2011-03-26 14:10:41 +01:00
}
});
return bodyClasses;
}
2011-07-07 19:59:34 +02:00
function setClassArray(elem, array) {
2011-03-26 14:10:41 +01:00
elem.className = array.join(' ');
}
2011-07-07 19:59:34 +02:00
function focus() {
2011-03-26 14:10:41 +01:00
window.focus();
}
function handleBlur(evt) {
2020-11-23 19:24:19 +01:00
if (browser.msie) {
2011-03-26 14:10:41 +01:00
// a fix: in IE, clicking on a control like a button outside the
// iframe can "blur" the editor, causing it to stop getting
// events, though typing still affects it(!).
setSelection(null);
}
}
function getSelectionPointX(point) {
2011-03-26 14:10:41 +01:00
// doesn't work in wrap-mode
2020-11-23 19:24:19 +01:00
const node = point.node;
const index = point.index;
2011-07-07 19:59:34 +02:00
function leftOf(n) {
2011-07-07 19:59:34 +02:00
return n.offsetLeft;
}
function rightOf(n) {
2011-07-07 19:59:34 +02:00
return n.offsetLeft + n.offsetWidth;
}
2020-11-23 19:24:19 +01:00
if (!isNodeText(node)) {
2012-02-19 14:52:24 +01:00
if (index === 0) return leftOf(node);
2011-03-26 14:10:41 +01:00
else return rightOf(node);
2020-11-23 19:24:19 +01:00
} else {
2011-03-26 14:10:41 +01:00
// we can get bounds of element nodes, so look for those.
// allow consecutive text nodes for robustness.
2020-11-23 19:24:19 +01:00
let charsToLeft = index;
let charsToRight = node.nodeValue.length - index;
let n;
for (n = node.previousSibling; n && isNodeText(n); n = n.previousSibling) charsToLeft += n.nodeValue;
const leftEdge = (n ? rightOf(n) : leftOf(node.parentNode));
for (n = node.nextSibling; n && isNodeText(n); n = n.nextSibling) charsToRight += n.nodeValue;
const rightEdge = (n ? leftOf(n) : rightOf(node.parentNode));
const frac = (charsToLeft / (charsToLeft + charsToRight));
const pixLoc = leftEdge + frac * (rightEdge - leftEdge);
2011-03-26 14:10:41 +01:00
return Math.round(pixLoc);
}
}
function getPageHeight() {
2020-11-23 19:24:19 +01:00
const win = outerWin;
const odoc = win.document;
2011-03-26 14:10:41 +01:00
if (win.innerHeight && win.scrollMaxY) return win.innerHeight + win.scrollMaxY;
else if (odoc.body.scrollHeight > odoc.body.offsetHeight) return odoc.body.scrollHeight;
else return odoc.body.offsetHeight;
}
function getPageWidth() {
2020-11-23 19:24:19 +01:00
const win = outerWin;
const odoc = win.document;
2011-03-26 14:10:41 +01:00
if (win.innerWidth && win.scrollMaxX) return win.innerWidth + win.scrollMaxX;
else if (odoc.body.scrollWidth > odoc.body.offsetWidth) return odoc.body.scrollWidth;
else return odoc.body.offsetWidth;
}
function getInnerHeight() {
2020-11-23 19:24:19 +01:00
const win = outerWin;
const odoc = win.document;
let h;
2015-01-21 16:01:39 +01:00
if (browser.opera) h = win.innerHeight;
2011-03-26 14:10:41 +01:00
else h = odoc.documentElement.clientHeight;
if (h) return h;
// deal with case where iframe is hidden, hope that
// style.height of iframe container is set in px
2011-07-07 19:59:34 +02:00
return Number(editorInfo.frame.parentNode.style.height.replace(/[^0-9]/g, '') || 0);
2011-03-26 14:10:41 +01:00
}
function getInnerWidth() {
2020-11-23 19:24:19 +01:00
const win = outerWin;
const odoc = win.document;
2011-03-26 14:10:41 +01:00
return odoc.documentElement.clientWidth;
}
function scrollXHorizontallyIntoView(pixelX) {
2020-11-23 19:24:19 +01:00
const win = outerWin;
const odoc = outerWin.document;
const distInsideLeft = pixelX - win.scrollX;
const distInsideRight = win.scrollX + getInnerWidth() - pixelX;
if (distInsideLeft < 0) {
2011-03-26 14:10:41 +01:00
win.scrollBy(distInsideLeft, 0);
2020-11-23 19:24:19 +01:00
} else if (distInsideRight < 0) {
2011-07-07 19:59:34 +02:00
win.scrollBy(-distInsideRight + 1, 0);
2011-03-26 14:10:41 +01:00
}
}
function scrollSelectionIntoView() {
2011-07-07 19:59:34 +02:00
if (!rep.selStart) return;
2011-03-26 14:10:41 +01:00
fixView();
2020-11-23 19:24:19 +01:00
const innerHeight = getInnerHeight();
scroll.scrollNodeVerticallyIntoView(rep, innerHeight);
2020-11-23 19:24:19 +01:00
if (!doesWrap) {
const browserSelection = getSelection();
if (browserSelection) {
const focusPoint = (browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint);
const selectionPointX = getSelectionPointX(focusPoint);
2011-07-07 19:59:34 +02:00
scrollXHorizontallyIntoView(selectionPointX);
fixView();
2011-03-26 14:10:41 +01:00
}
}
}
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
const listAttributeName = 'list';
2013-06-14 19:37:41 +02:00
function getLineListType(lineNum) {
2020-11-23 19:24:19 +01:00
return documentAttributeManager.getAttributeOnLine(lineNum, listAttributeName);
2011-03-26 14:10:41 +01:00
}
function setLineListType(lineNum, listType) {
2020-11-23 19:24:19 +01:00
if (listType == '') {
2012-04-05 00:50:04 +02:00
documentAttributeManager.removeAttributeOnLine(lineNum, listAttributeName);
documentAttributeManager.removeAttributeOnLine(lineNum, 'start');
2020-11-23 19:24:19 +01:00
} else {
2012-04-05 00:50:04 +02:00
documentAttributeManager.setAttributeOnLine(lineNum, listAttributeName, listType);
}
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
// if the list has been removed, it is necessary to renumber
// starting from the *next* line because the list may have been
// separated. If it returns null, it means that the list was not cut, try
// from the current one.
if (renumberList(lineNum + 1) == null) {
2012-04-05 00:50:04 +02:00
renumberList(lineNum);
}
2011-03-26 14:10:41 +01:00
}
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
function renumberList(lineNum) {
// 1-check we are in a list
let type = getLineListType(lineNum);
if (!type) {
2012-01-15 18:20:20 +01:00
return null;
}
2015-01-19 01:28:32 +01:00
type = /([a-z]+)[0-9]+/.exec(type);
2020-11-23 19:24:19 +01:00
if (type[1] == 'indent') {
2012-01-15 18:20:20 +01:00
return null;
}
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
// 2-find the first line of the list
while (lineNum - 1 >= 0 && (type = getLineListType(lineNum - 1))) {
2015-01-19 01:28:32 +01:00
type = /([a-z]+)[0-9]+/.exec(type);
2020-11-23 19:24:19 +01:00
if (type[1] == 'indent') break;
2012-01-15 18:20:20 +01:00
lineNum--;
}
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
// 3-renumber every list item of the same level from the beginning, level 1
// IMPORTANT: never skip a level because there imbrication may be arbitrary
const builder = Changeset.builder(rep.lines.totalWidth());
let loc = [0, 0];
function applyNumberList(line, level) {
2020-11-23 19:24:19 +01:00
// init
let position = 1;
let curLevel = level;
let listType;
// loop over the lines
while (listType = getLineListType(line)) {
// apply new num
2015-01-19 01:28:32 +01:00
listType = /([a-z]+)([0-9]+)/.exec(listType);
2012-01-15 18:20:20 +01:00
curLevel = Number(listType[2]);
2020-11-23 19:24:19 +01:00
if (isNaN(curLevel) || listType[0] == 'indent') {
2012-01-15 18:20:20 +01:00
return line;
2020-11-23 19:24:19 +01:00
} else if (curLevel == level) {
2012-04-05 00:50:04 +02:00
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
2020-11-23 19:24:19 +01:00
['start', position],
2012-01-15 18:20:20 +01:00
], rep.apool);
2013-06-14 19:37:41 +02:00
2012-01-15 18:20:20 +01:00
position++;
line++;
2020-11-23 19:24:19 +01:00
} else if (curLevel < level) {
return line;// back to parent
} else {
line = applyNumberList(line, level + 1);// recursive call
2012-01-15 18:20:20 +01:00
}
}
return line;
}
2013-06-14 19:37:41 +02:00
2012-01-15 18:20:20 +01:00
applyNumberList(lineNum, 1);
2020-11-23 19:24:19 +01:00
const cs = builder.toString();
if (!Changeset.isIdentity(cs)) {
2012-01-15 18:20:20 +01:00
performDocumentApplyChangeset(cs);
}
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
// 4-apply the modifications
2012-01-15 18:20:20 +01:00
}
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
function doInsertList(type) {
2020-11-23 19:24:19 +01:00
if (!(rep.selStart && rep.selEnd)) {
2011-03-26 14:10:41 +01:00
return;
}
2020-11-23 19:24:19 +01:00
let firstLine, lastLine;
2011-03-26 14:10:41 +01:00
firstLine = rep.selStart[0];
2012-02-19 14:52:24 +01:00
lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0));
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let allLinesAreList = true;
for (var n = firstLine; n <= lastLine; n++) {
var listType = getLineListType(n);
2020-11-23 19:24:19 +01:00
if (!listType || listType.slice(0, type.length) != type) {
2011-03-26 14:10:41 +01:00
allLinesAreList = false;
break;
}
}
2020-11-23 19:24:19 +01:00
const mods = [];
for (var n = firstLine; n <= lastLine; n++) {
var t = '';
2020-11-23 19:24:19 +01:00
let level = 0;
var listType = /([a-z]+)([0-9]+)/.exec(getLineListType(n));
2020-06-07 10:51:12 +02:00
// Used to outdent if ol is removed
2020-11-23 19:24:19 +01:00
if (allLinesAreList) {
2020-06-07 10:51:12 +02:00
var togglingOn = false;
2020-11-23 19:24:19 +01:00
} else {
2020-06-07 10:51:12 +02:00
var togglingOn = true;
}
2020-11-23 19:24:19 +01:00
if (listType) {
t = listType[1];
level = Number(listType[2]);
}
2011-03-26 14:10:41 +01:00
var t = getLineListType(n);
2016-01-19 05:57:40 +01:00
2020-11-23 19:24:19 +01:00
if (t === listType) togglingOn = false;
2020-06-07 10:51:12 +02:00
2020-11-23 19:24:19 +01:00
if (togglingOn) {
mods.push([n, allLinesAreList ? `indent${level}` : (t ? type + level : `${type}1`)]);
} else {
2020-06-07 10:51:12 +02:00
// scrap the entire indentation and list type
2020-11-23 19:24:19 +01:00
if (level === 1) { // if outdending but are the first item in the list then outdent
2020-06-07 10:51:12 +02:00
setLineListType(n, ''); // outdent
}
// else change to indented not bullet
2020-11-23 19:24:19 +01:00
if (level > 1) {
2020-06-07 10:51:12 +02:00
setLineListType(n, ''); // remove bullet
2020-11-23 19:24:19 +01:00
setLineListType(n, `indent${level}`); // in/outdent
2020-06-07 10:51:12 +02:00
}
}
2011-03-26 14:10:41 +01:00
}
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
_.each(mods, (mod) => {
2012-04-05 01:07:47 +02:00
setLineListType(mod[0], mod[1]);
2012-04-05 00:50:04 +02:00
});
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
function doInsertUnorderedList() {
2012-01-15 18:20:20 +01:00
doInsertList('bullet');
}
2020-11-23 19:24:19 +01:00
function doInsertOrderedList() {
2012-01-15 18:20:20 +01:00
doInsertList('number');
}
2011-03-26 14:10:41 +01:00
editorInfo.ace_doInsertUnorderedList = doInsertUnorderedList;
2012-01-15 18:20:20 +01:00
editorInfo.ace_doInsertOrderedList = doInsertOrderedList;
2013-06-14 19:37:41 +02:00
function initLineNumbers() {
2011-03-26 14:10:41 +01:00
lineNumbersShown = 1;
sideDiv.innerHTML = '<div id="sidedivinner" class="sidedivinner"><div><span class="line-number">1</span></div></div>';
2020-11-23 19:24:19 +01:00
sideDivInner = outerWin.document.getElementById('sidedivinner');
$(sideDiv).addClass('sidediv');
2011-03-26 14:10:41 +01:00
}
// We apply the height of a line in the doc body, to the corresponding sidediv line number
function updateLineNumbers() {
if (!currentCallStack || currentCallStack && !currentCallStack.domClean) return;
// Refs #4228, to avoid layout trashing, we need to first calculate all the heights,
// and then apply at once all new height to div elements
2020-11-23 19:24:19 +01:00
const lineHeights = [];
let docLine = doc.body.firstChild;
let currentLine = 0;
let h = null;
// First loop to calculate the heights from doc body
2020-11-23 19:24:19 +01:00
while (docLine) {
if (docLine.nextSibling) {
if (currentLine === 0) {
// It's the first line. For line number alignment purposes, its
// height is taken to be the top offset of the next line. If we
// didn't do this special case, we would miss out on any top margin
// included on the first line. The default stylesheet doesn't add
// extra margins/padding, but plugins might.
2020-11-23 19:24:19 +01:00
h = docLine.nextSibling.offsetTop - parseInt(window.getComputedStyle(doc.body).getPropertyValue('padding-top').split('px')[0]);
} else {
h = docLine.nextSibling.offsetTop - docLine.offsetTop;
}
} else {
// last line
h = (docLine.clientHeight || docLine.offsetHeight);
}
2020-11-23 19:24:19 +01:00
lineHeights.push(h);
docLine = docLine.nextSibling;
currentLine++;
}
2020-11-23 19:24:19 +01:00
let newNumLines = rep.lines.length();
2011-03-26 14:10:41 +01:00
if (newNumLines < 1) newNumLines = 1;
2020-11-23 19:24:19 +01:00
let sidebarLine = sideDivInner.firstChild;
2013-06-14 19:37:41 +02:00
// Apply height to existing sidediv lines
2020-11-23 19:24:19 +01:00
currentLine = 0;
while (sidebarLine && currentLine <= lineNumbersShown) {
if (lineHeights[currentLine]) {
2020-11-23 19:24:19 +01:00
sidebarLine.style.height = `${lineHeights[currentLine]}px`;
}
sidebarLine = sidebarLine.nextSibling;
currentLine++;
2013-06-14 19:37:41 +02:00
}
2020-11-23 19:24:19 +01:00
if (newNumLines != lineNumbersShown) {
const container = sideDivInner;
const odoc = outerWin.document;
const fragment = odoc.createDocumentFragment();
// Create missing line and apply height
2020-11-23 19:24:19 +01:00
while (lineNumbersShown < newNumLines) {
lineNumbersShown++;
2020-11-23 19:24:19 +01:00
const div = odoc.createElement('DIV');
if (lineHeights[currentLine]) {
2020-11-23 19:24:19 +01:00
div.style.height = `${lineHeights[currentLine]}px`;
}
2020-11-23 19:24:19 +01:00
$(div).append($(`<span class='line-number'>${String(lineNumbersShown)}</span>`));
2012-02-21 21:46:25 +01:00
fragment.appendChild(div);
currentLine++;
}
container.appendChild(fragment);
// Remove extra lines
2020-11-23 19:24:19 +01:00
while (lineNumbersShown > newNumLines) {
container.removeChild(container.lastChild);
lineNumbersShown--;
2011-03-26 14:10:41 +01:00
}
}
}
2013-06-14 19:37:41 +02:00
2012-04-05 00:50:04 +02:00
// Init documentAttributeManager
documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset);
editorInfo.ace_performDocumentApplyAttributesToRange = function () {
return documentAttributeManager.setAttributesOnRange.apply(documentAttributeManager, arguments);
};
this.init = function () {
2020-11-23 19:24:19 +01:00
$(document).ready(() => {
doc = document; // defined as a var in scope outside
2020-11-23 19:24:19 +01:00
inCallStack('setup', () => {
const body = doc.getElementById('innerdocbody');
root = body; // defined as a var in scope outside
2020-11-23 19:24:19 +01:00
if (browser.firefox) $(root).addClass('mozilla');
if (browser.safari) $(root).addClass('safari');
if (browser.msie) $(root).addClass('msie');
root.classList.toggle('authorColors', true);
root.classList.toggle('doesWrap', doesWrap);
initDynamicCSS();
enforceEditability();
// set up dom and rep
while (root.firstChild) root.removeChild(root.firstChild);
2020-11-23 19:24:19 +01:00
const oneEntry = createDomLineEntry('');
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
insertDomLines(null, [oneEntry.domInfo]);
rep.alines = Changeset.splitAttributionLines(
2020-11-23 19:24:19 +01:00
Changeset.makeAttribution('\n'), '\n');
bindTheEventHandlers();
});
2013-06-14 19:37:41 +02:00
hooks.callAll('aceInitialized', {
2020-11-23 19:24:19 +01:00
editorInfo,
rep,
documentAttributeManager,
});
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
scheduler.setTimeout(() => {
parent.readyFunc(); // defined in code that sets up the inner iframe
}, 0);
isSetUp = true;
});
2020-11-23 19:24:19 +01:00
};
}
2011-03-26 14:10:41 +01:00
exports.init = function () {
2020-11-23 19:24:19 +01:00
const editor = new Ace2Inner();
editor.init();
};