2021-01-14 11:00:14 +01:00
'use strict' ;
2011-12-04 16:33:56 +01:00
2011-03-26 14:10:41 +01:00
/ * *
* Copyright 2009 Google Inc .
2021-01-14 11:00:14 +01:00
* Copyright 2020 John McLear - The Etherpad Foundation .
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 .
* /
2021-01-14 11:00:14 +01:00
let documentAttributeManager ;
2020-12-11 21:55:13 +01:00
2020-11-23 19:24:19 +01:00
const browser = require ( './browser' ) ;
2021-01-14 11:00:14 +01:00
const padutils = require ( './pad_utils' ) . padutils ;
const Ace2Common = require ( './ace2_common' ) ;
const $ = require ( './rjquery' ) . $ ;
2020-11-23 19:24:19 +01:00
const isNodeText = Ace2Common . isNodeText ;
const getAssoc = Ace2Common . getAssoc ;
const setAssoc = Ace2Common . setAssoc ;
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' ) ;
2021-01-14 11:00:14 +01:00
const DEBUG = 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' ;
2018-01-04 15:28:00 +01:00
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
2021-01-14 11:00:14 +01:00
const focus = ( ) => {
window . focus ( ) ;
} ;
2018-01-03 22:57:28 +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 ;
2021-01-14 11:00:14 +01:00
const initLineNumbers = ( ) => {
const htmlOpen = '<div id="sidedivinner" class="sidedivinner"><div><span class="line-number">1' ;
const htmlClose = '</span></div></div>' ;
lineNumbersShown = 1 ;
sideDiv . innerHTML = ` ${ htmlOpen } ${ htmlClose } ` ;
sideDivInner = outerWin . document . getElementById ( 'sidedivinner' ) ;
$ ( sideDiv ) . addClass ( 'sidediv' ) ;
} ;
2011-03-26 14:10:41 +01:00
initLineNumbers ( ) ;
2020-11-23 19:24:19 +01:00
const scroll = Scroll . init ( outerWin ) ;
2018-01-03 22:57:28 +01:00
2020-11-23 19:24:19 +01:00
let outsideKeyDown = noop ;
2021-01-14 11:00:14 +01:00
let outsideKeyPress = ( e ) => true ;
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
2013-12-08 17:25:12 +01: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 ) ;
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
if ( ! window . console ) {
2021-01-14 11:00:14 +01:00
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
}
2011-03-26 20:47:31 +01:00
2020-11-23 19:24:19 +01:00
let PROFILER = window . PROFILER ;
if ( ! PROFILER ) {
2021-01-14 11:00:14 +01:00
PROFILER = ( ) => ( {
start : noop ,
mark : noop ,
literal : noop ,
end : noop ,
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
2013-02-09 17:42:47 +01:00
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
2021-01-14 11:00:14 +01:00
const performDocumentReplaceRange = ( start , end , newText ) => {
if ( start === undefined ) start = rep . selStart ;
if ( end === undefined ) end = rep . selEnd ;
// dmesg(String([start.toSource(),end.toSource(),newText.toSource()]));
// start[0]: <--- start[1] --->CCCCCCCCCCC\n
// CCCCCCCCCCCCCCCCCCCC\n
// CCCC\n
// end[0]: <CCC end[1] CCC>-------\n
const builder = Changeset . builder ( rep . lines . totalWidth ( ) ) ;
ChangesetUtils . buildKeepToStartOfRange ( rep , builder , start ) ;
ChangesetUtils . buildRemoveRange ( rep , builder , start , end ) ;
builder . insert ( newText , [
[ 'author' , thisAuthor ] ,
] , rep . apool ) ;
const cs = builder . toString ( ) ;
performDocumentApplyChangeset ( cs ) ;
} ;
const initDynamicCSS = ( ) => {
2020-11-23 19:24:19 +01:00
dynamicCSS = makeCSSManager ( 'dynamicsyntax' ) ;
outerDynamicCSS = makeCSSManager ( 'dynamicsyntax' , 'outer' ) ;
parentDynamicCSS = makeCSSManager ( 'dynamicsyntax' , 'parent' ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const changesetTracker = makeChangesetTracker ( scheduler , rep . apool , {
2021-01-14 11:00:14 +01:00
withCallbacks : ( operationName , f ) => {
2020-11-23 19:24:19 +01:00
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
{
2021-01-14 11:00:14 +01:00
setDocumentAttributedText : ( atext ) => {
2020-11-23 19:24:19 +01:00
setDocAText ( atext ) ;
} ,
2021-01-14 11:00:14 +01:00
applyChangesetToDocument : ( changeset , preferInsertionAfterCaret ) => {
2020-11-23 19:24:19 +01:00
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
2021-01-14 11:00:14 +01:00
const getAuthorInfos = ( ) => authorInfos ;
2020-11-23 19:24:19 +01:00
editorInfo . ace _getAuthorInfos = getAuthorInfos ;
2012-09-08 20:45:33 +02:00
2021-01-14 11:00:14 +01:00
const 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
2021-01-18 01:06:35 +01:00
if ( authorStyleSet . some ( ( it ) => it ) ) {
2020-11-23 19:24:19 +01:00
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
2021-01-18 01:10:26 +01:00
const textColor =
colorutils . textColorFromBackgroundColor ( bgcolor , parent . parent . clientVars . skinName ) ;
2020-11-23 19:24:19 +01:00
authorStyle . color = textColor ;
parentAuthorStyle . color = textColor ;
2013-06-06 06:30:48 +02:00
}
2021-01-14 11:00:14 +01:00
} ;
2013-06-06 06:30:48 +02:00
2021-01-14 11:00:14 +01:00
const setAuthorInfo = ( author , info ) => {
2020-11-23 19:24:19 +01:00
if ( ( typeof author ) !== 'string' ) {
2020-05-29 13:56:03 +02:00
// 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 ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const getAuthorClassName = ( author ) => ` author- ${ author . replace ( /[^a-y0-9]/g , ( c ) => {
if ( c === '.' ) return '-' ;
return ` z ${ c . charCodeAt ( 0 ) } z ` ;
} ) } ` ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const className2Author = ( className ) => {
if ( className . substring ( 0 , 7 ) === 'author-' ) {
2020-11-23 19:24:19 +01:00
return className . substring ( 7 ) . replace ( /[a-y0-9]+|-|z.+?z/g , ( cc ) => {
2021-01-14 11:00:14 +01:00
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 ;
2021-01-14 11:00:14 +01:00
} ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const getAuthorColorClassSelector = ( oneClassName ) => ` .authorColors . ${ oneClassName } ` ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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 ) ;
2012-02-19 14:52:24 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
editorInfo . ace _getRep = ( ) => rep ;
editorInfo . ace _getAuthor = ( ) => thisAuthor ;
2012-05-30 17:18:43 +02:00
2020-11-23 19:24:19 +01:00
const _nonScrollableEditEvents = {
applyChangesToBase : 1 ,
2016-03-30 16:51:18 +02:00
} ;
2021-01-18 01:06:35 +01:00
hooks . callAll ( 'aceRegisterNonScrollableEditEvents' ) . forEach ( ( eventType ) => {
2020-11-23 19:24:19 +01:00
_nonScrollableEditEvents [ eventType ] = 1 ;
2016-03-30 16:51:18 +02:00
} ) ;
2021-01-14 11:00:14 +01:00
const isScrollableEditEvent = ( eventType ) => ! _nonScrollableEditEvents [ eventType ] ;
2016-03-30 16:51:18 +02:00
2021-01-14 11:00:14 +01:00
let currentCallStack = null ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const inCallStack = ( type , action ) => {
2011-03-26 14:10:41 +01:00
if ( disposed ) return ;
2020-11-23 19:24:19 +01:00
if ( currentCallStack ) {
2021-01-14 11:00:14 +01:00
// Do not uncomment this in production. It will break Etherpad being provided in iFrames.
// I am leaving this in for testing usefulness.
2021-01-18 01:10:26 +01:00
// top.console.error(`Can't enter callstack ${type}, already in ${currentCallStack.type}`);
2011-03-26 14:10:41 +01:00
}
2021-01-14 11:00:14 +01:00
const newEditEvent = ( eventType ) => ( {
eventType ,
backset : null ,
} ) ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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 ( ) ;
2021-01-14 11:00:14 +01:00
} else if ( evt . eventType === 'nonundoable' ) {
2020-11-23 19:24:19 +01:00
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
}
}
}
2021-01-14 11:00:14 +01:00
} ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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 ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
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 ,
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 ) ;
2021-01-14 11:00:14 +01:00
if ( cs . domClean && cs . type !== 'setup' ) {
2012-02-19 12:15:42 +01:00
// 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 ( ) ;
}
}
2021-01-14 11:00:14 +01:00
} else if ( currentCallStack . type === 'idleWorkTimer' ) {
idleWorkTimer . atLeast ( 1000 ) ;
2011-03-26 14:10:41 +01:00
}
currentCallStack = null ;
}
return result ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
editorInfo . ace _inCallStack = inCallStack ;
2021-01-14 11:00:14 +01:00
const 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 ( ) ;
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
editorInfo . ace _inCallStackIfNecessary = inCallStackIfNecessary ;
2021-01-14 11:00:14 +01:00
const dispose = ( ) => {
2011-03-26 14:10:41 +01:00
disposed = true ;
if ( idleWorkTimer ) idleWorkTimer . never ( ) ;
teardown ( ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const setWraps = ( newVal ) => {
2011-03-26 14:10:41 +01:00
doesWrap = newVal ;
2020-11-02 21:43:30 +01:00
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 ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const setStyled = ( newVal ) => {
2020-11-23 19:24:19 +01:00
const oldVal = isStyled ;
isStyled = ! ! newVal ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
if ( newVal !== oldVal ) {
2020-11-23 19:24:19 +01:00
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 = [ ] ;
2021-01-14 11:00:14 +01:00
for ( const k of Object . keys ( 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
}
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const setTextFace = ( face ) => {
2020-06-02 11:25:43 +02:00
root . style . fontFamily = face ;
lineMetricsDiv . style . fontFamily = face ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const recreateDOM = ( ) => {
2011-03-26 14:10:41 +01:00
// precond: normalized
recolorLinesInRange ( 0 , rep . alltext . length ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const setEditable = ( newVal ) => {
2011-03-26 14:10:41 +01:00
isEditable = newVal ;
2020-11-02 21:46:37 +01:00
root . contentEditable = isEditable ? 'true' : 'false' ;
2020-11-02 21:43:30 +01:00
root . classList . toggle ( 'static' , ! isEditable ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const enforceEditability = ( ) => setEditable ( isEditable ) ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const importText = ( text , undoable , dontProcess ) => {
2020-11-23 19:24:19 +01:00
let lines ;
if ( dontProcess ) {
2021-01-14 11:00:14 +01:00
if ( text . charAt ( text . length - 1 ) !== '\n' ) {
2020-11-23 19:24:19 +01:00
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 {
2021-01-18 01:06:35 +01:00
lines = text . split ( '\n' ) . map ( 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 ) ;
} ) ;
2021-01-14 11:00:14 +01:00
if ( dontProcess && rep . alltext !== text ) {
2020-11-23 19:24:19 +01:00
throw new Error ( 'mismatch error setting raw text in importText' ) ;
2011-03-26 14:10:41 +01:00
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ) ;
} ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const setDocAText = ( atext ) => {
2020-11-23 19:24:19 +01:00
if ( atext . text === '' ) {
2020-04-13 20:16:33 +02:00
/ *
* 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' ;
2020-04-13 20:16:33 +02:00
}
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 ) ;
2021-01-14 11:00:14 +01: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 ) ;
2021-01-14 11:00:14 +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
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const setDocText = ( text ) => {
2011-03-26 14:10:41 +01:00
setDocAText ( Changeset . makeAText ( text ) ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ( ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const editorChangedSize = ( ) => fixView ( ) ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const setOnKeyPress = ( handler ) => {
2011-03-26 14:10:41 +01:00
outsideKeyPress = handler ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const setOnKeyDown = ( handler ) => {
2011-03-26 14:10:41 +01:00
outsideKeyDown = handler ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const setNotifyDirty = ( handler ) => {
2011-03-26 14:10:41 +01:00
outsideNotifyDirty = handler ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ;
2021-01-14 11:00:14 +01:00
buf . push ( ( domInfo && domInfo . getInnerHTML ( ) ) ||
domline . processSpaces ( domline . escapeHTML ( entry . text ) , doesWrap ) ||
' ' /* 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> ` ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const CMDS = {
2021-01-14 11:00:14 +01:00
clearauthorship : ( prompt ) => {
2020-11-23 19:24:19 +01:00
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
} ;
2021-01-14 11:00:14 +01:00
const execCommand = ( cmd , ... args ) => {
2011-03-26 14:10:41 +01:00
cmd = cmd . toLowerCase ( ) ;
2021-01-14 11:00:14 +01:00
// TODO: Rhansen to check this logic.
const cmdArgs = args ;
2020-11-23 19:24:19 +01:00
if ( CMDS [ cmd ] ) {
inCallStackIfNecessary ( cmd , ( ) => {
2011-07-07 19:59:34 +02:00
fastIncorp ( 9 ) ;
2021-01-14 11:00:14 +01:00
CMDS [ cmd ] ( CMDS , ... cmdArgs ) ;
2011-03-26 14:10:41 +01:00
} ) ;
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ) ;
} ) ;
2021-01-14 11:00:14 +01:00
} ;
2013-06-14 19:37:41 +02:00
2021-01-14 11:00:14 +01:00
editorInfo . ace _callWithAce = ( fn , callStack , normalize ) => {
let wrapper = ( ) => fn ( editorInfo ) ;
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
if ( normalize !== undefined ) {
const wrapper1 = wrapper ;
2021-01-14 11:00:14 +01:00
wrapper = ( ) => {
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
2021-01-14 11:00:14 +01:00
editorInfo . ace _setProperty = ( 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 ,
2020-11-02 21:43:30 +01:00
showsauthorcolors : ( val ) => root . classList . toggle ( 'authorColors' , ! ! val ) ,
showsuserselections : ( val ) => root . classList . toggle ( 'userSelections' , ! ! val ) ,
2021-01-14 11:00:14 +01:00
showslinenumbers : ( value ) => {
2020-11-23 19:24:19 +01:00
hasLineNumbers = ! ! value ;
2020-11-02 21:43:30 +01:00
sideDiv . parentNode . classList . toggle ( 'line-numbers-hidden' , ! hasLineNumbers ) ;
2012-02-21 22:29:40 +01:00
fixView ( ) ;
} ,
2020-11-02 21:43:30 +01:00
grayedout : ( val ) => outerWin . document . body . classList . toggle ( 'grayedout' , ! ! val ) ,
2021-01-14 11:00:14 +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 ,
2021-01-14 11:00:14 +01:00
rtlistrue : ( value ) => {
2020-11-02 21:43:30 +01:00
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
2021-01-14 11:00:14 +01:00
editorInfo . ace _setBaseText = ( txt ) => {
2011-03-26 14:10:41 +01:00
changesetTracker . setBaseText ( txt ) ;
} ;
2021-01-14 11:00:14 +01:00
editorInfo . ace _setBaseAttributedText = ( atxt , apoolJsonObj ) => {
2011-03-26 14:10:41 +01:00
changesetTracker . setBaseAttributedText ( atxt , apoolJsonObj ) ;
} ;
2021-01-14 11:00:14 +01:00
editorInfo . ace _applyChangesToBase = ( c , optAuthor , apoolJsonObj ) => {
2011-03-26 14:10:41 +01:00
changesetTracker . applyChangesToBase ( c , optAuthor , apoolJsonObj ) ;
} ;
2021-01-14 11:00:14 +01:00
editorInfo . ace _prepareUserChangeset = ( ) => changesetTracker . prepareUserChangeset ( ) ;
editorInfo . ace _applyPreparedChangesetToBase = ( ) => {
2011-03-26 14:10:41 +01:00
changesetTracker . applyPreparedChangesetToBase ( ) ;
} ;
2021-01-14 11:00:14 +01:00
editorInfo . ace _setUserChangeNotificationCallback = ( f ) => {
2011-03-26 14:10:41 +01:00
changesetTracker . setUserChangeNotificationCallback ( f ) ;
} ;
2021-01-14 11:00:14 +01:00
editorInfo . ace _setAuthorInfo = ( author , info ) => {
2011-03-26 14:10:41 +01:00
setAuthorInfo ( author , info ) ;
} ;
2021-01-14 11:00:14 +01:00
editorInfo . ace _setAuthorSelectionRange = ( author , start , end ) => {
2011-03-26 14:10:41 +01:00
changesetTracker . setAuthorSelectionRange ( author , start , end ) ;
} ;
2021-01-14 11:00:14 +01:00
editorInfo . ace _getUnhandledErrors = ( ) => caughtErrors . slice ( ) ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
editorInfo . ace _getDocument = ( ) => doc ;
2013-03-26 02:54:01 +01:00
2021-01-14 11:00:14 +01:00
editorInfo . ace _getDebugProperty = ( 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' ) ;
2021-01-14 11:00:14 +01:00
} else if ( prop === 'rep' ) {
2011-03-26 14:10:41 +01:00
return rep ;
2021-01-14 11:00:14 +01:00
} else if ( prop === 'window' ) {
2011-03-26 14:10:41 +01:00
return window ;
2021-01-14 11:00:14 +01:00
} else if ( prop === 'document' ) {
2011-03-26 14:10:41 +01:00
return document ;
}
return undefined ;
} ;
2021-01-14 11:00:14 +01:00
const now = ( ) => Date . now ( ) ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const newTimeLimit = ( ms ) => {
2020-11-23 19:24:19 +01:00
const startTime = now ( ) ;
let exceededAlready = false ;
let printedTrace = false ;
2021-01-14 11:00:14 +01:00
const isTimeUp = ( ) => {
2020-11-23 19:24:19 +01:00
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 {
return false ;
}
} ;
2013-06-14 19:37:41 +02:00
2021-01-14 11:00:14 +01:00
isTimeUp . elapsed = ( ) => now ( ) - startTime ;
2011-03-26 14:10:41 +01:00
return isTimeUp ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const makeIdleAction = ( func ) => {
2020-11-23 19:24:19 +01:00
let scheduledTimeout = null ;
let scheduledTime = 0 ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const unschedule = ( ) => {
2020-11-23 19:24:19 +01:00
if ( scheduledTimeout ) {
2013-02-11 21:51:40 +01:00
scheduler . clearTimeout ( scheduledTimeout ) ;
2011-07-07 19:59:34 +02:00
scheduledTimeout = null ;
2011-03-26 14:10:41 +01:00
}
2021-01-14 11:00:14 +01:00
} ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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 ;
2013-02-09 17:42:47 +01:00
scheduledTimeout = scheduler . setTimeout ( callback , delay ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const callback = ( ) => {
2011-03-26 14:10:41 +01:00
scheduledTimeout = null ;
// func may reschedule the action
func ( ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
return {
2021-01-14 11:00:14 +01:00
atMost : ( ms ) => {
2020-11-23 19:24:19 +01:00
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.
2021-01-14 11:00:14 +01:00
atLeast : ( ms ) => {
2020-11-23 19:24:19 +01:00
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
} ,
2021-01-14 11:00:14 +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
} ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const fastIncorp = ( n ) => {
2011-03-26 14:10:41 +01:00
// normalize but don't do any lexing or anything
2020-11-21 23:26:32 +01:00
incorporateUserChanges ( ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
editorInfo . ace _fastIncorp = fastIncorp ;
2021-01-14 11:00:14 +01:00
const idleWorkTimer = makeIdleAction ( ( ) => {
2020-11-23 19:24:19 +01:00
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 {
2020-11-21 23:26:32 +01:00
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 ;
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
2021-01-14 11:00:14 +01:00
const 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 ++ } ` ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ;
2011-03-26 14:10:41 +01:00
// tokenFunc function; accesses current value of lineEntry and curDocChar,
// also mutates curDocChar
2021-01-14 11:00:14 +01:00
const tokenFunc = ( tokenText , tokenClass ) => {
2020-11-23 19:24:19 +01:00
lineEntry . domInfo . appendSpan ( tokenText , tokenClass ) ;
} ;
2011-03-26 14:10:41 +01:00
2020-11-21 23:26:32 +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
lineEntry . domInfo . clearSpans ( ) ;
getSpansForLine ( lineEntry , tokenFunc , lineStart ) ;
lineEntry . domInfo . finishUpdate ( ) ;
markNodeClean ( lineEntry . lineNode ) ;
2021-01-14 11:00:14 +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
}
2021-01-14 11:00:14 +01:00
if ( firstLine == null ) firstLine = lineIndex ;
2011-03-26 14:10:41 +01:00
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 ;
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
// 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
2021-01-14 11:00:14 +01:00
const 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 ;
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 {
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 , '' ) ;
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let observedChanges ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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
} ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
clearObservedChanges ( ) ;
2021-01-14 11:00:14 +01:00
const getCleanNodeByKey = ( key ) => {
2021-01-18 01:10:26 +01:00
const p = PROFILER ( 'getCleanNodeByKey' , false ) ; // eslint-disable-line new-cap
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 ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ;
2021-01-14 11:00:14 +01:00
const prevSib = cleanNode . previousSibling ;
const nextSib = cleanNode . nextSibling ;
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 ) ;
2021-01-14 11:00:14 +01:00
const prevSib = cleanNode . previousSibling ;
const 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 ) ;
2021-01-14 11:00:14 +01:00
if ( actualPrevKey !== repPrevKey || actualNextKey !== repNextKey ) {
2020-11-23 19:24:19 +01:00
observedChanges . cleanNodesNearChanges [ ` $ ${ uniqueId ( cleanNode ) } ` ] = true ;
2011-03-26 14:10:41 +01:00
}
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const observeChangesAroundSelection = ( ) => {
2011-03-26 14:10:41 +01:00
if ( currentCallStack . observedSelection ) return ;
currentCallStack . observedSelection = true ;
2021-01-18 01:10:26 +01:00
const p = PROFILER ( 'getSelection' , false ) ; // eslint-disable-line new-cap
2020-11-23 19:24:19 +01:00
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 ) ;
2021-01-14 11:00:14 +01:00
if ( node2 && node1 !== node2 ) {
2011-07-07 19:59:34 +02:00
observeChangesAroundNode ( node2 ) ;
2011-03-26 14:10:41 +01:00
}
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ] ) ;
2021-01-14 11:00:14 +01:00
if ( n && n . parentNode === root ) {
2011-07-07 19:59:34 +02:00
observeChangesAroundNode ( n ) ;
}
2011-03-26 14:10:41 +01:00
}
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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
2021-01-18 01:10:26 +01:00
const p = PROFILER ( 'incorp' , false ) ; // eslint-disable-line new-cap
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 ;
2021-01-14 11:00:14 +01:00
let scrollToTheLeftNeeded = false ;
2020-11-23 19:24:19 +01:00
while ( j < dirtyRanges . length ) {
2011-03-26 14:10:41 +01:00
a = dirtyRanges [ j ] [ 0 ] ;
b = dirtyRanges [ j ] [ 1 ] ;
2021-01-14 11:00:14 +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 ;
2021-01-14 11:00:14 +01:00
for ( let k = 0 ; k < numBodyNodes ; k ++ ) {
2020-11-23 19:24:19 +01:00
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 ] ;
2021-01-14 11:00:14 +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 ) ;
2021-01-14 11:00:14 +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 = [ ] ;
2021-01-14 11:00:14 +01:00
for ( let n = firstDirtyNode ; n &&
! ( n . previousSibling && n . previousSibling === lastDirtyNode ) ;
2020-11-23 19:24:19 +01:00
n = n . nextSibling ) {
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 ;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
if ( linesWrapped > 0 ) {
2020-12-19 00:13:02 +01:00
// Chrome decides in its infinite wisdom that it's okay to put the browser's 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 browser's visible area
// to the left hand side of the span. Firefox isn't quite so bad, but it's still pretty
// quirky.
2021-01-14 11:00:14 +01:00
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 ) ;
2021-01-14 11:00:14 +01:00
for ( let k = 0 ; k < lines . length ; k ++ ) {
2020-11-23 19:24:19 +01:00
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 ] ) ;
2021-01-18 01:06:35 +01:00
dirtyNodes . forEach ( ( 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' ) ;
2021-01-18 01:06:35 +01:00
splicesToDo . forEach ( ( 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' ) ;
2021-01-18 01:06:35 +01:00
domInsertsNeeded . forEach ( ( ins ) => {
2020-11-21 23:26:32 +01:00
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
2021-01-18 01:06:35 +01:00
toDeleteAtEnd . forEach ( ( n ) => {
2020-11-23 19:24:19 +01:00
// 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
} ) ;
2021-01-14 11:00:14 +01:00
// needed to stop chrome from breaking the ui when long strings without spaces are pasted
if ( scrollToTheLeftNeeded ) {
2020-11-23 19:24:19 +01:00
$ ( '#innerdocbody' ) . scrollLeft ( 0 ) ;
2013-02-17 22:03:19 +01:00
}
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' , {
2012-09-08 20:45:33 +02:00
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
} ) ;
2021-01-14 11:00:14 +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' , {
2012-09-08 20:45:33 +02:00
callstack : currentCallStack ,
2020-11-23 19:24:19 +01:00
editorInfo ,
rep ,
root ,
point : selection . endPoint ,
documentAttributeManager ,
2012-09-08 20:45:33 +02:00
} ) ;
2021-01-14 11:00:14 +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 ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const STYLE _ATTRIBS = {
2011-07-07 19:59:34 +02:00
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
2021-01-14 11:00:14 +01:00
const isStyleAttribute = ( aname ) => ! ! STYLE _ATTRIBS [ aname ] ;
2011-07-07 19:59:34 +02:00
2021-01-18 01:10:26 +01:00
const isDefaultLineAttribute =
( aname ) => AttributeManager . DEFAULT _LINE _ATTRIBUTES . indexOf ( aname ) !== - 1 ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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
2021-01-18 01:06:35 +01:00
infoStructs . forEach ( ( info ) => {
2021-01-18 01:10:26 +01:00
const p2 = PROFILER ( 'insertLine' , false ) ; // eslint-disable-line new-cap
2020-11-23 19:24:19 +01:00
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 ) ;
2021-01-14 11:00:14 +01:00
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 ) ;
2020-11-21 23:26:32 +01:00
} , 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 ( ) ;
} ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const isCaret = ( ) => (
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
2021-01-14 11:00:14 +01:00
const caretLine = ( ) => rep . selStart [ 0 ] ;
2012-09-08 20:45:33 +02:00
editorInfo . ace _caretLine = caretLine ;
2013-06-14 19:37:41 +02:00
2021-01-14 11:00:14 +01:00
const caretColumn = ( ) => rep . selStart [ 1 ] ;
2012-09-08 20:45:33 +02:00
editorInfo . ace _caretColumn = caretColumn ;
2013-06-14 19:37:41 +02:00
2021-01-14 11:00:14 +01:00
const caretDocChar = ( ) => rep . lines . offsetOfIndex ( caretLine ( ) ) + caretColumn ( ) ;
2012-09-08 20:45:33 +02:00
editorInfo . ace _caretDocChar = caretDocChar ;
2013-06-14 19:37:41 +02:00
2021-01-14 11:00:14 +01:00
const 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 ;
2021-01-14 11:00:14 +01:00
if ( shouldIndent && /[[(:{]\s*$/ . exec ( prevLineText ) ) {
2015-10-13 23:39:23 +02:00
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 ] ) ;
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const getPointForLineAndChar = ( lineAndChar ) => {
2020-11-23 19:24:19 +01:00
const line = lineAndChar [ 0 ] ;
let charsLeft = lineAndChar [ 1 ] ;
2020-05-29 13:56:03 +02:00
// 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 ) {
2011-07-07 19:59:34 +02:00
return {
node : lineNode ,
2021-01-14 11:00:14 +01:00
index : 0 ,
2020-11-23 19:24:19 +01:00
maxIndex : 1 ,
2011-07-07 19:59:34 +02:00
} ;
}
2021-01-14 11:00:14 +01:00
while ( ! ( n === lineNode && after ) ) {
2020-11-23 19:24:19 +01:00
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
} ;
2021-01-14 11:00:14 +01:00
} ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const nodeText = ( n ) => n . textContent || n . nodeValue || '' ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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.
2021-01-14 11:00:14 +01:00
if ( point . node === root ) {
2020-11-23 19:24:19 +01:00
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 ;
2021-01-14 11:00:14 +01:00
while ( ( parNode = n . parentNode ) !== root ) {
2020-11-23 19:24:19 +01:00
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 ] ;
}
2021-01-14 11:00:14 +01:00
} ;
2012-03-27 22:24:16 +02:00
editorInfo . ace _getLineAndCharForPoint = getLineAndCharForPoint ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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
} ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const performDocumentApplyChangeset = ( changes , insertsAfterSelection ) => {
const 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
}
2021-01-18 01:06:35 +01:00
const lineEntries = newLineStrings . map ( 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
2021-01-18 01:06:35 +01:00
insertDomLines ( nodeToAddAfter , lineEntries . map ( ( entry ) => entry . domInfo ) ) ;
2011-03-26 14:10:41 +01:00
2021-01-18 01:06:35 +01:00
keysToDelete . forEach ( ( k ) => {
2020-11-23 19:24:19 +01:00
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
} ) ;
2021-01-14 11:00:14 +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
}
2021-01-14 11:00:14 +01:00
} ;
doRepApplyChangeset ( changes , insertsAfterSelection ) ;
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 ] ;
2021-01-18 01:10:26 +01:00
const result =
Changeset . characterRangeFollow ( changes , selStartChar , selEndChar , insertsAfterSelection ) ;
2021-01-14 11:00:14 +01:00
requiredSelectionSetting = [ result [ 0 ] , result [ 1 ] , rep . selFocusAtStart ] ;
2011-03-26 14:10:41 +01:00
}
2021-01-14 11:00:14 +01:00
const linesMutatee = {
// TODO: Rhansen to check usage of args here.
splice : ( start , numRemoved , ... args ) => {
2021-01-18 01:06:35 +01:00
domAndRepSplice ( start , numRemoved , args . map ( ( s ) => s . slice ( 0 , - 1 ) ) ) ;
2021-01-14 11:00:14 +01:00
} ,
get : ( i ) => ` ${ rep . lines . atIndex ( i ) . text } \n ` ,
length : ( ) => rep . lines . length ( ) ,
} ;
Changeset . mutateTextLines ( changes , linesMutatee ) ;
if ( requiredSelectionSetting ) {
performSelectionChange (
lineAndColumnFromChar (
requiredSelectionSetting [ 0 ]
) ,
lineAndColumnFromChar ( requiredSelectionSetting [ 1 ] ) ,
requiredSelectionSetting [ 2 ]
) ;
}
} ;
const doRepApplyChangeset = ( changes , insertsAfterSelection ) => {
2011-03-26 14:10:41 +01:00
Changeset . checkRep ( changes ) ;
2021-01-14 11:00:14 +01:00
if ( Changeset . oldLen ( changes ) !== rep . alltext . length ) {
const errMsg = ` ${ Changeset . oldLen ( changes ) } / ${ rep . alltext . length } ` ;
throw new Error ( ` doRepApplyChangeset length mismatch: ${ errMsg } ` ) ;
}
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
// (function doRecordUndoInformation(changes) {
( ( changes ) => {
2020-11-23 19:24:19 +01:00
const editEvent = currentCallStack . editEvent ;
2021-01-14 11:00:14 +01:00
if ( editEvent . eventType === 'nonundoable' ) {
2020-11-23 19:24:19 +01:00
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 , {
2021-01-14 11:00:14 +01:00
get : ( i ) => ` ${ rep . lines . atIndex ( i ) . text } \n ` ,
length : ( ) => rep . lines . length ( ) ,
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 ) ;
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2012-04-05 15:25:17 +02:00
/ *
Converts the position of a char ( index in String ) into a [ row , col ] tuple
* /
2021-01-14 11:00:14 +01:00
const 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 ] ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const performDocumentReplaceCharRange = ( startChar , endChar , newText ) => {
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.
2021-01-14 11:00:14 +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
}
}
2021-01-14 11:00:14 +01:00
performDocumentReplaceRange ( lineAndColumnFromChar ( startChar ) ,
lineAndColumnFromChar ( endChar ) , newText ) ;
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const performDocumentApplyAttributesToCharRange = ( start , end , attribs ) => {
end = Math . min ( end , rep . alltext . length - 1 ) ;
2021-01-18 01:10:26 +01:00
documentAttributeManager . setAttributesOnRange (
lineAndColumnFromChar ( start ) , lineAndColumnFromChar ( end ) , attribs ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
editorInfo . ace _performDocumentApplyAttributesToCharRange =
performDocumentApplyAttributesToCharRange ;
2013-06-14 19:37:41 +02:00
2021-01-14 11:00:14 +01:00
const setAttributeOnSelection = ( attributeName , attributeValue ) => {
2011-03-26 14:10:41 +01:00
if ( ! ( rep . selStart && rep . selEnd ) ) return ;
2012-04-05 15:22:22 +02:00
documentAttributeManager . setAttributesOnRange ( rep . selStart , rep . selEnd , [
2020-11-23 19:24:19 +01:00
[ attributeName , attributeValue ] ,
2011-07-07 19:59:34 +02:00
] ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
editorInfo . ace _setAttributeOnSelection = setAttributeOnSelection ;
2021-01-14 11:00:14 +01:00
const getAttributeOnSelection = ( attributeName , prevChar ) => {
2020-11-23 19:24:19 +01:00
if ( ! ( rep . selStart && rep . selEnd ) ) return ;
2021-01-14 11:00:14 +01:00
const isNotSelection = ( rep . selStart [ 0 ] === rep . selEnd [ 0 ] && rep . selEnd [ 1 ] === rep . selStart [ 1 ] ) ;
2020-11-23 19:24:19 +01:00
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 ] -- ;
}
}
}
2015-10-13 23:39:23 +02:00
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 , '\\*' ) } ( \\ *| $ ) ` ) ;
2021-01-14 11:00:14 +01:00
const hasIt = ( attribs ) => withItRegex . test ( attribs ) ;
2015-10-13 23:39:23 +02:00
2021-01-14 11:00:14 +01:00
const rangeHasAttrib = ( selStart , selEnd ) => {
2014-12-31 19:23:09 +01:00
// if range is collapsed -> no attribs in range
2021-01-14 11:00:14 +01:00
if ( selStart [ 1 ] === selEnd [ 1 ] && selStart [ 0 ] === selEnd [ 0 ] ) return false ;
2015-10-13 23:39:23 +02:00
2021-01-14 11:00:14 +01:00
if ( selStart [ 0 ] !== selEnd [ 0 ] ) { // -> More than one line selected
let hasAttrib = true ;
2015-10-13 23:39:23 +02:00
2014-12-31 19:23:09 +01:00
// from selStart to the end of the first line
2021-01-14 11:00:14 +01:00
hasAttrib = hasAttrib &&
rangeHasAttrib ( selStart , [ selStart [ 0 ] , rep . lines . atIndex ( selStart [ 0 ] ) . text . length ] ) ;
2014-12-31 19:23:09 +01:00
// 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 ] ) ;
2014-12-31 19:23:09 +01:00
}
// for the last, potentially partial, line
2020-11-23 19:24:19 +01:00
hasAttrib = hasAttrib && rangeHasAttrib ( [ selEnd [ 0 ] , 0 ] , [ selEnd [ 0 ] , selEnd [ 1 ] ] ) ;
2015-10-13 23:39:23 +02:00
2020-11-23 19:24:19 +01:00
return hasAttrib ;
2014-12-31 19:23:09 +01:00
}
2015-10-13 23:39:23 +02:00
2014-12-31 19:23:09 +01:00
// Logic tells us we now have a range on a single line
2015-10-13 23:39:23 +02:00
2020-11-23 19:24:19 +01:00
const lineNum = selStart [ 0 ] ;
const start = selStart [ 1 ] ;
const end = selEnd [ 1 ] ;
2021-01-14 11:00:14 +01:00
let hasAttrib = true ;
2015-10-13 23:39:23 +02:00
2014-12-31 19:23:09 +01:00
// Iterate over attribs on this line
2015-10-13 23:39:23 +02:00
2020-11-23 19:24:19 +01:00
const opIter = Changeset . opIterator ( rep . alines [ lineNum ] ) ;
let indexIntoLine = 0 ;
2015-10-13 23:39:23 +02:00
2014-12-31 19:23:09 +01:00
while ( opIter . hasNext ( ) ) {
2020-11-23 19:24:19 +01:00
const op = opIter . next ( ) ;
const opStartInLine = indexIntoLine ;
const opEndInLine = opStartInLine + op . chars ;
2014-12-31 19:23:09 +01:00
if ( ! hasIt ( op . attribs ) ) {
2013-11-28 18:27:52 +01:00
// does op overlap selection?
2014-12-31 19:23:09 +01:00
if ( ! ( opEndInLine <= start || opStartInLine >= end ) ) {
2021-01-14 11:00:14 +01:00
// since it's overlapping but hasn't got the attrib -> range hasn't got it
hasAttrib = false ;
2013-11-28 18:27:52 +01:00
break ;
}
}
indexIntoLine = opEndInLine ;
}
2015-10-13 23:39:23 +02:00
2020-11-23 19:24:19 +01:00
return hasAttrib ;
2021-01-14 11:00:14 +01:00
} ;
return rangeHasAttrib ( rep . selStart , rep . selEnd ) ;
} ;
2015-10-13 23:39:23 +02:00
2013-11-28 18:27:52 +01:00
editorInfo . ace _getAttributeOnSelection = getAttributeOnSelection ;
2021-01-14 11:00:14 +01:00
const 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
2021-01-14 11:00:14 +01:00
const hasIt = ( attribs ) => 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 ;
2015-09-08 16:55:36 +02:00
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
2021-01-14 11:00:14 +01:00
if ( n === selStartLine ) {
2011-07-07 19:59:34 +02:00
selectionStartInLine = rep . selStart [ 1 ] ;
}
2021-01-14 11:00:14 +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
}
}
2018-01-04 15:28:00 +01:00
2020-11-23 19:24:19 +01:00
const attributeValue = selectionAllHasIt ? '' : 'true' ;
2021-01-14 11:00:14 +01:00
documentAttributeManager . setAttributesOnRange (
rep . selStart ,
rep . selEnd ,
[ [ attributeName , attributeValue ] ]
) ;
2018-01-04 15:28:00 +01:00
if ( attribIsFormattingStyle ( attributeName ) ) {
updateStyleButtonState ( attributeName , ! selectionAllHasIt ) ; // italic, bold, ...
2011-03-26 14:10:41 +01:00
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
editorInfo . ace _toggleAttributeOnSelection = toggleAttributeOnSelection ;
2021-01-14 11:00:14 +01:00
const performDocumentReplaceSelection = ( newText ) => {
2011-03-26 14:10:41 +01:00
if ( ! ( rep . selStart && rep . selEnd ) ) return ;
performDocumentReplaceRange ( rep . selStart , rep . selEnd , newText ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
// Change the abstract representation of the document to have a different set of lines.
// Must be called after rep.alltext is set.
2021-01-14 11:00:14 +01:00
const doRepLineSplice = ( startLine , deleteCount , newLineEntries ) => {
2021-01-18 01:06:35 +01:00
newLineEntries . forEach ( ( 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
rep . lines . splice ( startLine , deleteCount , newLineEntries ) ;
currentCallStack . docTextChanged = true ;
currentCallStack . repChanged = true ;
2021-01-18 01:06:35 +01:00
const newText = newLineEntries . map ( ( e ) => ` ${ e . text } \n ` ) . join ( '' ) ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
rep . alltext = rep . alltext . substring ( 0 , startOldChar ) +
newText + rep . alltext . substring ( endOldChar , rep . alltext . length ) ;
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ) {
2021-01-14 11:00:14 +01: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
}
2021-01-18 01:06:35 +01:00
const newText = newLineEntries . map ( ( e ) => ` ${ e . text } \n ` ) . join ( '' ) ;
2020-11-23 19:24:19 +01:00
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
2021-01-14 11:00:14 +01:00
const analysis = analyzeChange (
oldText , newText , oldAttribs , newAttribs , selStartHintChar , selEndHintChar
) ;
2020-11-23 19:24:19 +01:00
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
2021-01-14 11:00:14 +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 ++ ;
}
2021-01-14 11:00:14 +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 ;
}
2021-01-14 11:00:14 +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
2021-01-14 11:00:14 +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
2021-01-14 11:00:14 +01:00
const startBuilder = ( ) => {
2020-11-23 19:24:19 +01:00
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
2021-01-14 11:00:14 +01:00
const eachAttribRun = ( attribs , func /* (startInNewText, endInNewText, attribs)*/ ) => {
2020-11-23 19:24:19 +01:00
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
2021-01-14 11:00:14 +01:00
const justApplyStyles = ( shortNewText === shortOldText ) ;
2020-11-23 19:24:19 +01:00
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".
2021-01-14 11:00:14 +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 ) ;
2021-01-14 11:00:14 +01:00
if ( a && a !== foundDomAuthor ) {
2020-11-23 19:24:19 +01:00
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 ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const cachedStrFunc = ( func ) => {
2020-11-23 19:24:19 +01:00
const cache = { } ;
2021-01-14 11:00:14 +01:00
return ( s ) => {
2020-11-23 19:24:19 +01:00
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 ] ;
} ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const analyzeChange = (
oldText , newText , oldAttribs , newAttribs , optSelStartHint , optSelEndHint ) => {
2018-07-09 22:44:38 +02:00
// 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
2021-01-14 11:00:14 +01:00
const incorpedAttribFilter = ( anum ) => ! isDefaultLineAttribute ( rep . apool . getAttribKey ( anum ) ) ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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 ] ;
2021-01-14 11:00:14 +01:00
} ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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 ;
2021-01-14 11:00:14 +01:00
const 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
} ;
2021-01-14 11:00:14 +01:00
return next ;
} ;
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 ) {
2021-01-14 11:00:14 +01:00
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 ++ ;
2021-01-14 11:00:14 +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
2021-01-14 11:00:14 +01:00
const minCommonEnd = oldLen - commonStart ;
const 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
2021-01-14 11:00:14 +01:00
const minCommonEnd = newLen - commonStart ;
const 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 ] ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const equalLineAndChars = ( a , b ) => {
2011-03-26 14:10:41 +01:00
if ( ! a ) return ! b ;
if ( ! b ) return ! a ;
2021-01-14 11:00:14 +01:00
return ( a [ 0 ] === b [ 0 ] && a [ 1 ] === b [ 1 ] ) ;
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ;
}
2021-01-14 11:00:14 +01:00
} ;
2012-03-22 18:34:08 +01:00
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
2021-01-14 11:00:14 +01:00
const repSelectionChange = ( selectStart , selectEnd , focusAtStart ) => {
2020-11-23 19:24:19 +01:00
focusAtStart = ! ! focusAtStart ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const newSelFocusAtStart = ( focusAtStart && ( ( ! selectStart ) ||
( ! selectEnd ) ||
( selectStart [ 0 ] !== selectEnd [ 0 ] ) ||
( selectStart [ 1 ] !== selectEnd [ 1 ] ) ) ) ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +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 ;
2018-01-04 15:28:00 +01:00
// select the formatting buttons when there is the style applied on selection
selectFormattingButtonIfLineHasStyleApplied ( rep ) ;
2015-07-11 17:32:19 +02:00
hooks . callAll ( 'aceSelectionChanged' , {
2020-11-23 19:24:19 +01:00
rep ,
2015-10-30 15:13:43 +01:00
callstack : currentCallStack ,
2020-11-23 19:24:19 +01:00
documentAttributeManager ,
2015-07-11 17:32:19 +02:00
} ) ;
2018-01-03 22:57:28 +01:00
// 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 ) {
2021-01-14 11:00:14 +01:00
const isScrollableEvent = ! isPadLoading ( currentCallStack . type ) &&
isScrollableEditEvent ( currentCallStack . type ) ;
2020-11-23 19:24:19 +01:00
const innerHeight = getInnerHeight ( ) ;
2021-01-14 11:00:14 +01:00
scroll . scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary (
rep , isScrollableEvent , innerHeight
) ;
2018-01-03 22:57:28 +01:00
}
2011-03-26 14:10:41 +01:00
return true ;
2020-05-29 13:56:03 +02:00
// 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 ;
2020-05-29 13:56:03 +02:00
// 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);
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const isPadLoading = ( eventType ) => (
eventType === 'setup' ) ||
( eventType === 'setBaseText' ) ||
( eventType === 'importText'
) ;
2018-01-03 22:57:28 +01:00
2021-01-14 11:00:14 +01:00
const updateStyleButtonState = ( attribName , hasStyleOnRepSelection ) => {
2020-11-23 19:24:19 +01:00
const $formattingButton = parent . parent . $ ( ` [data-key=" ${ attribName } "] ` ) . find ( 'a' ) ;
2018-01-04 15:28:00 +01:00
$formattingButton . toggleClass ( SELECT _BUTTON _CLASS , hasStyleOnRepSelection ) ;
2021-01-14 11:00:14 +01:00
} ;
2018-01-04 15:28:00 +01:00
2021-01-18 01:06:35 +01:00
const attribIsFormattingStyle = ( attribName ) => FORMATTING _STYLES . indexOf ( attribName ) !== - 1 ;
2018-01-04 15:28:00 +01:00
2021-01-14 11:00:14 +01:00
const selectFormattingButtonIfLineHasStyleApplied = ( rep ) => {
2021-01-18 01:06:35 +01:00
FORMATTING _STYLES . forEach ( ( style ) => {
2021-01-18 01:10:26 +01:00
const hasStyleOnRepSelection =
documentAttributeManager . hasAttributeOnSelectionOrCaretPosition ( style ) ;
2018-01-04 15:28:00 +01:00
updateStyleButtonState ( style , hasStyleOnRepSelection ) ;
2020-11-23 19:24:19 +01:00
} ) ;
2021-01-14 11:00:14 +01:00
} ;
2018-01-04 15:28:00 +01:00
2021-01-14 11:00:14 +01:00
const doCreateDomLine = ( nonEmpty ) => domline . createDomLine ( nonEmpty , doesWrap , browser , doc ) ;
2011-03-26 14:10:41 +01:00
2021-01-18 01:10:26 +01:00
const textify =
( str ) => str . replace ( /[\n\r ]/g , ' ' ) . replace ( /\xa0/g , ' ' ) . replace ( /\t/g , ' ' ) ;
2011-03-26 14:10:41 +01:00
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
} ;
2021-01-18 01:06:35 +01:00
hooks . callAll ( 'aceRegisterBlockElements' ) . forEach ( ( element ) => {
2020-11-23 19:24:19 +01:00
_blockElems [ element ] = 1 ;
2012-04-07 02:13:26 +02:00
} ) ;
2012-04-07 01:02:50 +02:00
2021-01-14 11:00:14 +01:00
const isBlockElement = ( n ) => ! ! _blockElems [ ( n . tagName || '' ) . toLowerCase ( ) ] ;
editorInfo . ace _isBlockElement = isBlockElement ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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.
2021-01-18 01:10:26 +01:00
const p = PROFILER ( 'getDirtyRanges' , false ) ; // eslint-disable-line new-cap
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
2021-01-14 11:00:14 +01:00
const 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 ] ;
2021-01-14 11:00:14 +01:00
} ;
2020-11-23 19:24:19 +01:00
const isConsecutiveCache = { } ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const isConsecutive = ( i ) => {
2020-11-23 19:24:19 +01:00
if ( isConsecutiveCache [ i ] === undefined ) {
2011-07-07 19:59:34 +02:00
p . consecutives ++ ;
2021-01-14 11:00:14 +01:00
isConsecutiveCache [ i ] = ( ( ) => {
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 ;
2021-01-14 11:00:14 +01:00
return a . nextSibling === b ;
2011-07-07 19:59:34 +02:00
} ) ( ) ;
2011-03-26 14:10:41 +01:00
}
return isConsecutiveCache [ i ] ;
2021-01-14 11:00:14 +01:00
} ;
// returns whether line (i) in the un-updated representation maps to a clean node,
// or is outside the bounds of the document
const isClean = ( i ) => ! ! cleanNodeForIndex ( i ) ;
2011-07-07 19:59:34 +02:00
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
] ;
2021-01-14 11:00:14 +01:00
const 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 ;
2021-01-18 01:06:35 +01:00
cleanRanges . forEach ( ( 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 ;
2021-01-14 11:00:14 +01:00
} ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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 ] ;
2021-01-14 11:00:14 +01:00
if ( ( a + 1 ) === b ) cleanRanges . splice ( rng , 1 ) ;
else if ( line === a ) cleanRanges [ rng ] [ 0 ] ++ ;
else if ( line === ( b - 1 ) ) cleanRanges [ rng ] [ 1 ] -- ;
2011-07-07 19:59:34 +02:00
else cleanRanges . splice ( rng , 1 , [ a , line ] , [ line + 1 , b ] ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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 ] ) ;
2021-01-14 11:00:14 +01:00
} ;
2020-11-23 19:24:19 +01:00
const correctedLines = { } ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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 ) {
2020-05-29 13:56:03 +02:00
// 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 ;
}
2021-01-14 11:00:14 +01:00
} ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
const 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
}
2021-01-14 11:00:14 +01:00
} ;
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 ) {
2021-01-14 11:00:14 +01:00
if ( observedChanges . cleanNodesNearChanges [ k ] ) {
const key = k . substring ( 1 ) ;
if ( rep . lines . containsKey ( key ) ) {
const line = rep . lines . indexOfKey ( key ) ;
detectChangesAroundLine ( line , 2 ) ;
}
2011-07-07 19:59:34 +02:00
}
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 ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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
setAssoc ( n , 'dirtiness' , dirtiness ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const isNodeDirty = ( n ) => {
2021-01-18 01:10:26 +01:00
const p = PROFILER ( 'cleanCheck' , false ) ; // eslint-disable-line new-cap
2021-01-14 11:00:14 +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 ;
if ( n . innerHTML !== data . knownHTML ) return true ;
p . end ( ) ;
return false ;
2021-01-14 11:00:14 +01:00
} ;
2015-02-27 18:54:29 +01:00
2021-01-14 11:00:14 +01:00
const handleClick = ( evt ) => {
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary ( 'handleClick' , ( ) => {
2011-03-26 14:10:41 +01:00
idleWorkTimer . atMost ( 200 ) ;
} ) ;
2021-01-14 11:00:14 +01:00
const isLink = ( n ) => ( n . tagName || '' ) . toLowerCase ( ) === 'a' && n . href ;
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
// only want to catch left-click
2021-01-14 11:00:14 +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 {
2020-03-31 10:02:46 +02:00
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
}
}
2017-05-04 16:22:18 +02:00
hideEditBarDropdowns ( ) ;
2021-01-14 11:00:14 +01:00
} ;
2017-05-04 16:22:18 +02:00
2021-01-14 11:00:14 +01:00
const 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
}
2021-01-14 11:00:14 +01:00
} ;
const renumberList = ( lineNum ) => {
// 1-check we are in a list
let type = getLineListType ( lineNum ) ;
if ( ! type ) {
return null ;
}
type = /([a-z]+)[0-9]+/ . exec ( type ) ;
if ( type [ 1 ] === 'indent' ) {
return null ;
}
// 2-find the first line of the list
while ( lineNum - 1 >= 0 && ( type = getLineListType ( lineNum - 1 ) ) ) {
type = /([a-z]+)[0-9]+/ . exec ( type ) ;
if ( type [ 1 ] === 'indent' ) break ;
lineNum -- ;
}
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +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 ] ;
const applyNumberList = ( line , level ) => {
// init
let position = 1 ;
let curLevel = level ;
let listType ;
// loop over the lines
while ( ( listType = getLineListType ( line ) ) ) {
// apply new num
listType = /([a-z]+)([0-9]+)/ . exec ( listType ) ;
curLevel = Number ( listType [ 2 ] ) ;
if ( isNaN ( curLevel ) || listType [ 0 ] === 'indent' ) {
return line ;
} else if ( curLevel === level ) {
ChangesetUtils . buildKeepRange ( rep , builder , loc , ( loc = [ line , 0 ] ) ) ;
ChangesetUtils . buildKeepRange ( rep , builder , loc , ( loc = [ line , 1 ] ) , [
[ 'start' , position ] ,
] , rep . apool ) ;
position ++ ;
line ++ ;
} else if ( curLevel < level ) {
return line ; // back to parent
} else {
line = applyNumberList ( line , level + 1 ) ; // recursive call
}
}
return line ;
} ;
applyNumberList ( lineNum , 1 ) ;
const cs = builder . toString ( ) ;
if ( ! Changeset . isIdentity ( cs ) ) {
performDocumentApplyChangeset ( cs ) ;
}
// 4-apply the modifications
} ;
editorInfo . ace _renumberList = renumberList ;
const setLineListType = ( lineNum , listType ) => {
if ( listType === '' ) {
documentAttributeManager . removeAttributeOnLine ( lineNum , listAttributeName ) ;
documentAttributeManager . removeAttributeOnLine ( lineNum , 'start' ) ;
} else {
documentAttributeManager . setAttributeOnLine ( lineNum , listAttributeName , listType ) ;
}
// if the list has been removed, it is necessary to renumber
// starting from the *next* line because the list may have been
// separated. If it returns null, it means that the list was not cut, try
// from the current one.
if ( renumberList ( lineNum + 1 ) == null ) {
renumberList ( lineNum ) ;
}
} ;
const 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 ;
2014-12-27 13:18:58 +01:00
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 ( ) ;
}
2021-01-14 11:00:14 +01:00
} ;
editorInfo . ace _doReturnKey = doReturnKey ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const doIndentOutdent = ( isOut ) => {
2013-02-18 02:40:34 +01:00
if ( ! ( ( rep . selStart && rep . selEnd ) ||
2021-01-14 11:00:14 +01:00
( ( rep . selStart [ 0 ] === rep . selEnd [ 0 ] ) &&
( rep . selStart [ 1 ] === rep . selEnd [ 1 ] ) &&
rep . selEnd [ 1 ] > 1 ) ) &&
( isOut !== true )
2020-11-23 19:24:19 +01:00
) {
2011-03-26 14:10:41 +01:00
return false ;
}
2021-01-14 11:00:14 +01:00
const firstLine = rep . selStart [ 0 ] ;
const 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 ) {
2014-12-27 13:18:58 +01:00
listType = /([a-z]+)([0-9]+)/ . exec ( listType ) ;
2020-11-23 19:24:19 +01:00
if ( listType ) {
2011-11-25 10:29:31 +01:00
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 ) ) ) ;
2021-01-14 11:00:14 +01:00
if ( level !== newLevel ) {
2011-11-25 10:29:31 +01:00
mods . push ( [ n , ( newLevel > 0 ) ? t + newLevel : '' ] ) ;
}
2011-03-26 14:10:41 +01:00
}
2021-01-18 01:06:35 +01:00
mods . forEach ( ( mod ) => {
2012-04-05 01:07:47 +02:00
setLineListType ( mod [ 0 ] , mod [ 1 ] ) ;
2012-04-05 00:50:04 +02:00
} ) ;
2011-11-25 10:29:31 +01:00
return true ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
editorInfo . ace _doIndentOutdent = doIndentOutdent ;
2021-01-14 11:00:14 +01:00
const doTabKey = ( shiftDown ) => {
2020-11-23 19:24:19 +01:00
if ( ! doIndentOutdent ( shiftDown ) ) {
2011-03-26 14:10:41 +01:00
performDocumentReplaceSelection ( THE _TAB ) ;
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ( ) ;
2021-01-14 11:00:14 +01:00
const 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 ( ) ;
2021-01-14 11:00:14 +01:00
const 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 prevLineListType = ( theLine > 0 ? getLineListType ( theLine - 1 ) : '' ) ;
const thisLineListType = getLineListType ( theLine ) ;
const prevLineEntry = ( theLine > 0 && rep . lines . atIndex ( theLine - 1 ) ) ;
2021-01-14 11:00:14 +01:00
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
2021-01-14 11:00:14 +01:00
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 ) {
2012-04-07 01:07:25 +02:00
// If the line has any attributes assigned, remove them by removing the marker '*'
2021-01-14 11:00:14 +01:00
performDocumentReplaceRange (
[ theLine - 1 , prevLineEntry . text . length ] ,
[ theLine , lineEntry . lineMarker ] , ''
) ;
2020-11-23 19:24:19 +01:00
} else if ( theLine > 0 ) {
2011-03-26 14:10:41 +01:00
// remove newline
2021-01-14 11:00:14 +01: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 ;
2021-01-14 11:00:14 +01:00
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 ( ) ;
2021-01-14 11:00:14 +01:00
if ( line !== - 1 && renumberList ( line + 1 ) == null ) {
2012-01-15 18:20:20 +01:00
renumberList ( line ) ;
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2020-12-11 21:55:13 +01:00
const isWordChar = ( c ) => padutils . wordCharRegex . test ( c ) ;
2012-09-08 20:45:33 +02:00
editorInfo . ace _isWordChar = isWordChar ;
2013-06-14 19:37:41 +02:00
2021-01-14 11:00:14 +01:00
const 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
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.
2021-01-14 11:00:14 +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 ;
2021-01-14 11:00:14 +01:00
// If the key is a keypress and the browser is opera and the key is enter,
// do nothign at all as this fires twice.
if ( keyCode === 13 && browser . opera && ( type === 'keypress' ) ) {
// 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
return ;
2013-02-12 20:04:43 +01:00
}
2020-11-23 19:24:19 +01:00
let specialHandled = false ;
2021-01-14 11:00:14 +01:00
const isTypeForSpecialKey = ( ( browser . safari ||
browser . chrome ||
browser . firefox ) ? ( type === 'keydown' ) : ( type === 'keypress' ) ) ;
const isTypeForCmdKey = ( ( browser . safari ||
browser . chrome ||
browser . firefox ) ? ( type === 'keydown' ) : ( type === 'keypress' ) ) ;
2020-11-23 19:24:19 +01:00
let stopped = false ;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
inCallStackIfNecessary ( 'handleKeyEvent' , function ( ) {
2021-01-14 11:00:14 +01:00
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' ) {
2015-04-17 18:01:15 +02:00
// If it's a dead key we don't want to do any Etherpad behavior.
stopped = true ;
return true ;
2021-01-14 11:00:14 +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' , {
2012-09-08 20:45:33 +02:00
callstack : currentCallStack ,
2020-11-23 19:24:19 +01:00
editorInfo ,
rep ,
documentAttributeManager ,
evt ,
2012-09-08 20:45:33 +02:00
} ) ;
2015-10-27 10:44:51 +01:00
// if any hook returned true, set specialHandled with true
if ( specialHandledInHook ) {
2021-01-18 01:06:35 +01:00
specialHandled = specialHandledInHook . indexOf ( true ) !== - 1 ;
2015-10-27 10:44:51 +01:00
}
2020-11-23 19:24:19 +01:00
const padShortcutEnabled = parent . parent . clientVars . padShortcutEnabled ;
2021-01-14 11:00:14 +01:00
if (
( ! specialHandled ) &&
altKey &&
isTypeForSpecialKey &&
keyCode === 120 &&
padShortcutEnabled . altF9
) {
2015-03-25 12:03:45 +01:00
// 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....
2021-01-14 11:00:14 +01:00
// Focus on the editbar.
// -- TODO: Move Focus back to previous state (we know it so we can use it)
const firstEditbarElement = parent . parent . $ ( '#editbar' )
. children ( 'ul' ) . first ( ) . children ( ) . first ( )
. children ( ) . first ( ) . children ( ) . first ( ) ;
2015-10-13 23:39:23 +02:00
$ ( this ) . blur ( ) ;
2015-03-25 16:49:41 +01:00
firstEditbarElement . focus ( ) ;
evt . preventDefault ( ) ;
2015-03-25 12:03:45 +01:00
}
2021-01-14 11:00:14 +01:00
if (
( ! specialHandled ) &&
altKey && keyCode === 67 &&
type === 'keydown' &&
padShortcutEnabled . altC
) {
2015-03-26 17:44:22 +01:00
// Alt c focuses on the Chat window
2015-10-13 23:39:23 +02:00
$ ( this ) . blur ( ) ;
2015-03-26 17:44:22 +01:00
parent . parent . chat . show ( ) ;
2020-11-23 19:24:19 +01:00
parent . parent . $ ( '#chatinput' ) . focus ( ) ;
2015-03-26 17:44:22 +01:00
evt . preventDefault ( ) ;
}
2021-01-14 11:00:14 +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 ;
2015-03-31 17:12:05 +02:00
// 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.
2021-01-14 11:00:14 +01:00
const authors = [ ] ;
2020-11-23 19:24:19 +01:00
let author = null ;
2015-03-31 17:12:05 +02:00
if ( alineAttrs ) {
2020-11-23 19:24:19 +01:00
const opIter = Changeset . opIterator ( alineAttrs ) ;
2015-03-31 21:26:55 +02:00
2020-11-23 19:24:19 +01:00
while ( opIter . hasNext ( ) ) {
const op = opIter . next ( ) ;
2021-01-14 11:00:14 +01:00
const authorId = Changeset . opAttributeValue ( op , 'author' , apool ) ;
2015-03-31 21:26:55 +02:00
// Only push unique authors and ones with values
2020-11-23 19:24:19 +01:00
if ( authors . indexOf ( authorId ) === - 1 && authorId !== '' ) {
2015-03-31 21:26:55 +02:00
authors . push ( authorId ) ;
}
2015-03-31 17:12:05 +02:00
}
2015-03-31 21:26:55 +02:00
}
2021-01-14 11:00:14 +01:00
let authorString ;
const authorNames = [ ] ;
2020-11-23 19:24:19 +01:00
if ( authors . length === 0 ) {
2021-01-14 11:00:14 +01:00
authorString = 'No author information is available' ;
2020-11-23 19:24:19 +01:00
} else {
2015-03-31 21:26:55 +02:00
// 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 ) => {
2015-03-31 21:26:55 +02:00
// 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 ) {
2015-03-31 21:26:55 +02:00
authorObj = {
2020-11-23 19:24:19 +01:00
name : 'Me' ,
} ;
} else {
2015-03-31 21:26:55 +02:00
authorObj = padAuthor ;
}
}
} ) ;
2020-11-23 19:24:19 +01:00
if ( ! authorObj ) {
author = 'Unknown' ;
2015-03-31 21:26:55 +02:00
return ;
}
author = authorObj . name ;
2020-11-23 19:24:19 +01:00
if ( ! author ) author = 'Unknown' ;
2015-03-31 21:26:55 +02:00
authorNames . push ( author ) ;
2020-11-23 19:24:19 +01:00
} ) ;
2015-03-31 17:12:05 +02:00
}
2020-11-23 19:24:19 +01:00
if ( authors . length === 1 ) {
2021-01-14 11:00:14 +01:00
authorString = ` The author of this line is ${ authorNames [ 0 ] } ` ;
2015-03-31 17:12:05 +02:00
}
2020-11-23 19:24:19 +01:00
if ( authors . length > 1 ) {
2021-01-14 11:00:14 +01:00
authorString = ` The authors of this line are ${ authorNames . join ( ' & ' ) } ` ;
}
2015-03-31 17:12:05 +02:00
parent . parent . $ . gritter . add ( {
// (string | mandatory) the heading of the notification
2015-03-31 21:26:55 +02:00
title : 'Line Authors' ,
2015-03-31 17:12:05 +02:00
// (string | mandatory) the text inside the notification
2015-03-31 21:26:55 +02:00
text : authorString ,
2015-03-31 17:12:05 +02:00
// (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' ,
2015-03-31 17:12:05 +02:00
} ) ;
}
2021-01-14 11:00:14 +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
}
2021-01-14 11:00:14 +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 ;
}
2021-01-14 11:00:14 +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 ;
2020-04-20 18:20:35 +02:00
// close all gritters when the user hits escape key
parent . parent . $ . gritter . removeAll ( ) ;
2015-10-29 18:54:21 +01:00
}
2021-01-14 11:00:14 +01:00
if (
( ! specialHandled ) &&
/* Do a saved revision on ctrl S */
isTypeForCmdKey &&
String . fromCharCode ( which ) . toLowerCase ( ) === 's' &&
( evt . metaKey || evt . ctrlKey ) &&
! evt . altKey &&
padShortcutEnabled . cmdS
) {
2012-11-28 18:17:35 +01:00
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 } ) ;
2013-03-28 16:39:33 +01:00
} , 1000 ) ;
2021-01-14 11:00:14 +01:00
/* The parent.parent part of this is BAD and I feel bad.. It may break something */
parent . parent . pad . collabClient . sendMessage ( { type : 'SAVE_REVISION' } ) ;
2012-11-28 18:17:35 +01:00
specialHandled = true ;
}
2021-01-14 11:00:14 +01:00
if ( ( ! specialHandled ) &&
2011-07-07 19:59:34 +02:00
// tab
2021-01-14 11:00:14 +01:00
isTypeForSpecialKey &&
keyCode === 9 &&
! ( evt . metaKey || evt . ctrlKey ) &&
padShortcutEnabled . tab ) {
2011-07-07 19:59:34 +02:00
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 ;
}
2021-01-14 11:00:14 +01:00
if ( ( ! specialHandled ) &&
2014-11-22 20:13:23 +01:00
// cmd-Z (undo)
2021-01-14 11:00:14 +01:00
isTypeForCmdKey &&
String . fromCharCode ( which ) . toLowerCase ( ) === 'z' &&
( evt . metaKey || evt . ctrlKey ) &&
! evt . altKey &&
padShortcutEnabled . cmdZ
) {
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 ;
}
2021-01-14 11:00:14 +01:00
if ( ( ! specialHandled ) &&
// cmd-Y (redo)
isTypeForCmdKey &&
String . fromCharCode ( which ) . toLowerCase ( ) === 'y' &&
( evt . metaKey || evt . ctrlKey ) &&
padShortcutEnabled . cmdY
) {
2011-07-07 19:59:34 +02:00
fastIncorp ( 10 ) ;
evt . preventDefault ( ) ;
2020-11-23 19:24:19 +01:00
doUndoRedo ( 'redo' ) ;
2011-07-07 19:59:34 +02:00
specialHandled = true ;
}
2021-01-14 11:00:14 +01:00
if ( ( ! specialHandled ) &&
2011-07-07 19:59:34 +02:00
// cmd-B (bold)
2021-01-14 11:00:14 +01:00
isTypeForCmdKey &&
String . fromCharCode ( which ) . toLowerCase ( ) === 'b' &&
( evt . metaKey || evt . ctrlKey ) &&
padShortcutEnabled . cmdB ) {
2011-07-07 19:59:34 +02:00
fastIncorp ( 13 ) ;
evt . preventDefault ( ) ;
2011-03-26 14:10:41 +01:00
toggleAttributeOnSelection ( 'bold' ) ;
2011-07-07 19:59:34 +02:00
specialHandled = true ;
}
2021-01-14 11:00:14 +01:00
if ( ( ! specialHandled ) &&
2011-07-07 19:59:34 +02:00
// cmd-I (italic)
2021-01-14 11:00:14 +01:00
isTypeForCmdKey &&
String . fromCharCode ( which ) . toLowerCase ( ) === 'i' &&
( evt . metaKey || evt . ctrlKey ) &&
padShortcutEnabled . cmdI
) {
2011-07-07 19:59:34 +02:00
fastIncorp ( 14 ) ;
evt . preventDefault ( ) ;
2011-03-26 14:10:41 +01:00
toggleAttributeOnSelection ( 'italic' ) ;
2011-07-07 19:59:34 +02:00
specialHandled = true ;
}
2021-01-14 11:00:14 +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 ;
}
2021-01-14 11:00:14 +01:00
if ( ( ! specialHandled ) &&
2014-10-11 19:16:01 +02:00
// cmd-5 (strikethrough)
2021-01-14 11:00:14 +01:00
isTypeForCmdKey &&
String . fromCharCode ( which ) . toLowerCase ( ) === '5' &&
( evt . metaKey || evt . ctrlKey ) &&
evt . altKey !== true &&
padShortcutEnabled . cmd5
) {
2014-10-11 19:16:01 +02:00
fastIncorp ( 13 ) ;
evt . preventDefault ( ) ;
toggleAttributeOnSelection ( 'strikethrough' ) ;
specialHandled = true ;
}
2021-01-14 11:00:14 +01:00
if ( ( ! specialHandled ) &&
2014-10-11 19:47:03 +02:00
// cmd-shift-L (unorderedlist)
2021-01-14 11:00:14 +01:00
isTypeForCmdKey &&
String . fromCharCode ( which ) . toLowerCase ( ) === 'l' &&
( evt . metaKey || evt . ctrlKey ) &&
evt . shiftKey &&
padShortcutEnabled . cmdShiftL
) {
2014-10-11 19:47:03 +02:00
fastIncorp ( 9 ) ;
evt . preventDefault ( ) ;
2020-11-23 19:24:19 +01:00
doInsertUnorderedList ( ) ;
2014-10-11 19:47:03 +02:00
specialHandled = true ;
2020-11-23 19:24:19 +01:00
}
2021-01-14 11:00:14 +01:00
if ( ( ! specialHandled ) &&
2017-05-11 19:56:09 +02:00
// cmd-shift-N and cmd-shift-1 (orderedlist)
2021-01-14 11:00:14 +01:00
isTypeForCmdKey &&
(
( String . fromCharCode ( which ) . toLowerCase ( ) === 'n' &&
padShortcutEnabled . cmdShiftN ) || ( String . fromCharCode ( which ) === '1' &&
padShortcutEnabled . cmdShift1 )
) && ( evt . metaKey || evt . ctrlKey ) &&
evt . shiftKey
) {
2014-10-11 19:47:03 +02:00
fastIncorp ( 9 ) ;
evt . preventDefault ( ) ;
2020-11-23 19:24:19 +01:00
doInsertOrderedList ( ) ;
2014-10-11 19:47:03 +02:00
specialHandled = true ;
2020-11-23 19:24:19 +01:00
}
2021-01-14 11:00:14 +01:00
if ( ( ! specialHandled ) &&
2014-11-07 09:31:32 +01:00
// cmd-shift-C (clearauthorship)
2021-01-14 11:00:14 +01:00
isTypeForCmdKey &&
String . fromCharCode ( which ) . toLowerCase ( ) === 'c' &&
( evt . metaKey || evt . ctrlKey ) &&
evt . shiftKey && padShortcutEnabled . cmdShiftC
) {
2014-11-07 09:31:32 +01:00
fastIncorp ( 9 ) ;
evt . preventDefault ( ) ;
CMDS . clearauthorship ( ) ;
}
2021-01-14 11:00:14 +01:00
if ( ( ! specialHandled ) &&
2011-07-07 19:59:34 +02:00
// cmd-H (backspace)
2021-01-14 11:00:14 +01:00
isTypeForCmdKey &&
String . fromCharCode ( which ) . toLowerCase ( ) === 'h' &&
( evt . ctrlKey ) &&
padShortcutEnabled . cmdH
) {
2011-07-07 19:59:34 +02:00
fastIncorp ( 20 ) ;
evt . preventDefault ( ) ;
2011-03-26 14:10:41 +01:00
doDeleteKey ( ) ;
2011-07-07 19:59:34 +02:00
specialHandled = true ;
}
2021-01-14 11:00:14 +01:00
if ( ( evt . which === 36 && evt . ctrlKey === true ) &&
// Control Home send to Y = 0
padShortcutEnabled . ctrlHome ) {
scroll . setScrollY ( 0 ) ;
}
if ( ( evt . which === 33 || evt . which === 34 ) && type === 'keydown' && ! evt . ctrlKey ) {
// This is required, browsers will try to do normal default behavior on
// page up / down and the default behavior SUCKS
evt . preventDefault ( ) ;
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 ( ( ) => {
2021-01-14 11:00:14 +01:00
// the visible lines IE 1,10
const newVisibleLineRange = scroll . getVisibleLineRange ( rep ) ;
// total count of lines in pad IE 10
const linesCount = rep . lines . length ( ) ;
// How many lines are in the viewport right now?
const numberOfLinesInViewport = newVisibleLineRange [ 1 ] - newVisibleLineRange [ 0 ] ;
2013-02-03 18:39:49 +01:00
2020-11-23 19:24:19 +01:00
if ( isPageUp && padShortcutEnabled . pageUp ) {
2021-01-14 11:00:14 +01:00
// move to the bottom line +1 in the viewport (essentially skipping over a page)
rep . selEnd [ 0 ] -= numberOfLinesInViewport ;
// move to the bottom line +1 in the viewport (essentially skipping over a page)
rep . selStart [ 0 ] -= numberOfLinesInViewport ;
2013-02-03 18:39:49 +01:00
}
2021-01-14 11:00:14 +01:00
// if we hit page down
if ( isPageDown && padShortcutEnabled . pageDown ) {
// If the new viewpoint position is actually further than where we are right now
if ( rep . selEnd [ 0 ] >= oldVisibleLineRange [ 0 ] ) {
// 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 . 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 ;
2013-03-18 18:40:18 +01:00
}
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 ) {
2013-03-18 18:40:18 +01:00
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 ( ) ;
2021-01-14 11:00:14 +01:00
// get the current caret selection, can't use rep. here because that only gives
// us the start position not the current
const myselection = document . getSelection ( ) ;
// get the carets selection offset in px IE 214
let caretOffsetTop = myselection . focusNode . parentNode . offsetTop ||
myselection . focusNode . offsetTop ;
// sometimes the first selection is -1 which causes problems
// (Especially with ep_page_view)
2015-01-18 20:58:38 +01:00
// so use focusNode.offsetTop value.
2020-11-23 19:24:19 +01:00
if ( caretOffsetTop === - 1 ) caretOffsetTop = myselection . focusNode . offsetTop ;
2021-01-14 11:00:14 +01:00
// set the scrollY offset of the viewport on the document
scroll . setScrollY ( caretOffsetTop ) ;
2013-02-03 18:39:49 +01:00
} , 200 ) ;
}
2013-09-25 15:57:02 +02:00
2018-01-03 22:57:28 +01:00
// scroll to viewport when user presses arrow keys and caret is out of the viewport
2021-01-14 11:00:14 +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
2018-01-03 22:57:28 +01:00
// this makes the scroll smooth
2020-11-23 19:24:19 +01:00
if ( ! continuouslyPressingArrowKey ( type ) ) {
2021-01-14 11:00:14 +01:00
// the caret position is not synchronized with the rep.
// For example, when an user presses arrow
// We use getSelection() instead of rep to get the caret position.
// This avoids errors like when down to scroll the pad without releasing the key.
// When the key is released the rep is not
2018-01-03 22:57:28 +01:00
// synchronized, so we don't get the right node where caret is.
2020-11-23 19:24:19 +01:00
const selection = getSelection ( ) ;
2018-01-03 22:57:28 +01:00
2020-11-23 19:24:19 +01:00
if ( selection ) {
const arrowUp = evt . which === 38 ;
const innerHeight = getInnerHeight ( ) ;
2018-01-03 22:57:28 +01:00
scroll . scrollWhenPressArrowKeys ( arrowUp , rep , innerHeight ) ;
2013-02-15 13:24:16 +01:00
}
}
}
2011-03-26 14:10:41 +01:00
}
2021-01-14 11:00:14 +01:00
if ( type === 'keydown' ) {
2011-07-07 19:59:34 +02:00
idleWorkTimer . atLeast ( 500 ) ;
2021-01-14 11:00:14 +01:00
} else if ( type === 'keypress' ) {
// OPINION ASKED. What's going on here? :D
if ( ! specialHandled ) {
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 ) ;
}
2021-01-14 11:00:14 +01:00
} else if ( type === 'keyup' ) {
2020-11-23 19:24:19 +01:00
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
2021-01-14 11:00:14 +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
2021-01-14 11:00:14 +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 ) ) {
2021-01-14 11:00:14 +01:00
if ( type !== 'keyup' ) {
2011-07-07 19:59:34 +02:00
observeChangesAroundSelection ( ) ;
}
2011-03-26 14:10:41 +01:00
}
2021-01-14 11:00:14 +01:00
if ( type === 'keyup' ) {
2011-07-07 19:59:34 +02:00
thisKeyDoesntTriggerNormalize = false ;
2011-03-26 14:10:41 +01:00
}
} ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
let thisKeyDoesntTriggerNormalize = false ;
2020-11-23 19:24:19 +01:00
let arrowKeyWasReleased = true ;
2021-01-14 11:00:14 +01:00
const continuouslyPressingArrowKey = ( type ) => {
2020-11-23 19:24:19 +01:00
let firstTimeKeyIsContinuouslyPressed = false ;
2018-01-03 22:57:28 +01:00
2021-01-14 11:00:14 +01:00
if ( type === 'keyup' ) {
arrowKeyWasReleased = true ;
} else if ( type === 'keydown' && arrowKeyWasReleased ) {
2018-01-03 22:57:28 +01:00
firstTimeKeyIsContinuouslyPressed = true ;
arrowKeyWasReleased = false ;
}
return ! firstTimeKeyIsContinuouslyPressed ;
2021-01-14 11:00:14 +01:00
} ;
2018-01-03 22:57:28 +01:00
2021-01-14 11:00:14 +01:00
const 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 ;
2021-01-14 11:00:14 +01:00
if ( which === 'undo' ) whichMethod = 'performUndo' ;
if ( which === 'redo' ) whichMethod = 'performRedo' ;
2020-11-23 19:24:19 +01:00
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 ) {
2021-01-14 11:00:14 +01:00
performSelectionChange (
lineAndColumnFromChar (
selectionInfo . selStart
) ,
lineAndColumnFromChar ( selectionInfo . selEnd ) ,
selectionInfo . selFocusAtStart
) ;
2011-07-07 19:59:34 +02:00
}
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
}
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
editorInfo . ace _doUndoRedo = doUndoRedo ;
2021-01-14 11:00:14 +01:00
const setSelection = ( selection ) => {
const copyPoint = ( pt ) => ( {
node : pt . node ,
index : pt . index ,
maxIndex : pt . maxIndex ,
} ) ;
let isCollapsed ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const pointToRangeBound = ( pt ) => {
2020-12-19 00:13:02 +01:00
const p = copyPoint ( pt ) ;
// Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level,
// and also problem where cut/copy of a whole line selected with fake arrow-keys
// copies the next line too.
if ( isCollapsed ) {
2021-01-14 11:00:14 +01:00
const diveDeep = ( ) => {
2020-12-19 00:13:02 +01:00
while ( p . node . childNodes . length > 0 ) {
// && (p.node == root || p.node.parentNode == root)) {
if ( p . index === 0 ) {
p . node = p . node . firstChild ;
p . maxIndex = nodeMaxIndex ( p . node ) ;
2021-01-14 11:00:14 +01:00
} else if ( p . index === p . maxIndex ) {
2020-12-19 00:13:02 +01:00
p . node = p . node . lastChild ;
p . maxIndex = nodeMaxIndex ( p . node ) ;
p . index = p . maxIndex ;
} else { break ; }
}
2021-01-14 11:00:14 +01:00
} ;
2020-12-19 00:13:02 +01: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...
2021-01-14 11:00:14 +01:00
if ( isNodeText ( p . node ) && p . index === p . maxIndex ) {
2020-12-19 00:13:02 +01:00
let n = p . node ;
2021-01-14 11:00:14 +01:00
while ( ( ! n . nextSibling ) && ( n !== root ) && ( n . parentNode !== root ) ) {
2020-12-19 00:13:02 +01:00
n = n . parentNode ;
2011-07-07 19:59:34 +02:00
}
2021-01-14 11:00:14 +01:00
if (
n . nextSibling &&
( ! ( ( typeof n . nextSibling . tagName ) === 'string' &&
n . nextSibling . tagName . toLowerCase ( ) === 'br' ) ) &&
( n !== p . node ) && ( n !== root ) && ( n . parentNode !== root )
) {
2020-12-19 00:13:02 +01:00
// found a parent, go to next node and dive in
p . node = n . nextSibling ;
p . maxIndex = nodeMaxIndex ( p . node ) ;
p . index = 0 ;
diveDeep ( ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
}
2020-12-19 00:13:02 +01:00
// try to make sure insertion point is styled;
// also fixes other FF problems
if ( ! isNodeText ( p . node ) ) {
diveDeep ( ) ;
2011-07-07 19:59:34 +02:00
}
}
2020-12-19 00:13:02 +01:00
if ( isNodeText ( p . node ) ) {
return {
container : p . node ,
offset : p . index ,
} ;
2020-11-23 19:24:19 +01:00
} else {
2020-12-19 00:13:02 +01:00
// p.index in {0,1}
return {
container : p . node . parentNode ,
offset : childIndex ( p . node ) + p . index ,
} ;
2011-07-07 19:59:34 +02:00
}
2021-01-14 11:00:14 +01:00
} ;
2020-12-19 00:13:02 +01:00
const browserSelection = window . getSelection ( ) ;
if ( browserSelection ) {
browserSelection . removeAllRanges ( ) ;
if ( selection ) {
2021-01-14 11:00:14 +01:00
isCollapsed = (
selection . startPoint . node === selection . endPoint . node &&
selection . startPoint . index === selection . endPoint . index
) ;
2020-12-19 00:13:02 +01:00
const start = pointToRangeBound ( selection . startPoint ) ;
const end = pointToRangeBound ( selection . endPoint ) ;
2021-01-14 11:00:14 +01:00
if (
( ! isCollapsed ) &&
selection . focusAtStart &&
browserSelection . collapse &&
browserSelection . extend
) {
2020-12-19 00:13:02 +01: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 {
2021-01-14 11:00:14 +01:00
const range = doc . createRange ( ) ;
2020-12-19 00:13:02 +01:00
range . setStart ( start . container , start . offset ) ;
range . setEnd ( end . container , end . offset ) ;
browserSelection . removeAllRanges ( ) ;
browserSelection . addRange ( range ) ;
2011-07-07 19:59:34 +02:00
}
}
}
2021-01-14 11:00:14 +01:00
} ;
const updateBrowserSelectionFromRep = ( ) => {
// requires normalized DOM!
const selStart = rep . selStart ;
const selEnd = rep . selEnd ;
2011-07-07 19:59:34 +02:00
2021-01-14 11:00:14 +01:00
if ( ! ( selStart && selEnd ) ) {
setSelection ( null ) ;
return ;
}
const selection = { } ;
const ss = [ selStart [ 0 ] , selStart [ 1 ] ] ;
selection . startPoint = getPointForLineAndChar ( ss ) ;
const se = [ selEnd [ 0 ] , selEnd [ 1 ] ] ;
selection . endPoint = getPointForLineAndChar ( se ) ;
selection . focusAtStart = ! ! rep . selFocusAtStart ;
setSelection ( selection ) ;
} ;
editorInfo . ace _updateBrowserSelectionFromRep = updateBrowserSelectionFromRep ;
editorInfo . ace _focus = focus ;
editorInfo . ace _importText = importText ;
editorInfo . ace _importAText = importAText ;
editorInfo . ace _exportText = exportText ;
editorInfo . ace _editorChangedSize = editorChangedSize ;
editorInfo . ace _setOnKeyPress = setOnKeyPress ;
editorInfo . ace _setOnKeyDown = setOnKeyDown ;
editorInfo . ace _setNotifyDirty = setNotifyDirty ;
editorInfo . ace _dispose = dispose ;
editorInfo . ace _getFormattedCode = getFormattedCode ;
editorInfo . ace _setEditable = setEditable ;
editorInfo . ace _execCommand = execCommand ;
editorInfo . ace _replaceRange = replaceRange ;
editorInfo . ace _getAuthorInfos = getAuthorInfos ;
editorInfo . ace _performDocumentReplaceRange = performDocumentReplaceRange ;
editorInfo . ace _performDocumentReplaceCharRange = performDocumentReplaceCharRange ;
editorInfo . ace _setSelection = setSelection ;
const nodeMaxIndex = ( nd ) => {
if ( isNodeText ( nd ) ) return nd . nodeValue . length ;
else return 1 ;
} ;
const getSelection = ( ) => {
// returns null, or a structure containing startPoint and endPoint,
// each of which has node (a magicdom node), index, and maxIndex. If the node
// is a text node, maxIndex is the length of the text; else maxIndex is 1.
// index is between 0 and maxIndex, inclusive.
const browserSelection = window . getSelection ( ) ;
if ( ! browserSelection || browserSelection . type === 'None' ||
browserSelection . rangeCount === 0 ) {
return null ;
}
const range = browserSelection . getRangeAt ( 0 ) ;
const isInBody = ( n ) => {
while ( n && ! ( n . tagName && n . tagName . toLowerCase ( ) === 'body' ) ) {
n = n . parentNode ;
}
return ! ! n ;
} ;
const pointFromRangeBound = ( container , offset ) => {
if ( ! isInBody ( container ) ) {
// command-click in Firefox selects whole document, HEAD and BODY!
return {
node : root ,
index : 0 ,
maxIndex : 1 ,
} ;
}
const n = container ;
const childCount = n . childNodes . length ;
if ( isNodeText ( n ) ) {
return {
node : n ,
index : offset ,
maxIndex : n . nodeValue . length ,
} ;
} else if ( childCount === 0 ) {
return {
node : n ,
index : 0 ,
maxIndex : 1 ,
} ;
// treat point between two nodes as BEFORE the second (rather than after the first)
// if possible; this way point at end of a line block-element is treated as
// at beginning of next line
} else if ( offset === childCount ) {
const nd = n . childNodes . item ( childCount - 1 ) ;
const max = nodeMaxIndex ( nd ) ;
return {
node : nd ,
index : max ,
maxIndex : max ,
} ;
} else {
const nd = n . childNodes . item ( offset ) ;
const max = nodeMaxIndex ( nd ) ;
return {
node : nd ,
index : 0 ,
maxIndex : max ,
} ;
}
} ;
2021-01-18 01:10:26 +01:00
const selection = { } ;
2021-01-14 11:00:14 +01:00
selection . startPoint = pointFromRangeBound ( range . startContainer , range . startOffset ) ;
selection . endPoint = pointFromRangeBound ( range . endContainer , range . endOffset ) ;
selection . focusAtStart = (
(
( range . startContainer !== range . endContainer ) ||
( range . startOffset !== range . endOffset )
) &&
browserSelection . anchorNode &&
( browserSelection . anchorNode === range . endContainer ) &&
( browserSelection . anchorOffset === range . endOffset )
) ;
if ( selection . startPoint . node . ownerDocument !== window . document ) {
return null ;
}
return selection ;
} ;
const 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 ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ;
}
enforceEditability ( ) ;
2012-03-14 01:41:05 +01:00
$ ( sideDiv ) . addClass ( 'sidedivdelayed' ) ;
2021-01-14 11:00:14 +01:00
} ;
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
2021-01-18 01:06:35 +01:00
const teardown = ( ) => _teardownActions . forEach ( ( a ) => a ( ) ) ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
let inInternationalComposition = false ;
const handleCompositionEvent = ( evt ) => {
2012-10-11 16:39:01 +02:00
// international input events, fired in FF3, at least; allow e.g. Japanese input
2021-01-14 11:00:14 +01:00
if ( evt . type === 'compositionstart' ) {
2012-10-11 16:39:01 +02:00
inInternationalComposition = true ;
2021-01-14 11:00:14 +01:00
} else if ( evt . type === 'compositionend' ) {
2012-10-11 16:39:01 +02:00
inInternationalComposition = false ;
}
2020-11-23 19:24:19 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
editorInfo . ace _getInInternationalComposition = ( ) => inInternationalComposition ;
const 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 ) ;
2017-05-04 16:22:18 +02:00
// 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 ) ;
2015-03-24 10:58:02 +01:00
// Disabled: https://github.com/ether/etherpad-lite/issues/2546
// Will break OL re-numbering: https://github.com/ether/etherpad-lite/pull/2533
2015-10-13 23:39:23 +02:00
// $(document).on("cut", handleCut);
2015-03-24 10:58:02 +01:00
2020-10-25 02:14:04 +02: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 ) => {
2020-10-25 02:14:04 +02:00
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' ) ) {
2020-10-25 02:14:04 +02:00
scheduler . clearTimeout ( suppressPasteOnLink ) ;
suppressPasteOnLink = null ;
e . preventDefault ( ) ;
return ;
2015-01-19 00:58:47 +01:00
}
2015-11-06 14:21:25 +01:00
// Call paste hook
hooks . callAll ( 'acePaste' , {
2020-11-23 19:24:19 +01:00
editorInfo ,
rep ,
documentAttributeManager ,
e ,
2015-11-06 14:21:25 +01:00
} ) ;
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 ( ) ;
}
2016-08-22 23:44:17 +02:00
// 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 ) ;
2016-08-22 23:44:17 +02:00
2020-11-23 19:24:19 +01:00
const lineBeforeSelection = firstLineSelected . previousSibling ;
const lineAfterSelection = lastLineSelected . nextSibling ;
2016-08-22 23:44:17 +02:00
2020-11-23 19:24:19 +01:00
const neighbor = lineBeforeSelection || lineAfterSelection ;
2016-08-22 23:44:17 +02:00
neighbor . appendChild ( document . createElement ( 'style' ) ) ;
}
2015-12-05 20:06:40 +01:00
// 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
} ) ;
} ) ;
2020-12-19 00:13:02 +01:00
$ ( document . documentElement ) . on ( 'compositionstart' , handleCompositionEvent ) ;
$ ( document . documentElement ) . on ( 'compositionend' , handleCompositionEvent ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const topLevel = ( n ) => {
if ( ( ! n ) || n === root ) return null ;
while ( n . parentNode !== root ) {
2016-08-22 23:44:17 +02:00
n = n . parentNode ;
}
return n ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ;
2021-01-14 11:00:14 +01:00
const leftOf = ( n ) => n . offsetLeft ;
const rightOf = ( n ) => n . offsetLeft + n . offsetWidth ;
2011-07-07 19:59:34 +02:00
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 ;
2021-01-14 11:00:14 +01:00
for ( n = node . previousSibling ; n &&
isNodeText ( n ) ; n = n . previousSibling ) {
charsToLeft += n . nodeValue ;
}
2020-11-23 19:24:19 +01:00
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 ) ;
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ) ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ;
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const scrollXHorizontallyIntoView = ( pixelX ) => {
2020-11-23 19:24:19 +01:00
const win = outerWin ;
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
}
2021-01-14 11:00:14 +01:00
} ;
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const 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 ( ) ;
2018-01-03 22:57:28 +01:00
scroll . scrollNodeVerticallyIntoView ( rep , innerHeight ) ;
2020-11-23 19:24:19 +01:00
if ( ! doesWrap ) {
const browserSelection = getSelection ( ) ;
if ( browserSelection ) {
2021-01-14 11:00:14 +01:00
const focusPoint = (
browserSelection . focusAtStart ? browserSelection . startPoint : browserSelection . endPoint
) ;
2020-11-23 19:24:19 +01:00
const selectionPointX = getSelectionPointX ( focusPoint ) ;
2011-07-07 19:59:34 +02:00
scrollXHorizontallyIntoView ( selectionPointX ) ;
fixView ( ) ;
2011-03-26 14:10:41 +01:00
}
}
2021-01-14 11:00:14 +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
2021-01-14 11:00:14 +01:00
const getLineListType = ( lineNum ) => documentAttributeManager
. getAttributeOnLine ( lineNum , listAttributeName ) ;
editorInfo . ace _getLineListType = getLineListType ;
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
const doInsertList = ( type ) => {
2020-11-23 19:24:19 +01:00
if ( ! ( rep . selStart && rep . selEnd ) ) {
2011-03-26 14:10:41 +01:00
return ;
}
2021-01-14 11:00:14 +01:00
const firstLine = rep . selStart [ 0 ] ;
const 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 ;
2021-01-14 11:00:14 +01:00
for ( let n = firstLine ; n <= lastLine ; n ++ ) {
const listType = getLineListType ( n ) ;
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 = [ ] ;
2021-01-14 11:00:14 +01:00
for ( let n = firstLine ; n <= lastLine ; n ++ ) {
// var t = '';
2020-11-23 19:24:19 +01:00
let level = 0 ;
2021-01-14 11:00:14 +01:00
let togglingOn = true ;
const 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 ) {
2021-01-14 11:00:14 +01:00
togglingOn = false ;
2020-06-07 10:51:12 +02:00
}
2020-11-23 19:24:19 +01:00
if ( listType ) {
2021-01-14 11:00:14 +01:00
// t = listType[1];
2011-11-25 11:10:57 +01:00
level = Number ( listType [ 2 ] ) ;
}
2021-01-14 11:00:14 +01:00
const 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
2021-01-18 01:06:35 +01:00
mods . forEach ( ( mod ) => {
2012-04-05 01:07:47 +02:00
setLineListType ( mod [ 0 ] , mod [ 1 ] ) ;
2012-04-05 00:50:04 +02:00
} ) ;
2021-01-14 11:00:14 +01:00
} ;
2013-03-18 18:40:18 +01:00
2021-01-14 11:00:14 +01:00
const doInsertUnorderedList = ( ) => {
2012-01-15 18:20:20 +01:00
doInsertList ( 'bullet' ) ;
2021-01-14 11:00:14 +01:00
} ;
const doInsertOrderedList = ( ) => {
2012-01-15 18:20:20 +01:00
doInsertList ( 'number' ) ;
2021-01-14 11:00:14 +01:00
} ;
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
2011-03-26 14:10:41 +01:00
2020-09-08 15:52:26 +02:00
// We apply the height of a line in the doc body, to the corresponding sidediv line number
2021-01-14 11:00:14 +01:00
const updateLineNumbers = ( ) => {
2020-09-08 15:52:26 +02:00
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 ;
2020-09-08 15:52:26 +02:00
// First loop to calculate the heights from doc body
2020-11-23 19:24:19 +01:00
while ( docLine ) {
2020-09-08 15:52:26 +02:00
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.
2021-01-14 11:00:14 +01:00
h = docLine . nextSibling . offsetTop - parseInt (
window . getComputedStyle ( doc . body )
. getPropertyValue ( 'padding-top' ) . split ( 'px' ) [ 0 ]
) ;
2020-09-08 15:52:26 +02:00
} 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 ) ;
2020-09-08 15:52:26 +02:00
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
2020-09-08 15:52:26 +02:00
// Apply height to existing sidediv lines
2020-11-23 19:24:19 +01:00
currentLine = 0 ;
2020-09-08 15:52:26 +02:00
while ( sidebarLine && currentLine <= lineNumbersShown ) {
if ( lineHeights [ currentLine ] ) {
2020-11-23 19:24:19 +01:00
sidebarLine . style . height = ` ${ lineHeights [ currentLine ] } px ` ;
2011-12-07 14:23:28 +01:00
}
2020-09-08 15:52:26 +02:00
sidebarLine = sidebarLine . nextSibling ;
currentLine ++ ;
2013-06-14 19:37:41 +02:00
}
2021-01-14 11:00:14 +01:00
if ( newNumLines !== lineNumbersShown ) {
2020-11-23 19:24:19 +01:00
const container = sideDivInner ;
const odoc = outerWin . document ;
const fragment = odoc . createDocumentFragment ( ) ;
2020-09-08 15:52:26 +02:00
// Create missing line and apply height
2020-11-23 19:24:19 +01:00
while ( lineNumbersShown < newNumLines ) {
2011-12-07 14:23:28 +01:00
lineNumbersShown ++ ;
2020-11-23 19:24:19 +01:00
const div = odoc . createElement ( 'DIV' ) ;
2020-09-08 15:52:26 +02:00
if ( lineHeights [ currentLine ] ) {
2020-11-23 19:24:19 +01:00
div . style . height = ` ${ lineHeights [ currentLine ] } px ` ;
2013-04-07 20:06:15 +02:00
}
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 ) ;
2020-09-08 15:52:26 +02:00
currentLine ++ ;
2011-12-07 14:23:28 +01:00
}
container . appendChild ( fragment ) ;
2020-09-08 15:52:26 +02:00
// Remove extra lines
2020-11-23 19:24:19 +01:00
while ( lineNumbersShown > newNumLines ) {
2011-12-07 14:23:28 +01:00
container . removeChild ( container . lastChild ) ;
lineNumbersShown -- ;
2011-03-26 14:10:41 +01:00
}
}
2021-01-14 11:00:14 +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 ) ;
2012-02-19 14:30:57 +01:00
2021-01-18 01:10:26 +01:00
editorInfo . ace _performDocumentApplyAttributesToRange =
( ... args ) => documentAttributeManager . setAttributesOnRange ( args ) ;
2021-01-14 11:00:14 +01:00
this . init = ( ) => {
2020-11-23 19:24:19 +01:00
$ ( document ) . ready ( ( ) => {
2012-09-12 09:04:15 +02:00
doc = document ; // defined as a var in scope outside
2020-11-23 19:24:19 +01:00
inCallStack ( 'setup' , ( ) => {
const body = doc . getElementById ( 'innerdocbody' ) ;
2012-09-12 09:04:15 +02:00
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' ) ;
2020-11-02 21:43:30 +01:00
root . classList . toggle ( 'authorColors' , true ) ;
root . classList . toggle ( 'doesWrap' , doesWrap ) ;
2012-03-14 01:41:05 +01:00
2012-09-12 09:04:15 +02:00
initDynamicCSS ( ) ;
2012-03-14 01:41:05 +01:00
2012-09-12 09:04:15 +02:00
enforceEditability ( ) ;
2012-03-14 01:41:05 +01:00
2012-09-12 09:04:15 +02:00
// set up dom and rep
while ( root . firstChild ) root . removeChild ( root . firstChild ) ;
2020-11-23 19:24:19 +01:00
const oneEntry = createDomLineEntry ( '' ) ;
2012-09-12 09:04:15 +02:00
doRepLineSplice ( 0 , rep . lines . length ( ) , [ oneEntry ] ) ;
2020-11-21 23:26:32 +01:00
insertDomLines ( null , [ oneEntry . domInfo ] ) ;
2012-09-12 09:04:15 +02:00
rep . alines = Changeset . splitAttributionLines (
2020-11-23 19:24:19 +01:00
Changeset . makeAttribution ( '\n' ) , '\n' ) ;
2012-03-14 01:41:05 +01:00
2012-09-12 09:04:15 +02:00
bindTheEventHandlers ( ) ;
} ) ;
2013-06-14 19:37:41 +02:00
2012-09-12 09:04:15 +02:00
hooks . callAll ( 'aceInitialized' , {
2020-11-23 19:24:19 +01:00
editorInfo ,
rep ,
documentAttributeManager ,
2012-09-12 09:04:15 +02:00
} ) ;
2013-06-14 19:37:41 +02:00
2020-11-23 19:24:19 +01:00
scheduler . setTimeout ( ( ) => {
2012-09-12 09:04:15 +02:00
parent . readyFunc ( ) ; // defined in code that sets up the inner iframe
} , 0 ) ;
} ) ;
2020-11-23 19:24:19 +01:00
} ;
2012-02-24 20:22:32 +01:00
}
2011-03-26 14:10:41 +01:00
2021-01-14 11:00:14 +01:00
exports . init = ( ) => {
2020-11-23 19:24:19 +01:00
const editor = new Ace2Inner ( ) ;
2012-09-12 09:04:15 +02:00
editor . init ( ) ;
} ;