2011-12-04 16:33:56 +01:00
/ * *
2013-06-14 19:37:41 +02:00
* This code is mostly from the old Etherpad . Please help us to comment this code .
2011-12-04 16:33:56 +01:00
* This helps other people to understand this code better and helps them to improve it .
* TL ; DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
* /
2011-03-26 14:10:41 +01:00
/ * *
* Copyright 2009 Google Inc .
2011-07-07 19:59:34 +02:00
*
2011-03-26 14:10:41 +01:00
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
2011-07-07 19:59:34 +02:00
*
2011-03-26 14:10:41 +01:00
* http : //www.apache.org/licenses/LICENSE-2.0
2011-07-07 19:59:34 +02:00
*
2011-03-26 14:10:41 +01:00
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS-IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2015-01-21 15:55:29 +01:00
var _ , $ , jQuery , plugins , Ace2Common ;
2015-12-01 15:53:49 +01:00
var browser = require ( './browser' ) ;
2015-01-21 16:01:39 +01:00
if ( browser . msie ) {
2015-01-21 15:25:24 +01:00
// Honestly fuck IE royally.
// Basically every hack we have since V11 causes a problem
2015-01-21 16:01:39 +01:00
if ( parseInt ( browser . version ) >= 11 ) {
delete browser . msie ;
browser . chrome = true ;
2015-01-21 16:21:31 +01:00
browser . modernIE = true ;
2015-01-21 15:25:24 +01:00
}
}
2015-01-21 15:55:29 +01:00
2012-03-14 01:41:05 +01:00
Ace2Common = require ( './ace2_common' ) ;
2012-02-19 12:34:24 +01:00
2012-05-29 03:39:32 +02:00
plugins = require ( 'ep_etherpad-lite/static/js/pluginfw/client_plugins' ) ;
2012-03-14 01:41:05 +01:00
$ = jQuery = require ( './rjquery' ) . $ ;
2012-03-17 13:36:42 +01:00
_ = require ( "./underscore" ) ;
2012-03-14 01:41:05 +01:00
var isNodeText = Ace2Common . isNodeText ,
getAssoc = Ace2Common . getAssoc ,
setAssoc = Ace2Common . setAssoc ,
isTextNode = Ace2Common . isTextNode ,
binarySearchInfinite = Ace2Common . binarySearchInfinite ,
htmlPrettyEscape = Ace2Common . htmlPrettyEscape ,
noop = Ace2Common . noop ;
2015-01-21 03:55:03 +01:00
var hooks = require ( './pluginfw/hooks' ) ;
2012-03-18 09:05:46 +01:00
2012-02-19 14:30:57 +01:00
function Ace2Inner ( ) {
2013-06-14 19:37:41 +02:00
2012-03-14 01:41:05 +01:00
var makeChangesetTracker = require ( './changesettracker' ) . makeChangesetTracker ;
var colorutils = require ( './colorutils' ) . colorutils ;
var makeContentCollector = require ( './contentcollector' ) . makeContentCollector ;
var makeCSSManager = require ( './cssmanager' ) . makeCSSManager ;
var domline = require ( './domline' ) . domline ;
2012-03-27 11:15:48 +02:00
var AttribPool = require ( './AttributePool' ) ;
2012-03-14 01:41:05 +01:00
var Changeset = require ( './Changeset' ) ;
2012-04-05 00:50:04 +02:00
var ChangesetUtils = require ( './ChangesetUtils' ) ;
2012-03-14 01:41:05 +01:00
var linestylefilter = require ( './linestylefilter' ) . linestylefilter ;
2012-03-27 11:15:48 +02:00
var SkipList = require ( './skiplist' ) ;
2012-03-14 01:41:05 +01:00
var undoModule = require ( './undomodule' ) . undoModule ;
2012-04-05 00:50:04 +02:00
var AttributeManager = require ( './AttributeManager' ) ;
2018-01-03 22:57:28 +01:00
var Scroll = require ( './scroll' ) ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
var DEBUG = false ; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
2013-06-14 19:37:41 +02:00
// changed to false
2011-03-26 14:10:41 +01:00
var isSetUp = false ;
2011-07-07 19:59:34 +02:00
var THE _TAB = ' ' ; //4
2014-12-27 13:18:58 +01:00
var MAX _LIST _LEVEL = 16 ;
2011-03-26 14:10:41 +01:00
2018-01-04 15:28:00 +01:00
var FORMATTING _STYLES = [ 'bold' , 'italic' , 'underline' , 'strikethrough' ] ;
var SELECT _BUTTON _CLASS = 'selected' ;
2011-03-26 14:10:41 +01:00
var caughtErrors = [ ] ;
var thisAuthor = '' ;
var disposed = false ;
var editorInfo = parent . editorInfo ;
2018-01-03 22:57:28 +01:00
2011-03-26 14:10:41 +01:00
var iframe = window . frameElement ;
var outerWin = iframe . ace _outerWin ;
iframe . ace _outerWin = null ; // prevent IE 6 memory leak
var sideDiv = iframe . nextSibling ;
var lineMetricsDiv = sideDiv . nextSibling ;
initLineNumbers ( ) ;
2018-01-03 22:57:28 +01:00
var scroll = Scroll . init ( outerWin ) ;
2012-03-27 12:28:47 +02:00
var outsideKeyDown = noop ;
2013-06-14 19:37:41 +02:00
2012-03-27 12:28:47 +02:00
var outsideKeyPress = function ( ) { return true ; } ;
2013-06-14 19:37:41 +02:00
2012-03-27 12:28:47 +02:00
var 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!
2011-07-07 19:59:34 +02:00
var 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 ,
alltext : "" ,
alines : [ ] ,
apool : new AttribPool ( )
} ;
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()
2011-07-07 19:59:34 +02:00
if ( undoModule . enabled )
{
2011-03-26 14:10:41 +01:00
undoModule . apool = rep . apool ;
}
2013-12-08 17:25:12 +01:00
var root , doc ; // set in init()
2011-03-26 14:10:41 +01:00
var isEditable = true ;
var doesWrap = true ;
var hasLineNumbers = true ;
var isStyled = true ;
2013-06-14 19:37:41 +02:00
2011-03-26 20:47:31 +01:00
var console = ( DEBUG && window . console ) ;
2012-04-05 00:50:04 +02:00
var documentAttributeManager ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
if ( ! window . console )
{
var 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 = { } ;
for ( var i = 0 ; i < names . length ; ++ i )
2012-02-19 14:52:24 +01:00
console [ names [ i ] ] = noop ;
2011-03-26 14:10:41 +01:00
}
2011-03-26 20:47:31 +01:00
2011-03-26 14:10:41 +01:00
var PROFILER = window . PROFILER ;
2011-07-07 19:59:34 +02:00
if ( ! PROFILER )
{
PROFILER = function ( )
{
return {
start : noop ,
mark : noop ,
literal : noop ,
end : noop ,
cancel : noop
} ;
} ;
}
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.
var dmesg = noop ;
window . dmesg = noop ;
2013-02-09 17:42:47 +01:00
var scheduler = parent ; // hack for opera required
2011-03-26 14:10:41 +01:00
var dynamicCSS = null ;
2013-06-14 19:37:41 +02:00
var outerDynamicCSS = null ;
2012-12-03 03:43:56 +01:00
var parentDynamicCSS = null ;
2011-07-07 19:59:34 +02:00
function initDynamicCSS ( )
{
2011-03-26 14:10:41 +01:00
dynamicCSS = makeCSSManager ( "dynamicsyntax" ) ;
2013-06-14 19:37:41 +02:00
outerDynamicCSS = makeCSSManager ( "dynamicsyntax" , "outer" ) ;
parentDynamicCSS = makeCSSManager ( "dynamicsyntax" , "parent" ) ;
2011-03-26 14:10:41 +01:00
}
2013-02-09 17:42:47 +01:00
var changesetTracker = makeChangesetTracker ( scheduler , rep . apool , {
2011-07-07 19:59:34 +02:00
withCallbacks : function ( operationName , f )
{
inCallStackIfNecessary ( operationName , function ( )
{
2011-03-26 14:10:41 +01:00
fastIncorp ( 1 ) ;
2011-07-07 19:59:34 +02:00
f (
{
setDocumentAttributedText : function ( atext )
{
setDocAText ( atext ) ;
2011-03-26 14:10:41 +01:00
} ,
2011-07-07 19:59:34 +02:00
applyChangesetToDocument : function ( changeset , preferInsertionAfterCaret )
{
var oldEventType = currentCallStack . editEvent . eventType ;
currentCallStack . startNewEvent ( "nonundoable" ) ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
performDocumentApplyChangeset ( changeset , preferInsertionAfterCaret ) ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
currentCallStack . startNewEvent ( oldEventType ) ;
2011-03-26 14:10:41 +01:00
}
} ) ;
} ) ;
}
} ) ;
var authorInfos = { } ; // presence of key determines if author is present in doc
2012-09-08 20:45:33 +02:00
function getAuthorInfos ( ) {
return authorInfos ;
} ;
editorInfo . ace _getAuthorInfos = getAuthorInfos ;
2013-06-06 06:30:48 +02:00
function setAuthorStyle ( author , info )
{
if ( ! dynamicCSS ) {
return ;
}
var authorSelector = getAuthorColorClassSelector ( getAuthorClassName ( author ) ) ;
2013-06-06 06:59:56 +02:00
var authorStyleSet = hooks . callAll ( 'aceSetAuthorStyle' , {
dynamicCSS : dynamicCSS ,
parentDynamicCSS : parentDynamicCSS ,
2013-06-14 19:37:41 +02:00
outerDynamicCSS : outerDynamicCSS ,
2013-06-06 06:59:56 +02:00
info : info ,
author : author ,
authorSelector : authorSelector ,
} ) ;
// Prevent default behaviour if any hook says so
if ( _ . any ( authorStyleSet , function ( it ) { return it } ) )
{
return
}
2013-06-06 06:30:48 +02:00
if ( ! info )
{
dynamicCSS . removeSelectorStyle ( authorSelector ) ;
parentDynamicCSS . removeSelectorStyle ( authorSelector ) ;
}
else
{
if ( info . bgcolor )
{
var bgcolor = info . bgcolor ;
if ( ( typeof info . fade ) == "number" )
{
bgcolor = fadeColor ( bgcolor , info . fade ) ;
}
var authorStyle = dynamicCSS . selectorStyle ( authorSelector ) ;
var parentAuthorStyle = parentDynamicCSS . selectorStyle ( authorSelector ) ;
// author color
authorStyle . backgroundColor = bgcolor ;
parentAuthorStyle . backgroundColor = bgcolor ;
2020-04-16 08:20:57 +02:00
var textColor = colorutils . textColorFromBackgroundColor ( bgcolor , parent . parent . clientVars . skinName ) ;
authorStyle . color = textColor ;
parentAuthorStyle . color = textColor ;
2013-06-06 06:30:48 +02:00
}
}
}
2011-07-07 19:59:34 +02:00
function setAuthorInfo ( author , info )
{
if ( ( typeof author ) != "string" )
{
2020-05-29 13:56:03 +02:00
// Potentially caused by: https://github.com/ether/etherpad-lite/issues/2802");
2011-07-07 19:59:34 +02:00
throw new Error ( "setAuthorInfo: author (" + author + ") is not a string" ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
if ( ! info )
{
2011-03-26 14:10:41 +01:00
delete authorInfos [ author ] ;
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
authorInfos [ author ] = info ;
}
2013-06-06 06:30:48 +02:00
setAuthorStyle ( author , info ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function getAuthorClassName ( author )
{
return "author-" + author . replace ( /[^a-y0-9]/g , function ( c )
{
2011-03-26 14:10:41 +01:00
if ( c == "." ) return "-" ;
2011-07-07 19:59:34 +02:00
return 'z' + c . charCodeAt ( 0 ) + 'z' ;
2011-03-26 14:10:41 +01:00
} ) ;
}
2011-07-07 19:59:34 +02:00
function className2Author ( className )
{
if ( className . substring ( 0 , 7 ) == "author-" )
{
return className . substring ( 7 ) . replace ( /[a-y0-9]+|-|z.+?z/g , function ( cc )
{
2011-03-26 14:10:41 +01:00
if ( cc == '-' ) return '.' ;
2011-07-07 19:59:34 +02:00
else if ( cc . charAt ( 0 ) == 'z' )
{
return String . fromCharCode ( Number ( cc . slice ( 1 , - 1 ) ) ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
return cc ;
}
} ) ;
}
return null ;
}
2011-07-07 19:59:34 +02:00
function getAuthorColorClassSelector ( oneClassName )
{
return ".authorColors ." + oneClassName ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function fadeColor ( colorCSS , fadeFrac )
{
2011-03-26 14:10:41 +01:00
var 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 ) ;
}
2011-07-07 19:59:34 +02:00
editorInfo . ace _getRep = function ( )
{
2011-03-26 14:10:41 +01:00
return rep ;
2012-02-19 14:52:24 +01:00
} ;
2011-03-26 14:10:41 +01:00
2012-05-30 17:18:43 +02:00
editorInfo . ace _getAuthor = function ( )
{
return thisAuthor ;
}
2016-03-30 16:51:18 +02:00
var _nonScrollableEditEvents = {
"applyChangesToBase" : 1
} ;
_ . each ( hooks . callAll ( 'aceRegisterNonScrollableEditEvents' ) , function ( eventType ) {
_nonScrollableEditEvents [ eventType ] = 1 ;
} ) ;
function isScrollableEditEvent ( eventType )
{
return ! _nonScrollableEditEvents [ eventType ] ;
}
2011-03-26 14:10:41 +01:00
var currentCallStack = null ;
2011-07-07 19:59:34 +02:00
function inCallStack ( type , action )
{
2011-03-26 14:10:41 +01:00
if ( disposed ) return ;
2011-07-07 19:59:34 +02:00
if ( currentCallStack )
{
2020-05-29 13:56:03 +02:00
// Do not uncomment this in production. It will break Etherpad being provided in iFrames. I'm leaving this in for testing usefulness.
// top.console.error("Can't enter callstack " + type + ", already in " + currentCallStack.type);
2011-03-26 14:10:41 +01:00
}
var profiling = false ;
2011-07-07 19:59:34 +02:00
function profileRest ( )
{
2011-03-26 14:10:41 +01:00
profiling = true ;
}
2011-07-07 19:59:34 +02:00
function newEditEvent ( eventType )
{
return {
eventType : eventType ,
backset : null
} ;
}
function submitOldEvent ( evt )
{
if ( rep . selStart && rep . selEnd )
{
var selStartChar = rep . lines . offsetOfIndex ( rep . selStart [ 0 ] ) + rep . selStart [ 1 ] ;
var selEndChar = rep . lines . offsetOfIndex ( rep . selEnd [ 0 ] ) + rep . selEnd [ 1 ] ;
evt . selStart = selStartChar ;
evt . selEnd = selEndChar ;
evt . selFocusAtStart = rep . selFocusAtStart ;
}
if ( undoModule . enabled )
{
var undoWorked = false ;
try
{
2018-01-03 22:57:28 +01:00
if ( isPadLoading ( evt . eventType ) )
2011-07-07 19:59:34 +02:00
{
undoModule . clearHistory ( ) ;
}
else if ( evt . eventType == "nonundoable" )
{
if ( evt . changeset )
{
undoModule . reportExternalChange ( evt . changeset ) ;
}
}
else
{
undoModule . reportEvent ( evt ) ;
}
undoWorked = true ;
}
finally
{
if ( ! undoWorked )
{
undoModule . enabled = false ; // for safety
}
}
}
}
function startNewEvent ( eventType , dontSubmitOld )
{
2011-03-26 14:10:41 +01:00
var oldEvent = currentCallStack . editEvent ;
2011-07-07 19:59:34 +02:00
if ( ! dontSubmitOld )
{
submitOldEvent ( oldEvent ) ;
2011-03-26 14:10:41 +01:00
}
currentCallStack . editEvent = newEditEvent ( eventType ) ;
return oldEvent ;
}
2011-07-07 19:59:34 +02:00
currentCallStack = {
type : type ,
docTextChanged : false ,
selectionAffected : false ,
userChangedSelection : false ,
domClean : false ,
profileRest : profileRest ,
isUserChange : false ,
// is this a "user change" type of call-stack
repChanged : false ,
editEvent : newEditEvent ( type ) ,
startNewEvent : startNewEvent
} ;
2011-03-26 14:10:41 +01:00
var cleanExit = false ;
var result ;
2011-07-07 19:59:34 +02:00
try
{
2011-03-26 14:10:41 +01:00
result = action ( ) ;
2012-05-30 17:18:43 +02:00
hooks . callAll ( 'aceEditEvent' , {
callstack : currentCallStack ,
editorInfo : editorInfo ,
rep : rep ,
documentAttributeManager : documentAttributeManager
} ) ;
2011-03-26 14:10:41 +01:00
cleanExit = true ;
}
2011-07-07 19:59:34 +02:00
catch ( e )
{
caughtErrors . push (
{
error : e ,
time : + new Date ( )
} ) ;
2011-03-26 14:10:41 +01:00
dmesg ( e . toString ( ) ) ;
throw e ;
}
2011-07-07 19:59:34 +02:00
finally
{
2011-03-26 14:10:41 +01:00
var cs = currentCallStack ;
2011-07-07 19:59:34 +02:00
if ( cleanExit )
{
submitOldEvent ( cs . editEvent ) ;
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();
// }
2011-07-07 19:59:34 +02:00
if ( cs . selectionAffected )
{
updateBrowserSelectionFromRep ( ) ;
}
2016-03-30 16:51:18 +02:00
if ( ( cs . docTextChanged || cs . userChangedSelection ) && isScrollableEditEvent ( cs . type ) )
2011-07-07 19:59:34 +02:00
{
scrollSelectionIntoView ( ) ;
}
if ( cs . docTextChanged && cs . type . indexOf ( "importText" ) < 0 )
{
outsideNotifyDirty ( ) ;
}
}
}
else
{
// non-clean exit
if ( currentCallStack . type == "idleWorkTimer" )
{
idleWorkTimer . atLeast ( 1000 ) ;
}
2011-03-26 14:10:41 +01:00
}
currentCallStack = null ;
}
return result ;
}
editorInfo . ace _inCallStack = inCallStack ;
2011-07-07 19:59:34 +02:00
function inCallStackIfNecessary ( type , action )
{
if ( ! currentCallStack )
{
2011-03-26 14:10:41 +01:00
inCallStack ( type , action ) ;
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
action ( ) ;
}
}
editorInfo . ace _inCallStackIfNecessary = inCallStackIfNecessary ;
2011-07-07 19:59:34 +02:00
function dispose ( )
{
2011-03-26 14:10:41 +01:00
disposed = true ;
if ( idleWorkTimer ) idleWorkTimer . never ( ) ;
teardown ( ) ;
}
2011-07-07 19:59:34 +02:00
function checkALines ( )
{
2011-03-26 14:10:41 +01:00
return ; // disable for speed
2011-07-07 19:59:34 +02:00
function error ( )
{
throw new Error ( "checkALines" ) ;
}
if ( rep . alines . length != rep . lines . length ( ) )
{
2011-03-26 14:10:41 +01:00
error ( ) ;
}
2011-07-07 19:59:34 +02:00
for ( var i = 0 ; i < rep . alines . length ; i ++ )
{
2011-03-26 14:10:41 +01:00
var aline = rep . alines [ i ] ;
2011-07-07 19:59:34 +02:00
var lineText = rep . lines . atIndex ( i ) . text + "\n" ;
2011-03-26 14:10:41 +01:00
var lineTextLength = lineText . length ;
var opIter = Changeset . opIterator ( aline ) ;
var alineLength = 0 ;
2011-07-07 19:59:34 +02:00
while ( opIter . hasNext ( ) )
{
var o = opIter . next ( ) ;
alineLength += o . chars ;
if ( opIter . hasNext ( ) )
{
2012-02-19 14:52:24 +01:00
if ( o . lines !== 0 ) error ( ) ;
2011-07-07 19:59:34 +02:00
}
else
{
if ( o . lines != 1 ) error ( ) ;
}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
if ( alineLength != lineTextLength )
{
error ( ) ;
2011-03-26 14:10:41 +01:00
}
}
}
2011-07-07 19:59:34 +02:00
function setWraps ( newVal )
{
2011-03-26 14:10:41 +01:00
doesWrap = newVal ;
var dwClass = "doesWrap" ;
setClassPresence ( root , "doesWrap" , doesWrap ) ;
2013-02-09 17:42:47 +01:00
scheduler . setTimeout ( function ( )
2011-07-07 19:59:34 +02:00
{
inCallStackIfNecessary ( "setWraps" , function ( )
{
fastIncorp ( 7 ) ;
recreateDOM ( ) ;
fixView ( ) ;
2011-03-26 14:10:41 +01:00
} ) ;
} , 0 ) ;
2014-10-24 01:13:34 +02:00
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function setStyled ( newVal )
{
2011-03-26 14:10:41 +01:00
var oldVal = isStyled ;
2011-07-07 19:59:34 +02:00
isStyled = ! ! newVal ;
if ( newVal != oldVal )
{
if ( ! newVal )
{
// clear styles
inCallStackIfNecessary ( "setStyled" , function ( )
{
fastIncorp ( 12 ) ;
var clearStyles = [ ] ;
for ( var k in STYLE _ATTRIBS )
{
clearStyles . push ( [ k , '' ] ) ;
}
performDocumentApplyAttributesToCharRange ( 0 , rep . alltext . length , clearStyles ) ;
} ) ;
2011-03-26 14:10:41 +01:00
}
}
}
2011-07-07 19:59:34 +02:00
function setTextFace ( face )
{
2020-06-02 11:25:43 +02:00
root . style . fontFamily = face ;
lineMetricsDiv . style . fontFamily = face ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function recreateDOM ( )
{
2011-03-26 14:10:41 +01:00
// precond: normalized
recolorLinesInRange ( 0 , rep . alltext . length ) ;
}
2011-07-07 19:59:34 +02:00
function setEditable ( newVal )
{
2011-03-26 14:10:41 +01:00
isEditable = newVal ;
// the following may fail, e.g. if iframe is hidden
2011-07-07 19:59:34 +02:00
if ( ! isEditable )
{
2011-03-26 14:10:41 +01:00
setDesignMode ( false ) ;
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
setDesignMode ( true ) ;
}
2011-07-07 19:59:34 +02:00
setClassPresence ( root , "static" , ! isEditable ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function enforceEditability ( )
{
2011-03-26 14:10:41 +01:00
setEditable ( isEditable ) ;
}
2011-07-07 19:59:34 +02:00
function importText ( text , undoable , dontProcess )
{
2011-03-26 14:10:41 +01:00
var lines ;
2011-07-07 19:59:34 +02:00
if ( dontProcess )
{
if ( text . charAt ( text . length - 1 ) != "\n" )
{
throw new Error ( "new raw text must end with newline" ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02: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' ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
2012-03-17 13:36:42 +01:00
lines = _ . map ( text . split ( '\n' ) , textify ) ;
2011-03-26 14:10:41 +01:00
}
var newText = "\n" ;
2011-07-07 19:59:34 +02:00
if ( lines . length > 0 )
{
newText = lines . join ( '\n' ) + '\n' ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
inCallStackIfNecessary ( "importText" + ( undoable ? "Undoable" : "" ) , function ( )
{
2011-03-26 14:10:41 +01:00
setDocText ( newText ) ;
} ) ;
2011-07-07 19:59:34 +02:00
if ( dontProcess && rep . alltext != text )
{
2011-03-26 14:10:41 +01:00
throw new Error ( "mismatch error setting raw text in importText" ) ;
}
}
2011-07-07 19:59:34 +02:00
function importAText ( atext , apoolJsonObj , undoable )
{
2011-03-26 14:10:41 +01:00
atext = Changeset . cloneAText ( atext ) ;
2011-07-07 19:59:34 +02:00
if ( apoolJsonObj )
{
2011-03-26 14:10:41 +01:00
var wireApool = ( new AttribPool ( ) ) . fromJsonable ( apoolJsonObj ) ;
atext . attribs = Changeset . moveOpsToNewPool ( atext . attribs , wireApool , rep . apool ) ;
}
2011-07-07 19:59:34 +02:00
inCallStackIfNecessary ( "importText" + ( undoable ? "Undoable" : "" ) , function ( )
{
2011-03-26 14:10:41 +01:00
setDocAText ( atext ) ;
} ) ;
}
2011-07-07 19:59:34 +02:00
function setDocAText ( atext )
{
2020-04-13 20:16:33 +02:00
if ( atext . text === "" ) {
/ *
* The server is fine with atext . text being an empty string , but the front
* end is not , and crashes .
*
* It is not clear if this is a problem in the server or in the client
* code , and this is a client - side hack fix . The underlying problem needs
* to be investigated .
*
* See for reference :
* - https : //github.com/ether/etherpad-lite/issues/3861
* /
atext . text = "\n" ;
}
2011-03-26 14:10:41 +01:00
fastIncorp ( 8 ) ;
var oldLen = rep . lines . totalWidth ( ) ;
var numLines = rep . lines . length ( ) ;
2011-07-07 19:59:34 +02:00
var upToLastLine = rep . lines . offsetOfIndex ( numLines - 1 ) ;
var lastLineLength = rep . lines . atIndex ( numLines - 1 ) . text . length ;
2011-03-26 14:10:41 +01:00
var assem = Changeset . smartOpAssembler ( ) ;
var o = Changeset . newOp ( '-' ) ;
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 ) ;
var newLen = oldLen + assem . getLengthChange ( ) ;
var changeset = Changeset . checkRep (
2011-07-07 19:59:34 +02:00
Changeset . pack ( oldLen , newLen , assem . toString ( ) , atext . text . slice ( 0 , - 1 ) ) ) ;
2011-03-26 14:10:41 +01:00
performDocumentApplyChangeset ( changeset ) ;
2011-07-07 19:59:34 +02:00
performSelectionChange ( [ 0 , rep . lines . atIndex ( 0 ) . lineMarker ] , [ 0 , rep . lines . atIndex ( 0 ) . lineMarker ] ) ;
2011-03-26 14:10:41 +01:00
idleWorkTimer . atMost ( 100 ) ;
2011-07-07 19:59:34 +02:00
if ( rep . alltext != atext . text )
{
2011-03-26 14:10:41 +01:00
dmesg ( htmlPrettyEscape ( rep . alltext ) ) ;
dmesg ( htmlPrettyEscape ( atext . text ) ) ;
throw new Error ( "mismatch error setting raw text in setDocAText" ) ;
}
}
2011-07-07 19:59:34 +02:00
function setDocText ( text )
{
2011-03-26 14:10:41 +01:00
setDocAText ( Changeset . makeAText ( text ) ) ;
}
2011-07-07 19:59:34 +02:00
function getDocText ( )
{
2011-03-26 14:10:41 +01:00
var alltext = rep . alltext ;
var len = alltext . length ;
if ( len > 0 ) len -- ; // final extra newline
return alltext . substring ( 0 , len ) ;
}
2011-07-07 19:59:34 +02:00
function exportText ( )
{
if ( currentCallStack && ! currentCallStack . domClean )
{
inCallStackIfNecessary ( "exportText" , function ( )
{
fastIncorp ( 2 ) ;
} ) ;
2011-03-26 14:10:41 +01:00
}
return getDocText ( ) ;
}
2011-07-07 19:59:34 +02:00
function editorChangedSize ( )
{
2011-03-26 14:10:41 +01:00
fixView ( ) ;
}
2011-07-07 19:59:34 +02:00
function setOnKeyPress ( handler )
{
2011-03-26 14:10:41 +01:00
outsideKeyPress = handler ;
}
2011-07-07 19:59:34 +02:00
function setOnKeyDown ( handler )
{
2011-03-26 14:10:41 +01:00
outsideKeyDown = handler ;
}
2011-07-07 19:59:34 +02:00
function setNotifyDirty ( handler )
{
2011-03-26 14:10:41 +01:00
outsideNotifyDirty = handler ;
}
2011-07-07 19:59:34 +02:00
function getFormattedCode ( )
{
if ( currentCallStack && ! currentCallStack . domClean )
{
2011-03-26 14:10:41 +01:00
inCallStackIfNecessary ( "getFormattedCode" , incorporateUserChanges ) ;
}
var buf = [ ] ;
2011-07-07 19:59:34 +02:00
if ( rep . lines . length ( ) > 0 )
{
2011-03-26 14:10:41 +01:00
// should be the case, even for empty file
var entry = rep . lines . atIndex ( 0 ) ;
2011-07-07 19:59:34 +02:00
while ( entry )
{
var domInfo = entry . domInfo ;
buf . push ( ( domInfo && domInfo . getInnerHTML ( ) ) || domline . processSpaces ( domline . escapeHTML ( entry . text ) , doesWrap ) || ' ' /*empty line*/ ) ;
entry = rep . lines . next ( entry ) ;
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
return '<div class="syntax"><div>' + buf . join ( '</div>\n<div>' ) + '</div></div>' ;
2011-03-26 14:10:41 +01:00
}
var CMDS = {
2011-07-07 19:59:34 +02:00
clearauthorship : function ( prompt )
{
if ( ( ! ( rep . selStart && rep . selEnd ) ) || isCaret ( ) )
{
if ( prompt )
{
2011-03-26 14:10:41 +01:00
prompt ( ) ;
}
2011-07-07 19:59:34 +02:00
else
{
performDocumentApplyAttributesToCharRange ( 0 , rep . alltext . length , [
[ 'author' , '' ]
] ) ;
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
setAttributeOnSelection ( 'author' , '' ) ;
}
2011-03-27 12:46:45 +02:00
}
2011-03-26 14:10:41 +01:00
} ;
2011-07-07 19:59:34 +02:00
function execCommand ( cmd )
{
2011-03-26 14:10:41 +01:00
cmd = cmd . toLowerCase ( ) ;
var cmdArgs = Array . prototype . slice . call ( arguments , 1 ) ;
2011-07-07 19:59:34 +02:00
if ( CMDS [ cmd ] )
{
2012-03-22 18:34:08 +01:00
inCallStackIfNecessary ( cmd , function ( )
2011-07-07 19:59:34 +02:00
{
fastIncorp ( 9 ) ;
CMDS [ cmd ] . apply ( CMDS , cmdArgs ) ;
2011-03-26 14:10:41 +01:00
} ) ;
}
}
2011-07-07 19:59:34 +02:00
function replaceRange ( start , end , text )
{
2012-03-22 18:34:08 +01:00
inCallStackIfNecessary ( 'replaceRange' , function ( )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
fastIncorp ( 9 ) ;
performDocumentReplaceRange ( start , end , text ) ;
} ) ;
}
editorInfo . ace _focus = focus ;
editorInfo . ace _importText = importText ;
editorInfo . ace _importAText = importAText ;
editorInfo . ace _exportText = exportText ;
editorInfo . ace _editorChangedSize = editorChangedSize ;
editorInfo . ace _setOnKeyPress = setOnKeyPress ;
editorInfo . ace _setOnKeyDown = setOnKeyDown ;
editorInfo . ace _setNotifyDirty = setNotifyDirty ;
editorInfo . ace _dispose = dispose ;
editorInfo . ace _getFormattedCode = getFormattedCode ;
editorInfo . ace _setEditable = setEditable ;
editorInfo . ace _execCommand = execCommand ;
editorInfo . ace _replaceRange = replaceRange ;
2012-09-08 20:45:33 +02:00
editorInfo . ace _getAuthorInfos = getAuthorInfos ;
editorInfo . ace _performDocumentReplaceRange = performDocumentReplaceRange ;
editorInfo . ace _performDocumentReplaceCharRange = performDocumentReplaceCharRange ;
editorInfo . ace _renumberList = renumberList ;
editorInfo . ace _doReturnKey = doReturnKey ;
editorInfo . ace _isBlockElement = isBlockElement ;
editorInfo . ace _getLineListType = getLineListType ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
editorInfo . ace _callWithAce = function ( fn , callStack , normalize )
{
var wrapper = function ( )
2012-02-19 14:52:24 +01:00
{
return fn ( editorInfo ) ;
} ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
if ( normalize !== undefined )
{
2011-03-26 14:10:41 +01:00
var wrapper1 = wrapper ;
2011-07-07 19:59:34 +02:00
wrapper = function ( )
{
2011-03-26 14:10:41 +01:00
editorInfo . ace _fastIncorp ( 9 ) ;
2011-07-07 19:59:34 +02:00
wrapper1 ( ) ;
2012-02-19 14:52:24 +01:00
} ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
if ( callStack !== undefined )
{
2011-03-26 14:10:41 +01:00
return editorInfo . ace _inCallStack ( callStack , wrapper ) ;
2011-07-07 19:59:34 +02: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
2011-07-07 19:59:34 +02:00
editorInfo . ace _setProperty = function ( key , value )
{
2013-06-14 19:37:41 +02:00
// Convinience function returning a setter for a class on an element
2012-02-21 22:29:40 +01:00
var setClassPresenceNamed = function ( element , cls ) {
return function ( value ) {
setClassPresence ( element , cls , ! ! value )
}
} ;
2013-06-14 19:37:41 +02:00
2012-02-21 22:29:40 +01:00
// These properties are exposed
var setters = {
wraps : setWraps ,
showsauthorcolors : setClassPresenceNamed ( root , "authorColors" ) ,
showsuserselections : setClassPresenceNamed ( root , "userSelections" ) ,
showslinenumbers : function ( value ) {
hasLineNumbers = ! ! value ;
2020-05-01 16:43:25 +02:00
setClassPresence ( sideDiv . parentNode , "line-numbers-hidden" , ! hasLineNumbers ) ;
2012-02-21 22:29:40 +01:00
fixView ( ) ;
} ,
grayedout : setClassPresenceNamed ( outerWin . document . body , "grayedout" ) ,
dmesg : function ( ) { dmesg = window . dmesg = value ; } ,
2013-06-14 19:37:41 +02:00
userauthor : function ( 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 ,
2013-03-06 15:40:02 +01:00
rtlistrue : function ( value ) {
setClassPresence ( root , "rtl" , value )
setClassPresence ( root , "ltr" , ! value )
document . documentElement . dir = value ? 'rtl' : 'ltr'
}
2012-02-21 22:29:40 +01:00
} ;
2013-06-14 19:37:41 +02:00
2012-02-21 23:15:19 +01:00
var setter = setters [ key . toLowerCase ( ) ] ;
2013-06-14 19:37:41 +02:00
// check if setter is present
2012-02-21 22:29:40 +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
2011-07-07 19:59:34 +02:00
editorInfo . ace _setBaseText = function ( txt )
{
2011-03-26 14:10:41 +01:00
changesetTracker . setBaseText ( txt ) ;
} ;
2011-07-07 19:59:34 +02:00
editorInfo . ace _setBaseAttributedText = function ( atxt , apoolJsonObj )
{
2011-03-26 14:10:41 +01:00
changesetTracker . setBaseAttributedText ( atxt , apoolJsonObj ) ;
} ;
2011-07-07 19:59:34 +02:00
editorInfo . ace _applyChangesToBase = function ( c , optAuthor , apoolJsonObj )
{
2011-03-26 14:10:41 +01:00
changesetTracker . applyChangesToBase ( c , optAuthor , apoolJsonObj ) ;
} ;
2011-07-07 19:59:34 +02:00
editorInfo . ace _prepareUserChangeset = function ( )
{
2011-03-26 14:10:41 +01:00
return changesetTracker . prepareUserChangeset ( ) ;
} ;
2011-07-07 19:59:34 +02:00
editorInfo . ace _applyPreparedChangesetToBase = function ( )
{
2011-03-26 14:10:41 +01:00
changesetTracker . applyPreparedChangesetToBase ( ) ;
} ;
2011-07-07 19:59:34 +02:00
editorInfo . ace _setUserChangeNotificationCallback = function ( f )
{
2011-03-26 14:10:41 +01:00
changesetTracker . setUserChangeNotificationCallback ( f ) ;
} ;
2011-07-07 19:59:34 +02:00
editorInfo . ace _setAuthorInfo = function ( author , info )
{
2011-03-26 14:10:41 +01:00
setAuthorInfo ( author , info ) ;
} ;
2011-07-07 19:59:34 +02:00
editorInfo . ace _setAuthorSelectionRange = function ( author , start , end )
{
2011-03-26 14:10:41 +01:00
changesetTracker . setAuthorSelectionRange ( author , start , end ) ;
} ;
2011-07-07 19:59:34 +02:00
editorInfo . ace _getUnhandledErrors = function ( )
{
2011-03-26 14:10:41 +01:00
return caughtErrors . slice ( ) ;
} ;
2013-03-26 02:54:01 +01:00
editorInfo . ace _getDocument = function ( )
{
return doc ;
} ;
2011-07-07 19:59:34 +02:00
editorInfo . ace _getDebugProperty = function ( prop )
{
if ( prop == "debugger" )
{
2011-03-26 14:10:41 +01:00
// obfuscate "eval" so as not to scare yuicompressor
2011-07-07 19:59:34 +02:00
window [ 'ev' + 'al' ] ( "debugger" ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else if ( prop == "rep" )
{
2011-03-26 14:10:41 +01:00
return rep ;
}
2011-07-07 19:59:34 +02:00
else if ( prop == "window" )
{
2011-03-26 14:10:41 +01:00
return window ;
}
2011-07-07 19:59:34 +02:00
else if ( prop == "document" )
{
2011-03-26 14:10:41 +01:00
return document ;
}
return undefined ;
} ;
2011-07-07 19:59:34 +02:00
function now ( )
{
2019-02-26 23:25:15 +01:00
return Date . now ( ) ;
2011-07-07 19:59:34 +02:00
}
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
function newTimeLimit ( ms )
{
2011-03-26 14:10:41 +01:00
var startTime = now ( ) ;
var lastElapsed = 0 ;
var exceededAlready = false ;
var printedTrace = false ;
2011-07-07 19:59:34 +02:00
var isTimeUp = function ( )
{
if ( exceededAlready )
{
if ( ( ! printedTrace ) )
{ // && now() - startTime - ms > 300) {
printedTrace = true ;
}
return true ;
}
var elapsed = now ( ) - startTime ;
if ( elapsed > ms )
{
exceededAlready = true ;
return true ;
}
else
{
lastElapsed = elapsed ;
return false ;
}
2012-02-19 14:52:24 +01:00
} ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
isTimeUp . elapsed = function ( )
{
return now ( ) - startTime ;
2012-02-19 14:52:24 +01:00
} ;
2011-03-26 14:10:41 +01:00
return isTimeUp ;
}
2011-07-07 19:59:34 +02:00
function makeIdleAction ( func )
{
2011-03-26 14:10:41 +01:00
var scheduledTimeout = null ;
var scheduledTime = 0 ;
2011-07-07 19:59:34 +02:00
function unschedule ( )
{
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
}
}
2011-07-07 19:59:34 +02:00
function reschedule ( time )
{
2011-03-26 14:10:41 +01:00
unschedule ( ) ;
scheduledTime = time ;
var delay = time - now ( ) ;
if ( delay < 0 ) delay = 0 ;
2013-02-09 17:42:47 +01:00
scheduledTimeout = scheduler . setTimeout ( callback , delay ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function callback ( )
{
2011-03-26 14:10:41 +01:00
scheduledTimeout = null ;
// func may reschedule the action
func ( ) ;
}
return {
2011-07-07 19:59:34 +02:00
atMost : function ( ms )
{
var latestTime = now ( ) + ms ;
if ( ( ! scheduledTimeout ) || scheduledTime > latestTime )
{
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.
2011-07-07 19:59:34 +02:00
atLeast : function ( ms )
{
var earliestTime = now ( ) + ms ;
if ( ( ! scheduledTimeout ) || scheduledTime < earliestTime )
{
reschedule ( earliestTime ) ;
}
2011-03-26 14:10:41 +01:00
} ,
2011-07-07 19:59:34 +02:00
never : function ( )
{
unschedule ( ) ;
2011-03-26 14:10:41 +01:00
}
2012-02-19 14:52:24 +01:00
} ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function fastIncorp ( n )
{
2011-03-26 14:10:41 +01:00
// normalize but don't do any lexing or anything
incorporateUserChanges ( newTimeLimit ( 0 ) ) ;
}
editorInfo . ace _fastIncorp = fastIncorp ;
2011-07-07 19:59:34 +02:00
var idleWorkTimer = makeIdleAction ( function ( )
{
2011-03-26 14:10:41 +01:00
//if (! top.BEFORE) top.BEFORE = [];
//top.BEFORE.push(magicdom.root.dom.innerHTML);
//if (! isEditable) return; // and don't reschedule
2012-10-11 16:39:01 +02:00
if ( inInternationalComposition )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
// don't do idle input incorporation during international input composition
idleWorkTimer . atLeast ( 500 ) ;
return ;
}
2012-03-22 18:34:08 +01:00
inCallStackIfNecessary ( "idleWorkTimer" , function ( )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
var isTimeUp = newTimeLimit ( 250 ) ;
var finishedImportantWork = false ;
var finishedWork = false ;
2011-07-07 19:59:34 +02:00
try
{
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
// isTimeUp() is a soft constraint for incorporateUserChanges,
// which always renormalizes the DOM, no matter how long it takes,
// but doesn't necessarily lex and highlight it
incorporateUserChanges ( isTimeUp ) ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
if ( isTimeUp ( ) ) return ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
updateLineNumbers ( ) ; // update line numbers if any time left
if ( isTimeUp ( ) ) return ;
2011-03-26 14:10:41 +01:00
2018-01-03 22:57:28 +01:00
var visibleRange = scroll . getVisibleCharRange ( rep ) ;
2011-07-07 19:59:34 +02:00
var docRange = [ 0 , rep . lines . totalWidth ( ) ] ;
finishedImportantWork = true ;
finishedWork = true ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
finally
{
if ( finishedWork )
{
idleWorkTimer . atMost ( 1000 ) ;
}
else if ( finishedImportantWork )
{
// if we've finished highlighting the view area,
// more highlighting could be counter-productive,
// e.g. if the user just opened a triple-quote and will soon close it.
idleWorkTimer . atMost ( 500 ) ;
}
else
{
var timeToWait = Math . round ( isTimeUp . elapsed ( ) / 2 ) ;
if ( timeToWait < 100 ) timeToWait = 100 ;
idleWorkTimer . atMost ( timeToWait ) ;
}
2011-03-26 14:10:41 +01:00
}
} ) ;
//if (! top.AFTER) top.AFTER = [];
//top.AFTER.push(magicdom.root.dom.innerHTML);
} ) ;
var _nextId = 1 ;
2011-07-07 19:59:34 +02:00
function uniqueId ( n )
{
2011-03-26 14:10:41 +01:00
// not actually guaranteed to be unique, e.g. if user copy-pastes
// nodes with ids
var nid = n . id ;
if ( nid ) return nid ;
2011-07-07 19:59:34 +02:00
return ( n . id = "magicdomid" + ( _nextId ++ ) ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function recolorLinesInRange ( startChar , endChar , isTimeUp , optModFunc )
{
2011-03-26 14:10:41 +01:00
if ( endChar <= startChar ) return ;
if ( startChar < 0 || startChar >= rep . lines . totalWidth ( ) ) return ;
2011-07-07 19:59:34 +02:00
var lineEntry = rep . lines . atOffset ( startChar ) ; // rounds down to line boundary
2011-03-26 14:10:41 +01:00
var lineStart = rep . lines . offsetOfEntry ( lineEntry ) ;
var lineIndex = rep . lines . indexOfEntry ( lineEntry ) ;
var selectionNeedsResetting = false ;
var firstLine = null ;
var lastLine = null ;
isTimeUp = ( isTimeUp || noop ) ;
// tokenFunc function; accesses current value of lineEntry and curDocChar,
// also mutates curDocChar
var curDocChar ;
2011-07-07 19:59:34 +02:00
var tokenFunc = function ( tokenText , tokenClass )
{
lineEntry . domInfo . appendSpan ( tokenText , tokenClass ) ;
} ;
if ( optModFunc )
{
2011-03-26 14:10:41 +01:00
var f = tokenFunc ;
2011-07-07 19:59:34 +02:00
tokenFunc = function ( tokenText , tokenClass )
{
optModFunc ( tokenText , tokenClass , f , curDocChar ) ;
curDocChar += tokenText . length ;
2011-03-26 14:10:41 +01:00
} ;
}
2011-07-07 19:59:34 +02:00
while ( lineEntry && lineStart < endChar && ! isTimeUp ( ) )
{
2011-03-26 14:10:41 +01:00
//var timer = newTimeLimit(200);
var lineEnd = lineStart + lineEntry . width ;
curDocChar = lineStart ;
lineEntry . domInfo . clearSpans ( ) ;
getSpansForLine ( lineEntry , tokenFunc , lineStart ) ;
lineEntry . domInfo . finishUpdate ( ) ;
markNodeClean ( lineEntry . lineNode ) ;
2011-07-07 19:59:34 +02:00
if ( rep . selStart && rep . selStart [ 0 ] == lineIndex || rep . selEnd && rep . selEnd [ 0 ] == lineIndex )
{
selectionNeedsResetting = true ;
2011-03-26 14:10:41 +01:00
}
if ( firstLine === null ) firstLine = lineIndex ;
lastLine = lineIndex ;
lineStart = lineEnd ;
lineEntry = rep . lines . next ( lineEntry ) ;
lineIndex ++ ;
}
2011-07-07 19:59:34 +02:00
if ( selectionNeedsResetting )
{
2011-03-26 14:10:41 +01:00
currentCallStack . selectionAffected = true ;
}
}
// like getSpansForRange, but for a line, and the func takes (text,class)
// instead of (width,class); excludes the trailing '\n' from
// consideration by func
2011-07-07 19:59:34 +02:00
function getSpansForLine ( lineEntry , textAndClassFunc , lineEntryOffsetHint )
{
2011-03-26 14:10:41 +01:00
var lineEntryOffset = lineEntryOffsetHint ;
2011-07-07 19:59:34 +02:00
if ( ( typeof lineEntryOffset ) != "number" )
{
2011-03-26 14:10:41 +01:00
lineEntryOffset = rep . lines . offsetOfEntry ( lineEntry ) ;
}
var text = lineEntry . text ;
var width = lineEntry . width ; // text.length+1
2012-02-19 14:52:24 +01:00
if ( text . length === 0 )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
// allow getLineStyleFilter to set line-div styles
var func = linestylefilter . getLineStyleFilter (
2011-07-07 19:59:34 +02:00
0 , '' , textAndClassFunc , rep . apool ) ;
2011-03-26 14:10:41 +01:00
func ( '' , '' ) ;
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
var offsetIntoLine = 0 ;
2015-01-21 16:01:39 +01:00
var filteredFunc = linestylefilter . getFilterStack ( text , textAndClassFunc , browser ) ;
2011-03-26 14:10:41 +01:00
var lineNum = rep . lines . indexOfEntry ( lineEntry ) ;
var aline = rep . alines [ lineNum ] ;
filteredFunc = linestylefilter . getLineStyleFilter (
2011-07-07 19:59:34 +02:00
text . length , aline , filteredFunc , rep . apool ) ;
2011-03-26 14:10:41 +01:00
filteredFunc ( text , '' ) ;
}
}
var observedChanges ;
2011-07-07 19:59:34 +02:00
function clearObservedChanges ( )
{
observedChanges = {
cleanNodesNearChanges : { }
} ;
2011-03-26 14:10:41 +01:00
}
clearObservedChanges ( ) ;
2011-07-07 19:59:34 +02:00
function getCleanNodeByKey ( key )
{
2011-03-26 14:10:41 +01:00
var p = PROFILER ( "getCleanNodeByKey" , false ) ;
p . extra = 0 ;
var n = doc . getElementById ( key ) ;
// copying and pasting can lead to duplicate ids
2011-07-07 19:59:34 +02:00
while ( n && isNodeDirty ( n ) )
{
2011-03-26 14:10:41 +01:00
p . extra ++ ;
n . id = "" ;
n = doc . getElementById ( key ) ;
}
p . literal ( p . extra , "extra" ) ;
p . end ( ) ;
return n ;
}
2011-07-07 19:59:34 +02:00
function observeChangesAroundNode ( node )
{
2011-03-26 14:10:41 +01:00
// Around this top-level DOM node, look for changes to the document
// (from how it looks in our representation) and record them in a way
// that can be used to "normalize" the document (apply the changes to our
// representation, and put the DOM in a canonical form).
var cleanNode ;
var hasAdjacentDirtyness ;
2011-07-07 19:59:34 +02:00
if ( ! isNodeDirty ( node ) )
{
2011-03-26 14:10:41 +01:00
cleanNode = node ;
var prevSib = cleanNode . previousSibling ;
var nextSib = cleanNode . nextSibling ;
2011-07-07 19:59:34 +02:00
hasAdjacentDirtyness = ( ( prevSib && isNodeDirty ( prevSib ) ) || ( nextSib && isNodeDirty ( nextSib ) ) ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
// node is dirty, look for clean node above
var upNode = node . previousSibling ;
2011-07-07 19:59:34 +02:00
while ( upNode && isNodeDirty ( upNode ) )
{
upNode = upNode . previousSibling ;
}
if ( upNode )
{
cleanNode = upNode ;
}
else
{
var downNode = node . nextSibling ;
while ( downNode && isNodeDirty ( downNode ) )
{
downNode = downNode . nextSibling ;
}
if ( downNode )
{
cleanNode = downNode ;
}
}
if ( ! cleanNode )
{
// Couldn't find any adjacent clean nodes!
// Since top and bottom of doc is dirty, the dirty area will be detected.
return ;
2011-03-26 14:10:41 +01:00
}
hasAdjacentDirtyness = true ;
}
2011-07-07 19:59:34 +02:00
if ( hasAdjacentDirtyness )
{
2011-03-26 14:10:41 +01:00
// previous or next line is dirty
2011-07-07 19:59:34 +02:00
observedChanges . cleanNodesNearChanges [ '$' + uniqueId ( cleanNode ) ] = true ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
// next and prev lines are clean (if they exist)
var lineKey = uniqueId ( cleanNode ) ;
var prevSib = cleanNode . previousSibling ;
var nextSib = cleanNode . nextSibling ;
var actualPrevKey = ( ( prevSib && uniqueId ( prevSib ) ) || null ) ;
var actualNextKey = ( ( nextSib && uniqueId ( nextSib ) ) || null ) ;
var repPrevEntry = rep . lines . prev ( rep . lines . atKey ( lineKey ) ) ;
var repNextEntry = rep . lines . next ( rep . lines . atKey ( lineKey ) ) ;
var repPrevKey = ( ( repPrevEntry && repPrevEntry . key ) || null ) ;
var repNextKey = ( ( repNextEntry && repNextEntry . key ) || null ) ;
2011-07-07 19:59:34 +02:00
if ( actualPrevKey != repPrevKey || actualNextKey != repNextKey )
{
observedChanges . cleanNodesNearChanges [ '$' + uniqueId ( cleanNode ) ] = true ;
2011-03-26 14:10:41 +01:00
}
}
}
2011-07-07 19:59:34 +02:00
function observeChangesAroundSelection ( )
{
2011-03-26 14:10:41 +01:00
if ( currentCallStack . observedSelection ) return ;
currentCallStack . observedSelection = true ;
var p = PROFILER ( "getSelection" , false ) ;
var selection = getSelection ( ) ;
p . end ( ) ;
2013-06-14 19:37:41 +02:00
2012-02-19 14:52:24 +01:00
if ( selection )
{
2011-03-26 14:10:41 +01:00
var node1 = topLevel ( selection . startPoint . node ) ;
var node2 = topLevel ( selection . endPoint . node ) ;
if ( node1 ) observeChangesAroundNode ( node1 ) ;
2011-07-07 19:59:34 +02:00
if ( node2 && node1 != node2 )
{
observeChangesAroundNode ( node2 ) ;
2011-03-26 14:10:41 +01:00
}
}
}
2011-07-07 19:59:34 +02:00
function observeSuspiciousNodes ( )
{
2011-03-26 14:10:41 +01:00
// inspired by Firefox bug #473255, where pasting formatted text
// causes the cursor to jump away, making the new HTML never found.
2011-07-07 19:59:34 +02:00
if ( root . getElementsByTagName )
{
2011-03-26 14:10:41 +01:00
var nds = root . getElementsByTagName ( "style" ) ;
2011-07-07 19:59:34 +02:00
for ( var i = 0 ; i < nds . length ; i ++ )
{
2016-08-22 23:44:17 +02:00
var n = topLevel ( nds [ i ] ) ;
if ( n && n . parentNode == root )
2011-07-07 19:59:34 +02:00
{
observeChangesAroundNode ( n ) ;
}
2011-03-26 14:10:41 +01:00
}
}
}
2011-07-07 19:59:34 +02:00
function incorporateUserChanges ( isTimeUp )
{
2011-03-26 14:10:41 +01:00
if ( currentCallStack . domClean ) return false ;
currentCallStack . isUserChange = true ;
2011-07-07 19:59:34 +02:00
isTimeUp = ( isTimeUp ||
function ( )
{
return false ;
} ) ;
2011-03-26 14:10:41 +01:00
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
var p = PROFILER ( "incorp" , false ) ;
//if (doc.body.innerHTML.indexOf("AppJet") >= 0)
//dmesg(htmlPrettyEscape(doc.body.innerHTML));
//if (top.RECORD) top.RECORD.push(doc.body.innerHTML);
// returns true if dom changes were made
2011-07-07 19:59:34 +02:00
if ( ! root . firstChild )
{
2011-03-26 14:10:41 +01:00
root . innerHTML = "<div><!-- --></div>" ;
}
p . mark ( "obs" ) ;
observeChangesAroundSelection ( ) ;
observeSuspiciousNodes ( ) ;
p . mark ( "dirty" ) ;
var dirtyRanges = getDirtyRanges ( ) ;
var dirtyRangesCheckOut = true ;
var j = 0 ;
2011-07-07 19:59:34 +02:00
var a , b ;
while ( j < dirtyRanges . length )
{
2011-03-26 14:10:41 +01:00
a = dirtyRanges [ j ] [ 0 ] ;
b = dirtyRanges [ j ] [ 1 ] ;
2012-02-19 14:52:24 +01:00
if ( ! ( ( a === 0 || getCleanNodeByKey ( rep . lines . atIndex ( a - 1 ) . key ) ) && ( b == rep . lines . length ( ) || getCleanNodeByKey ( rep . lines . atIndex ( b ) . key ) ) ) )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
dirtyRangesCheckOut = false ;
break ;
}
j ++ ;
}
2011-07-07 19:59:34 +02:00
if ( ! dirtyRangesCheckOut )
{
2011-03-26 14:10:41 +01:00
var numBodyNodes = root . childNodes . length ;
2011-07-07 19:59:34 +02:00
for ( var k = 0 ; k < numBodyNodes ; k ++ )
{
2011-03-26 14:10:41 +01:00
var bodyNode = root . childNodes . item ( k ) ;
2011-07-07 19:59:34 +02:00
if ( ( bodyNode . tagName ) && ( ( ! bodyNode . id ) || ( ! rep . lines . containsKey ( bodyNode . id ) ) ) )
{
2011-03-26 14:10:41 +01:00
observeChangesAroundNode ( bodyNode ) ;
}
}
dirtyRanges = getDirtyRanges ( ) ;
}
clearObservedChanges ( ) ;
p . mark ( "getsel" ) ;
var selection = getSelection ( ) ;
var selStart , selEnd ; // each one, if truthy, has [line,char] needed to set selection
var i = 0 ;
var splicesToDo = [ ] ;
var netNumLinesChangeSoFar = 0 ;
var toDeleteAtEnd = [ ] ;
p . mark ( "ranges" ) ;
p . literal ( dirtyRanges . length , "numdirt" ) ;
var domInsertsNeeded = [ ] ; // each entry is [nodeToInsertAfter, [info1, info2, ...]]
2011-07-07 19:59:34 +02:00
while ( i < dirtyRanges . length )
{
2011-03-26 14:10:41 +01:00
var range = dirtyRanges [ i ] ;
a = range [ 0 ] ;
b = range [ 1 ] ;
2012-02-19 14:52:24 +01:00
var 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 ) ;
2011-07-07 19:59:34 +02:00
var 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 ) ;
2011-07-07 19:59:34 +02:00
if ( firstDirtyNode && lastDirtyNode )
{
2015-01-21 16:01:39 +01:00
var cc = makeContentCollector ( isStyled , browser , rep . apool , null , className2Author ) ;
2011-07-07 19:59:34 +02:00
cc . notifySelection ( selection ) ;
var dirtyNodes = [ ] ;
for ( var n = firstDirtyNode ; n && ! ( n . previousSibling && n . previousSibling == lastDirtyNode ) ;
n = n . nextSibling )
{
2015-01-21 16:01:39 +01:00
if ( browser . msie )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
// try to undo IE's pesky and overzealous linkification
2011-07-07 19:59:34 +02:00
try
{
n . createTextRange ( ) . execCommand ( "unlink" , false , null ) ;
}
catch ( e )
{ }
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
cc . collectContent ( n ) ;
dirtyNodes . push ( n ) ;
}
cc . notifyNextNode ( lastDirtyNode . nextSibling ) ;
var lines = cc . getLines ( ) ;
if ( ( lines . length <= 1 || lines [ lines . length - 1 ] !== "" ) && lastDirtyNode . nextSibling )
{
// dirty region doesn't currently end a line, even taking the following node
// (or lack of node) into account, so include the following clean node.
// It could be SPAN or a DIV; basically this is any case where the contentCollector
// decides it isn't done.
// Note that this clean node might need to be there for the next dirty range.
b ++ ;
var cleanLine = lastDirtyNode . nextSibling ;
cc . collectContent ( cleanLine ) ;
toDeleteAtEnd . push ( cleanLine ) ;
cc . notifyNextNode ( cleanLine . nextSibling ) ;
}
2011-03-26 14:10:41 +01:00
var ccData = cc . finish ( ) ;
var ss = ccData . selStart ;
var se = ccData . selEnd ;
lines = ccData . lines ;
var lineAttribs = ccData . lineAttribs ;
var linesWrapped = ccData . linesWrapped ;
2013-02-17 22:03:19 +01:00
var scrollToTheLeftNeeded = false ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
if ( linesWrapped > 0 )
{
2015-01-21 16:01:39 +01:00
if ( ! browser . msie ) {
2013-02-17 22:03:19 +01:00
// chrome decides in it's infinite wisdom that its okay to put the browsers visisble window in the middle of the span
// an outcome of this is that the first chars of the string are no longer visible to the user.. Yay chrome..
// Move the browsers visible area to the left hand side of the span
2013-02-17 22:19:15 +01:00
// Firefox isn't quite so bad, but it's still pretty quirky.
2013-02-17 22:03:19 +01:00
var scrollToTheLeftNeeded = true ;
}
2011-07-07 19:59:34 +02:00
}
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
if ( ss [ 0 ] >= 0 ) selStart = [ ss [ 0 ] + a + netNumLinesChangeSoFar , ss [ 1 ] ] ;
if ( se [ 0 ] >= 0 ) selEnd = [ se [ 0 ] + a + netNumLinesChangeSoFar , se [ 1 ] ] ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
var entries = [ ] ;
var nodeToAddAfter = lastDirtyNode ;
var lineNodeInfos = new Array ( lines . length ) ;
for ( var k = 0 ; k < lines . length ; k ++ )
{
2011-03-26 14:10:41 +01:00
var lineString = lines [ k ] ;
2011-07-07 19:59:34 +02:00
var newEntry = createDomLineEntry ( lineString ) ;
entries . push ( newEntry ) ;
lineNodeInfos [ k ] = newEntry . domInfo ;
}
//var fragment = magicdom.wrapDom(document.createDocumentFragment());
domInsertsNeeded . push ( [ nodeToAddAfter , lineNodeInfos ] ) ;
2012-03-17 13:36:42 +01:00
_ . each ( dirtyNodes , function ( n ) {
2011-07-07 19:59:34 +02:00
toDeleteAtEnd . push ( n ) ;
} ) ;
var spliceHints = { } ;
if ( selStart ) spliceHints . selStart = selStart ;
if ( selEnd ) spliceHints . selEnd = selEnd ;
splicesToDo . push ( [ a + netNumLinesChangeSoFar , b - a , entries , lineAttribs , spliceHints ] ) ;
netNumLinesChangeSoFar += ( lines . length - ( b - a ) ) ;
}
else if ( b > a )
{
splicesToDo . push ( [ a + netNumLinesChangeSoFar , b - a , [ ] ,
[ ]
] ) ;
2011-03-26 14:10:41 +01:00
}
i ++ ;
}
var domChanges = ( splicesToDo . length > 0 ) ;
// update the representation
p . mark ( "splice" ) ;
2012-03-17 13:36:42 +01:00
_ . each ( splicesToDo , function ( splice )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
doIncorpLineSplice ( splice [ 0 ] , splice [ 1 ] , splice [ 2 ] , splice [ 3 ] , splice [ 4 ] ) ;
} ) ;
//p.mark("relex");
2018-01-03 22:57:28 +01:00
//rep.lexer.lexCharRange(scroll.getVisibleCharRange(rep), function() { return false; });
2011-03-26 14:10:41 +01:00
//var isTimeUp = newTimeLimit(100);
// do DOM inserts
p . mark ( "insert" ) ;
2012-03-17 13:36:42 +01:00
_ . each ( domInsertsNeeded , function ( ins )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
insertDomLines ( ins [ 0 ] , ins [ 1 ] , isTimeUp ) ;
} ) ;
p . mark ( "del" ) ;
// delete old dom nodes
2012-03-17 13:36:42 +01:00
_ . each ( toDeleteAtEnd , function ( n )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
//var id = n.uniqueId();
// parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf)
2013-12-18 19:34:35 +01:00
if ( n . parentNode ) n . parentNode . removeChild ( n ) ;
2011-03-26 14:10:41 +01:00
//dmesg(htmlPrettyEscape(htmlForRemovedChild(n)));
} ) ;
2013-02-17 22:03:19 +01:00
if ( scrollToTheLeftNeeded ) { // needed to stop chrome from breaking the ui when long strings without spaces are pasted
$ ( "#innerdocbody" ) . scrollLeft ( 0 ) ;
}
2011-03-26 14:10:41 +01:00
p . mark ( "findsel" ) ;
// if the nodes that define the selection weren't encountered during
// content collection, figure out where those nodes are now.
2011-07-07 19:59:34 +02:00
if ( selection && ! selStart )
{
2011-03-26 14:10:41 +01:00
//if (domChanges) dmesg("selection not collected");
2012-09-08 20:45:33 +02:00
var selStartFromHook = hooks . callAll ( 'aceStartLineAndCharForPoint' , {
callstack : currentCallStack ,
editorInfo : editorInfo ,
rep : rep ,
root : root ,
point : selection . startPoint ,
documentAttributeManager : documentAttributeManager
2013-06-14 19:37:41 +02:00
} ) ;
2012-09-11 23:21:14 +02:00
selStart = ( selStartFromHook == null || selStartFromHook . length == 0 ) ? getLineAndCharForPoint ( selection . startPoint ) : selStartFromHook ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
if ( selection && ! selEnd )
{
2012-09-08 20:45:33 +02:00
var selEndFromHook = hooks . callAll ( 'aceEndLineAndCharForPoint' , {
callstack : currentCallStack ,
editorInfo : editorInfo ,
rep : rep ,
root : root ,
point : selection . endPoint ,
documentAttributeManager : documentAttributeManager
} ) ;
2013-06-14 19:37:41 +02: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
var numLines = rep . lines . length ( ) ;
2011-07-07 19:59:34 +02:00
if ( selStart && selStart [ 0 ] >= numLines )
{
selStart [ 0 ] = numLines - 1 ;
2011-03-26 14:10:41 +01:00
selStart [ 1 ] = rep . lines . atIndex ( selStart [ 0 ] ) . text . length ;
}
2011-07-07 19:59:34 +02:00
if ( selEnd && selEnd [ 0 ] >= numLines )
{
selEnd [ 0 ] = numLines - 1 ;
2011-03-26 14:10:41 +01:00
selEnd [ 1 ] = rep . lines . atIndex ( selEnd [ 0 ] ) . text . length ;
}
p . mark ( "repsel" ) ;
// update rep if we have a new selection
// NOTE: IE loses the selection when you click stuff in e.g. the
// editbar, so removing the selection when it's lost is not a good
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
p . mark ( "browsel" ) ;
2011-07-07 19:59:34 +02:00
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 ;
p . mark ( "fixview" ) ;
fixView ( ) ;
p . end ( "END" ) ;
return domChanges ;
}
2011-07-07 19:59:34 +02:00
var STYLE _ATTRIBS = {
bold : true ,
italic : true ,
underline : true ,
strikethrough : true ,
list : true
} ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
function isStyleAttribute ( aname )
{
return ! ! STYLE _ATTRIBS [ aname ] ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
2018-07-09 22:44:38 +02:00
function isDefaultLineAttribute ( aname )
2011-07-07 19:59:34 +02:00
{
2018-07-09 22:44:38 +02:00
return AttributeManager . DEFAULT _LINE _ATTRIBUTES . indexOf ( aname ) !== - 1 ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function insertDomLines ( nodeToAddAfter , infoStructs , isTimeUp )
{
isTimeUp = ( isTimeUp ||
function ( )
{
return false ;
} ) ;
2011-03-26 14:10:41 +01:00
var lastEntry ;
var lineStartOffset ;
if ( infoStructs . length < 1 ) return ;
var startEntry = rep . lines . atKey ( uniqueId ( infoStructs [ 0 ] . node ) ) ;
2011-07-07 19:59:34 +02:00
var endEntry = rep . lines . atKey ( uniqueId ( infoStructs [ infoStructs . length - 1 ] . node ) ) ;
2011-03-26 14:10:41 +01:00
var charStart = rep . lines . offsetOfEntry ( startEntry ) ;
var charEnd = rep . lines . offsetOfEntry ( endEntry ) + endEntry . width ;
//rep.lexer.lexCharRange([charStart, charEnd], isTimeUp);
2012-03-17 13:36:42 +01:00
_ . each ( infoStructs , function ( info )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
var p2 = PROFILER ( "insertLine" , false ) ;
var node = info . node ;
var key = uniqueId ( node ) ;
var entry ;
p2 . mark ( "findEntry" ) ;
2011-07-07 19:59:34 +02:00
if ( lastEntry )
{
// optimization to avoid recalculation
var next = rep . lines . next ( lastEntry ) ;
if ( next && next . key == key )
{
entry = next ;
lineStartOffset += lastEntry . width ;
}
}
if ( ! entry )
{
p2 . literal ( 1 , "nonopt" ) ;
entry = rep . lines . atKey ( key ) ;
lineStartOffset = rep . lines . offsetOfKey ( key ) ;
2011-03-26 14:10:41 +01:00
}
else p2 . literal ( 0 , "nonopt" ) ;
lastEntry = entry ;
p2 . mark ( "spans" ) ;
2011-07-07 19:59:34 +02:00
getSpansForLine ( entry , function ( tokenText , tokenClass )
{
info . appendSpan ( tokenText , tokenClass ) ;
2011-03-26 14:10:41 +01:00
} , lineStartOffset , isTimeUp ( ) ) ;
//else if (entry.text.length > 0) {
2011-07-07 19:59:34 +02:00
//info.appendSpan(entry.text, 'dirty');
2011-03-26 14:10:41 +01:00
//}
p2 . mark ( "addLine" ) ;
info . prepareForAdd ( ) ;
entry . lineMarker = info . lineMarker ;
2011-07-07 19:59:34 +02:00
if ( ! nodeToAddAfter )
{
root . insertBefore ( node , root . firstChild ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
root . insertBefore ( node , nodeToAddAfter . nextSibling ) ;
2011-03-26 14:10:41 +01:00
}
nodeToAddAfter = node ;
info . notifyAdded ( ) ;
p2 . mark ( "markClean" ) ;
markNodeClean ( node ) ;
p2 . end ( ) ;
} ) ;
}
2011-07-07 19:59:34 +02:00
function isCaret ( )
{
return ( rep . selStart && rep . selEnd && rep . selStart [ 0 ] == rep . selEnd [ 0 ] && rep . selStart [ 1 ] == rep . selEnd [ 1 ] ) ;
2011-03-26 14:10:41 +01:00
}
editorInfo . ace _isCaret = isCaret ;
// prereq: isCaret()
2011-07-07 19:59:34 +02:00
function caretLine ( )
{
return rep . selStart [ 0 ] ;
}
2012-09-08 20:45:33 +02:00
editorInfo . ace _caretLine = caretLine ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
function caretColumn ( )
{
return rep . selStart [ 1 ] ;
}
2012-09-08 20:45:33 +02:00
editorInfo . ace _caretColumn = caretColumn ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
function caretDocChar ( )
{
2011-03-26 14:10:41 +01:00
return 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
2011-07-07 19:59:34 +02:00
function handleReturnIndentation ( )
{
2011-03-26 14:10:41 +01:00
// on return, indent to level of previous line
2012-02-19 14:52:24 +01:00
if ( isCaret ( ) && caretColumn ( ) === 0 && caretLine ( ) > 0 )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
var lineNum = caretLine ( ) ;
var thisLine = rep . lines . atIndex ( lineNum ) ;
var prevLine = rep . lines . prev ( thisLine ) ;
var prevLineText = prevLine . text ;
var theIndent = /^ *(?:)/ . exec ( prevLineText ) [ 0 ] ;
2015-10-13 23:39:23 +02:00
var shouldIndent = parent . parent . clientVars . indentationOnNewLine ;
if ( shouldIndent && /[\[\(\:\{]\s*$/ . exec ( prevLineText ) )
{
theIndent += THE _TAB ;
}
2011-03-26 14:10:41 +01:00
var cs = Changeset . builder ( rep . lines . totalWidth ( ) ) . keep (
2011-07-07 19:59:34 +02:00
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 ] ) ;
}
}
2011-07-07 19:59:34 +02:00
function getPointForLineAndChar ( lineAndChar )
{
2011-03-26 14:10:41 +01:00
var line = lineAndChar [ 0 ] ;
var charsLeft = lineAndChar [ 1 ] ;
2020-05-29 13:56:03 +02:00
// Do not uncomment this in production it will break iFrames.
2020-04-22 22:19:40 +02:00
//top.console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key,
2011-03-26 14:10:41 +01:00
//getCleanNodeByKey(rep.lines.atIndex(line).key));
var lineEntry = rep . lines . atIndex ( line ) ;
charsLeft -= lineEntry . lineMarker ;
2011-07-07 19:59:34 +02:00
if ( charsLeft < 0 )
{
2011-03-26 14:10:41 +01:00
charsLeft = 0 ;
}
var lineNode = lineEntry . lineNode ;
var n = lineNode ;
var after = false ;
2012-02-19 14:52:24 +01:00
if ( charsLeft === 0 )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
var index = 0 ;
2015-04-12 14:00:01 +02:00
if ( browser . msie && parseInt ( browser . version ) >= 11 ) {
browser . msie = false ; // Temp fix to resolve enter and backspace issues..
// Note that this makes MSIE behave like modern browsers..
}
2015-01-21 16:01:39 +01:00
if ( browser . msie && line == ( rep . lines . length ( ) - 1 ) && lineNode . childNodes . length === 0 )
2011-07-07 19:59:34 +02:00
{
// best to stay at end of last empty div in IE
index = 1 ;
}
return {
node : lineNode ,
index : index ,
maxIndex : 1
} ;
}
while ( ! ( n == lineNode && after ) )
{
if ( after )
{
if ( n . nextSibling )
{
n = n . nextSibling ;
after = false ;
}
else n = n . parentNode ;
}
else
{
if ( isNodeText ( n ) )
{
var len = n . nodeValue . length ;
if ( charsLeft <= len )
{
return {
node : n ,
index : charsLeft ,
maxIndex : len
} ;
}
charsLeft -= len ;
after = true ;
}
else
{
if ( n . firstChild ) n = n . firstChild ;
else after = true ;
}
}
}
return {
node : lineNode ,
index : 1 ,
maxIndex : 1
} ;
}
function nodeText ( n )
{
2015-05-07 16:47:14 +02:00
if ( browser . msie ) {
return n . innerText ;
} else {
return n . textContent || n . nodeValue || '' ;
}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function getLineAndCharForPoint ( point )
{
2011-03-26 14:10:41 +01:00
// Turn DOM node selection into [line,char] selection.
// This method has to work when the DOM is not pristine,
// assuming the point is not in a dirty node.
2011-07-07 19:59:34 +02:00
if ( point . node == root )
{
2012-02-19 14:52:24 +01:00
if ( point . index === 0 )
2011-07-07 19:59:34 +02:00
{
return [ 0 , 0 ] ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
var N = rep . lines . length ( ) ;
var ln = rep . lines . atIndex ( N - 1 ) ;
return [ N - 1 , ln . text . length ] ;
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
var n = point . node ;
var col = 0 ;
// if this part fails, it probably means the selection node
// was dirty, and we didn't see it when collecting dirty nodes.
2011-07-07 19:59:34 +02:00
if ( isNodeText ( n ) )
{
col = point . index ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else if ( point . index > 0 )
{
col = nodeText ( n ) . length ;
2011-03-26 14:10:41 +01:00
}
var parNode , prevSib ;
2011-07-07 19:59:34 +02:00
while ( ( parNode = n . parentNode ) != root )
{
if ( ( prevSib = n . previousSibling ) )
{
n = prevSib ;
col += nodeText ( n ) . length ;
}
else
{
n = parNode ;
}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
if ( n . firstChild && isBlockElement ( n . firstChild ) )
{
2011-03-26 14:10:41 +01:00
col += 1 ; // lineMarker
}
var lineEntry = rep . lines . atKey ( n . id ) ;
var lineNum = rep . lines . indexOfEntry ( lineEntry ) ;
return [ lineNum , col ] ;
}
}
2012-03-27 22:24:16 +02:00
editorInfo . ace _getLineAndCharForPoint = getLineAndCharForPoint ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
function createDomLineEntry ( lineString )
{
2011-03-26 14:10:41 +01:00
var info = doCreateDomLine ( lineString . length > 0 ) ;
var newNode = info . node ;
2011-07-07 19:59:34 +02:00
return {
key : uniqueId ( newNode ) ,
text : lineString ,
lineNode : newNode ,
domInfo : info ,
lineMarker : 0
} ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function canApplyChangesetToDocument ( changes )
{
2011-03-26 14:10:41 +01:00
return Changeset . oldLen ( changes ) == rep . alltext . length ;
}
2011-07-07 19:59:34 +02:00
function performDocumentApplyChangeset ( changes , insertsAfterSelection )
{
2011-03-26 14:10:41 +01:00
doRepApplyChangeset ( changes , insertsAfterSelection ) ;
var requiredSelectionSetting = null ;
2011-07-07 19:59:34 +02:00
if ( rep . selStart && rep . selEnd )
{
2011-03-26 14:10:41 +01:00
var selStartChar = rep . lines . offsetOfIndex ( rep . selStart [ 0 ] ) + rep . selStart [ 1 ] ;
var selEndChar = rep . lines . offsetOfIndex ( rep . selEnd [ 0 ] ) + rep . selEnd [ 1 ] ;
2011-07-07 19:59:34 +02:00
var result = Changeset . characterRangeFollow ( changes , selStartChar , selEndChar , insertsAfterSelection ) ;
2011-03-26 14:10:41 +01:00
requiredSelectionSetting = [ result [ 0 ] , result [ 1 ] , rep . selFocusAtStart ] ;
}
var linesMutatee = {
2011-07-07 19:59:34 +02:00
splice : function ( start , numRemoved , newLinesVA )
{
2012-03-13 21:10:10 +01:00
var args = Array . prototype . slice . call ( arguments , 2 ) ;
2012-03-17 13:36:42 +01:00
domAndRepSplice ( start , numRemoved , _ . map ( args , function ( s ) { return s . slice ( 0 , - 1 ) ; } ) , null ) ;
2011-07-07 19:59:34 +02:00
} ,
get : function ( i )
{
return rep . lines . atIndex ( i ) . text + '\n' ;
} ,
length : function ( )
{
return rep . lines . length ( ) ;
2011-03-26 14:10:41 +01:00
} ,
2011-07-07 19:59:34 +02:00
slice _notused : function ( start , end )
{
2012-03-17 13:36:42 +01:00
return _ . map ( rep . lines . slice ( start , end ) , function ( e )
2011-07-07 19:59:34 +02:00
{
return e . text + '\n' ;
} ) ;
2011-03-26 14:10:41 +01:00
}
} ;
Changeset . mutateTextLines ( changes , linesMutatee ) ;
checkALines ( ) ;
2011-07-07 19:59:34 +02:00
if ( requiredSelectionSetting )
{
performSelectionChange ( lineAndColumnFromChar ( requiredSelectionSetting [ 0 ] ) , lineAndColumnFromChar ( requiredSelectionSetting [ 1 ] ) , requiredSelectionSetting [ 2 ] ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function domAndRepSplice ( startLine , deleteCount , newLineStrings , isTimeUp )
{
2011-03-26 14:10:41 +01:00
// dgreensp 3/2009: the spliced lines may be in the middle of a dirty region,
// so if no explicit time limit, don't spend a lot of time highlighting
isTimeUp = ( isTimeUp || newTimeLimit ( 50 ) ) ;
var keysToDelete = [ ] ;
2011-07-07 19:59:34 +02:00
if ( deleteCount > 0 )
{
var entryToDelete = rep . lines . atIndex ( startLine ) ;
for ( var i = 0 ; i < deleteCount ; i ++ )
{
keysToDelete . push ( entryToDelete . key ) ;
entryToDelete = rep . lines . next ( entryToDelete ) ;
}
2011-03-26 14:10:41 +01:00
}
2012-03-17 13:36:42 +01:00
var lineEntries = _ . map ( newLineStrings , createDomLineEntry ) ;
2011-03-26 14:10:41 +01:00
doRepLineSplice ( startLine , deleteCount , lineEntries ) ;
var nodeToAddAfter ;
2011-07-07 19:59:34 +02:00
if ( startLine > 0 )
{
nodeToAddAfter = getCleanNodeByKey ( rep . lines . atIndex ( startLine - 1 ) . key ) ;
2011-03-26 14:10:41 +01:00
}
else nodeToAddAfter = null ;
2012-03-17 13:36:42 +01:00
insertDomLines ( nodeToAddAfter , _ . map ( lineEntries , function ( entry )
2011-07-07 19:59:34 +02:00
{
return entry . domInfo ;
} ) , isTimeUp ) ;
2011-03-26 14:10:41 +01:00
2012-03-17 13:36:42 +01:00
_ . each ( keysToDelete , function ( k )
2011-07-07 19:59:34 +02:00
{
var n = doc . getElementById ( k ) ;
n . parentNode . removeChild ( n ) ;
2011-03-26 14:10:41 +01:00
} ) ;
2011-07-07 19:59:34 +02:00
if ( ( rep . selStart && rep . selStart [ 0 ] >= startLine && rep . selStart [ 0 ] <= startLine + deleteCount ) || ( rep . selEnd && rep . selEnd [ 0 ] >= startLine && rep . selEnd [ 0 ] <= startLine + deleteCount ) )
{
currentCallStack . selectionAffected = true ;
2011-03-26 14:10:41 +01:00
}
}
}
2011-07-07 19:59:34 +02:00
function checkChangesetLineInformationAgainstRep ( changes )
{
2011-03-26 14:10:41 +01:00
return true ; // disable for speed
var opIter = Changeset . opIterator ( Changeset . unpack ( changes ) . ops ) ;
var curOffset = 0 ;
var curLine = 0 ;
var curCol = 0 ;
2011-07-07 19:59:34 +02:00
while ( opIter . hasNext ( ) )
{
2011-03-26 14:10:41 +01:00
var o = opIter . next ( ) ;
2011-07-07 19:59:34 +02:00
if ( o . opcode == '-' || o . opcode == '=' )
{
curOffset += o . chars ;
if ( o . lines )
{
curLine += o . lines ;
curCol = 0 ;
}
else
{
curCol += o . chars ;
}
2011-03-26 14:10:41 +01:00
}
var calcLine = rep . lines . indexOfOffset ( curOffset ) ;
var calcLineStart = rep . lines . offsetOfIndex ( calcLine ) ;
var calcCol = curOffset - calcLineStart ;
2011-07-07 19:59:34 +02:00
if ( calcCol != curCol || calcLine != curLine )
{
return false ;
2011-03-26 14:10:41 +01:00
}
}
return true ;
}
2011-07-07 19:59:34 +02:00
function doRepApplyChangeset ( changes , insertsAfterSelection )
{
2011-03-26 14:10:41 +01:00
Changeset . checkRep ( changes ) ;
2011-07-07 19:59:34 +02:00
if ( Changeset . oldLen ( changes ) != rep . alltext . length ) throw new Error ( "doRepApplyChangeset length mismatch: " + Changeset . oldLen ( changes ) + "/" + rep . alltext . length ) ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
if ( ! checkChangesetLineInformationAgainstRep ( changes ) )
{
2011-03-26 14:10:41 +01:00
throw new Error ( "doRepApplyChangeset line break mismatch" ) ;
}
2011-07-07 19:59:34 +02:00
( function doRecordUndoInformation ( changes )
{
2011-03-26 14:10:41 +01:00
var editEvent = currentCallStack . editEvent ;
2011-07-07 19:59:34 +02:00
if ( editEvent . eventType == "nonundoable" )
{
if ( ! editEvent . changeset )
{
editEvent . changeset = changes ;
}
else
{
editEvent . changeset = Changeset . compose ( editEvent . changeset , changes , rep . apool ) ;
}
}
else
{
var inverseChangeset = Changeset . inverse ( changes , {
get : function ( i )
{
return rep . lines . atIndex ( i ) . text + '\n' ;
} ,
length : function ( )
{
return rep . lines . length ( ) ;
}
} , rep . alines , rep . apool ) ;
if ( ! editEvent . backset )
{
editEvent . backset = inverseChangeset ;
}
else
{
editEvent . backset = Changeset . compose ( inverseChangeset , editEvent . backset , rep . apool ) ;
}
2011-03-26 14:10:41 +01:00
}
} ) ( changes ) ;
//rep.alltext = Changeset.applyToText(changes, rep.alltext);
Changeset . mutateAttributionLines ( changes , rep . alines , rep . apool ) ;
2011-07-07 19:59:34 +02:00
if ( changesetTracker . isTracking ( ) )
{
2011-03-26 14:10:41 +01:00
changesetTracker . composeUserChangeset ( changes ) ;
}
}
2012-04-05 15:25:17 +02:00
/ *
Converts the position of a char ( index in String ) into a [ row , col ] tuple
* /
2011-07-07 19:59:34 +02:00
function lineAndColumnFromChar ( x )
{
2011-03-26 14:10:41 +01:00
var lineEntry = rep . lines . atOffset ( x ) ;
var lineStart = rep . lines . offsetOfEntry ( lineEntry ) ;
var lineNum = rep . lines . indexOfEntry ( lineEntry ) ;
return [ lineNum , x - lineStart ] ;
}
2011-07-07 19:59:34 +02:00
function performDocumentReplaceCharRange ( startChar , endChar , newText )
{
2012-02-19 14:52:24 +01:00
if ( startChar == endChar && newText . length === 0 )
2011-07-07 19:59:34 +02:00
{
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.
2011-07-07 19:59:34 +02:00
if ( endChar == rep . alltext . length )
{
if ( startChar == endChar )
{
// an insert at end
startChar -- ;
endChar -- ;
newText = '\n' + newText . substring ( 0 , newText . length - 1 ) ;
2011-03-26 14:10:41 +01:00
}
2012-02-19 14:52:24 +01:00
else if ( newText . length === 0 )
2011-07-07 19:59:34 +02:00
{
// a delete at end
startChar -- ;
endChar -- ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
// a replace at end
endChar -- ;
newText = newText . substring ( 0 , newText . length - 1 ) ;
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
performDocumentReplaceRange ( lineAndColumnFromChar ( startChar ) , lineAndColumnFromChar ( endChar ) , newText ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function performDocumentReplaceRange ( start , end , newText )
{
2012-02-19 14:52:24 +01:00
if ( start === undefined ) start = rep . selStart ;
if ( end === undefined ) end = rep . selEnd ;
2011-03-26 14:10:41 +01:00
//dmesg(String([start.toSource(),end.toSource(),newText.toSource()]));
// start[0]: <--- start[1] --->CCCCCCCCCCC\n
// CCCCCCCCCCCCCCCCCCCC\n
// CCCC\n
// end[0]: <CCC end[1] CCC>-------\n
var builder = Changeset . builder ( rep . lines . totalWidth ( ) ) ;
2012-04-05 00:50:04 +02:00
ChangesetUtils . buildKeepToStartOfRange ( rep , builder , start ) ;
ChangesetUtils . buildRemoveRange ( rep , builder , start , end ) ;
2011-07-07 19:59:34 +02:00
builder . insert ( newText , [
[ 'author' , thisAuthor ]
] , rep . apool ) ;
2011-03-26 14:10:41 +01:00
var cs = builder . toString ( ) ;
performDocumentApplyChangeset ( cs ) ;
}
2011-07-07 19:59:34 +02:00
function performDocumentApplyAttributesToCharRange ( start , end , attribs )
{
2012-04-05 15:26:51 +02:00
end = Math . min ( end , rep . alltext . length - 1 ) ;
documentAttributeManager . setAttributesOnRange ( lineAndColumnFromChar ( start ) , lineAndColumnFromChar ( end ) , attribs ) ;
2011-03-26 14:10:41 +01:00
}
editorInfo . ace _performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
function 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 , [
2011-07-07 19:59:34 +02:00
[ attributeName , attributeValue ]
] ) ;
2011-03-26 14:10:41 +01:00
}
editorInfo . ace _setAttributeOnSelection = setAttributeOnSelection ;
2014-12-27 00:42:00 +01:00
2016-03-26 15:01:26 +01:00
function getAttributeOnSelection ( attributeName , prevChar ) {
2014-12-31 19:23:09 +01:00
if ( ! ( rep . selStart && rep . selEnd ) ) return
2016-03-26 15:01:26 +01:00
var isNotSelection = ( rep . selStart [ 0 ] == rep . selEnd [ 0 ] && rep . selEnd [ 1 ] === rep . selStart [ 1 ] ) ;
if ( isNotSelection ) {
if ( prevChar ) {
// If it's not the start of the line
if ( rep . selStart [ 1 ] !== 0 ) {
rep . selStart [ 1 ] -- ;
}
}
}
2015-10-13 23:39:23 +02:00
2013-11-28 18:27:52 +01:00
var withIt = Changeset . makeAttribsString ( '+' , [
[ attributeName , 'true' ]
] , rep . apool ) ;
var withItRegex = new RegExp ( withIt . replace ( /\*/g , '\\*' ) + "(\\*|$)" ) ;
function hasIt ( attribs )
{
return withItRegex . test ( attribs ) ;
}
2014-12-31 19:23:09 +01:00
return rangeHasAttrib ( rep . selStart , rep . selEnd )
2015-10-13 23:39:23 +02:00
2014-12-31 19:23:09 +01:00
function rangeHasAttrib ( selStart , selEnd ) {
// if range is collapsed -> no attribs in range
if ( selStart [ 1 ] == selEnd [ 1 ] && selStart [ 0 ] == selEnd [ 0 ] ) return false
2015-10-13 23:39:23 +02:00
2014-12-31 19:23:09 +01:00
if ( selStart [ 0 ] != selEnd [ 0 ] ) { // -> More than one line selected
var 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
hasAttrib = hasAttrib && rangeHasAttrib ( selStart , [ selStart [ 0 ] , rep . lines . atIndex ( selStart [ 0 ] ) . text . length ] )
// for all lines in between
for ( var n = selStart [ 0 ] + 1 ; n < selEnd [ 0 ] ; n ++ ) {
hasAttrib = hasAttrib && rangeHasAttrib ( [ n , 0 ] , [ n , rep . lines . atIndex ( n ) . text . length ] )
}
// for the last, potentially partial, line
hasAttrib = hasAttrib && rangeHasAttrib ( [ selEnd [ 0 ] , 0 ] , [ selEnd [ 0 ] , selEnd [ 1 ] ] )
2015-10-13 23:39:23 +02:00
2014-12-31 19:23:09 +01:00
return hasAttrib
}
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
2014-12-31 19:23:09 +01:00
var lineNum = selStart [ 0 ]
, start = selStart [ 1 ]
, end = selEnd [ 1 ]
, 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
2014-12-31 19:23:09 +01:00
var opIter = Changeset . opIterator ( rep . alines [ lineNum ] )
, indexIntoLine = 0
2015-10-13 23:39:23 +02:00
2014-12-31 19:23:09 +01:00
while ( opIter . hasNext ( ) ) {
2013-11-28 18:27:52 +01:00
var op = opIter . next ( ) ;
var opStartInLine = indexIntoLine ;
var 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 ) ) {
hasAttrib = false ; // since it's overlapping but hasn't got the attrib -> range hasn't got it
2013-11-28 18:27:52 +01:00
break ;
}
}
indexIntoLine = opEndInLine ;
}
2015-10-13 23:39:23 +02:00
2014-12-31 19:23:09 +01:00
return hasAttrib
2013-11-28 18:27:52 +01:00
}
}
2015-10-13 23:39:23 +02:00
2013-11-28 18:27:52 +01:00
editorInfo . ace _getAttributeOnSelection = getAttributeOnSelection ;
2011-07-07 19:59:34 +02:00
function toggleAttributeOnSelection ( attributeName )
{
2011-03-26 14:10:41 +01:00
if ( ! ( rep . selStart && rep . selEnd ) ) return ;
var selectionAllHasIt = true ;
2011-07-07 19:59:34 +02:00
var withIt = Changeset . makeAttribsString ( '+' , [
[ attributeName , 'true' ]
] , rep . apool ) ;
var withItRegex = new RegExp ( withIt . replace ( /\*/g , '\\*' ) + "(\\*|$)" ) ;
function hasIt ( attribs )
{
return withItRegex . test ( attribs ) ;
}
2011-03-26 14:10:41 +01:00
var selStartLine = rep . selStart [ 0 ] ;
var selEndLine = rep . selEnd [ 0 ] ;
2011-07-07 19:59:34 +02:00
for ( var n = selStartLine ; n <= selEndLine ; n ++ )
{
2011-03-26 14:10:41 +01:00
var opIter = Changeset . opIterator ( rep . alines [ n ] ) ;
var indexIntoLine = 0 ;
var selectionStartInLine = 0 ;
2015-09-08 16:55:36 +02:00
if ( documentAttributeManager . lineHasMarker ( n ) ) {
selectionStartInLine = 1 ; // ignore "*" used as line marker
}
2011-03-26 14:10:41 +01:00
var selectionEndInLine = rep . lines . atIndex ( n ) . text . length ; // exclude newline
2011-07-07 19:59:34 +02:00
if ( n == selStartLine )
{
selectionStartInLine = rep . selStart [ 1 ] ;
}
if ( n == selEndLine )
{
selectionEndInLine = rep . selEnd [ 1 ] ;
}
while ( opIter . hasNext ( ) )
{
var op = opIter . next ( ) ;
var opStartInLine = indexIntoLine ;
var opEndInLine = opStartInLine + op . chars ;
if ( ! hasIt ( op . attribs ) )
{
// does op overlap selection?
if ( ! ( opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine ) )
{
selectionAllHasIt = false ;
break ;
}
}
indexIntoLine = opEndInLine ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
if ( ! selectionAllHasIt )
{
break ;
2011-03-26 14:10:41 +01:00
}
}
2018-01-04 15:28:00 +01:00
var attributeValue = selectionAllHasIt ? '' : 'true' ;
documentAttributeManager . setAttributesOnRange ( rep . selStart , rep . selEnd , [ [ attributeName , attributeValue ] ] ) ;
if ( attribIsFormattingStyle ( attributeName ) ) {
updateStyleButtonState ( attributeName , ! selectionAllHasIt ) ; // italic, bold, ...
2011-03-26 14:10:41 +01:00
}
}
editorInfo . ace _toggleAttributeOnSelection = toggleAttributeOnSelection ;
2011-07-07 19:59:34 +02:00
function performDocumentReplaceSelection ( newText )
{
2011-03-26 14:10:41 +01:00
if ( ! ( rep . selStart && rep . selEnd ) ) return ;
performDocumentReplaceRange ( rep . selStart , rep . selEnd , newText ) ;
}
// Change the abstract representation of the document to have a different set of lines.
// Must be called after rep.alltext is set.
2011-07-07 19:59:34 +02:00
function doRepLineSplice ( startLine , deleteCount , newLineEntries )
{
2012-03-17 13:36:42 +01:00
_ . each ( newLineEntries , function ( entry )
2011-07-07 19:59:34 +02:00
{
entry . width = entry . text . length + 1 ;
} ) ;
2011-03-26 14:10:41 +01:00
var startOldChar = rep . lines . offsetOfIndex ( startLine ) ;
2011-07-07 19:59:34 +02:00
var endOldChar = rep . lines . offsetOfIndex ( startLine + deleteCount ) ;
2011-03-26 14:10:41 +01:00
var oldRegionStart = rep . lines . offsetOfIndex ( startLine ) ;
2011-07-07 19:59:34 +02:00
var oldRegionEnd = rep . lines . offsetOfIndex ( startLine + deleteCount ) ;
2011-03-26 14:10:41 +01:00
rep . lines . splice ( startLine , deleteCount , newLineEntries ) ;
currentCallStack . docTextChanged = true ;
currentCallStack . repChanged = true ;
var newRegionEnd = rep . lines . offsetOfIndex ( startLine + newLineEntries . length ) ;
2012-03-17 13:36:42 +01:00
var newText = _ . map ( newLineEntries , function ( e )
2011-07-07 19:59:34 +02:00
{
return e . text + '\n' ;
} ) . join ( '' ) ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
rep . alltext = rep . alltext . substring ( 0 , startOldChar ) + newText + rep . alltext . substring ( endOldChar , rep . alltext . length ) ;
2011-03-26 14:10:41 +01:00
//var newTotalLength = rep.alltext.length;
//rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart,
//newRegionEnd - oldRegionStart);
}
2011-07-07 19:59:34 +02:00
function doIncorpLineSplice ( startLine , deleteCount , newLineEntries , lineAttribs , hints )
{
2011-03-26 14:10:41 +01:00
var startOldChar = rep . lines . offsetOfIndex ( startLine ) ;
2011-07-07 19:59:34 +02:00
var endOldChar = rep . lines . offsetOfIndex ( startLine + deleteCount ) ;
2011-03-26 14:10:41 +01:00
var oldRegionStart = rep . lines . offsetOfIndex ( startLine ) ;
var selStartHintChar , selEndHintChar ;
2011-07-07 19:59:34 +02:00
if ( hints && hints . selStart )
{
selStartHintChar = rep . lines . offsetOfIndex ( hints . selStart [ 0 ] ) + hints . selStart [ 1 ] - oldRegionStart ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
if ( hints && hints . selEnd )
{
selEndHintChar = rep . lines . offsetOfIndex ( hints . selEnd [ 0 ] ) + hints . selEnd [ 1 ] - oldRegionStart ;
2011-03-26 14:10:41 +01:00
}
2012-03-17 13:36:42 +01:00
var newText = _ . map ( newLineEntries , function ( e )
2011-07-07 19:59:34 +02:00
{
return e . text + '\n' ;
} ) . join ( '' ) ;
2011-03-26 14:10:41 +01:00
var oldText = rep . alltext . substring ( startOldChar , endOldChar ) ;
2011-07-07 19:59:34 +02:00
var oldAttribs = rep . alines . slice ( startLine , startLine + deleteCount ) . join ( '' ) ;
var newAttribs = lineAttribs . join ( '|1+1' ) + '|1+1' ; // not valid in a changeset
var analysis = analyzeChange ( oldText , newText , oldAttribs , newAttribs , selStartHintChar , selEndHintChar ) ;
2011-03-26 14:10:41 +01:00
var commonStart = analysis [ 0 ] ;
var commonEnd = analysis [ 1 ] ;
var shortOldText = oldText . substring ( commonStart , oldText . length - commonEnd ) ;
var shortNewText = newText . substring ( commonStart , newText . length - commonEnd ) ;
2011-07-07 19:59:34 +02:00
var spliceStart = startOldChar + commonStart ;
var spliceEnd = endOldChar - commonEnd ;
2011-03-26 14:10:41 +01:00
var shiftFinalNewlineToBeforeNewText = false ;
// adjust the splice to not involve the final newline of the document;
// be very defensive
2011-07-07 19:59:34 +02: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 ++ ;
}
2012-02-19 14:52:24 +01:00
if ( shortOldText . length === 0 && spliceStart == rep . alltext . length && shortNewText . length > 0 )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
// inserting after final newline, bad
spliceStart -- ;
spliceEnd -- ;
2011-07-07 19:59:34 +02:00
shortNewText = '\n' + shortNewText . slice ( 0 , - 1 ) ;
2011-03-26 14:10:41 +01:00
shiftFinalNewlineToBeforeNewText = true ;
}
2012-02-19 14:52:24 +01:00
if ( spliceEnd == rep . alltext . length && shortOldText . length > 0 && shortNewText . length === 0 )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
// deletion at end of rep.alltext
2011-07-07 19:59:34 +02:00
if ( rep . alltext . charAt ( spliceStart - 1 ) == '\n' )
{
// (if not then what the heck? it will definitely lead
// to a rep.alltext without a final newline)
spliceStart -- ;
spliceEnd -- ;
2011-03-26 14:10:41 +01:00
}
}
2012-02-19 14:52:24 +01:00
if ( ! ( shortOldText . length === 0 && shortNewText . length === 0 ) )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
var oldDocText = rep . alltext ;
var oldLen = oldDocText . length ;
var spliceStartLine = rep . lines . indexOfOffset ( spliceStart ) ;
var spliceStartLineStart = rep . lines . offsetOfIndex ( spliceStartLine ) ;
2011-07-07 19:59:34 +02:00
2012-02-19 14:52:24 +01:00
var startBuilder = function ( )
2011-07-07 19:59:34 +02:00
{
var builder = Changeset . builder ( oldLen ) ;
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
2012-02-19 14:52:24 +01:00
var eachAttribRun = function ( attribs , func /*(startInNewText, endInNewText, attribs)*/ )
2011-07-07 19:59:34 +02:00
{
var attribsIter = Changeset . opIterator ( attribs ) ;
var textIndex = 0 ;
var newTextStart = commonStart ;
var newTextEnd = newText . length - commonEnd - ( shiftFinalNewlineToBeforeNewText ? 1 : 0 ) ;
while ( attribsIter . hasNext ( ) )
{
var op = attribsIter . next ( ) ;
var nextIndex = textIndex + op . chars ;
if ( ! ( nextIndex <= newTextStart || textIndex >= newTextEnd ) )
{
func ( Math . max ( newTextStart , textIndex ) , Math . min ( newTextEnd , nextIndex ) , op . attribs ) ;
}
textIndex = nextIndex ;
}
2012-02-19 14:52:24 +01:00
} ;
2011-03-26 14:10:41 +01:00
var justApplyStyles = ( shortNewText == shortOldText ) ;
var theChangeset ;
2011-07-07 19:59:34 +02:00
if ( justApplyStyles )
{
// create changeset that clears the incorporated styles on
// the existing text. we compose this with the
// changeset the applies the styles found in the DOM.
// This allows us to incorporate, e.g., Safari's native "unbold".
var incorpedAttribClearer = cachedStrFunc ( function ( oldAtts )
{
return Changeset . mapAttribNumbers ( oldAtts , function ( n )
{
var k = rep . apool . getAttribKey ( n ) ;
if ( isStyleAttribute ( k ) )
{
return rep . apool . putAttrib ( [ k , '' ] ) ;
}
return false ;
} ) ;
} ) ;
var builder1 = startBuilder ( ) ;
if ( shiftFinalNewlineToBeforeNewText )
{
builder1 . keep ( 1 , 1 ) ;
}
eachAttribRun ( oldAttribs , function ( start , end , attribs )
{
builder1 . keepText ( newText . substring ( start , end ) , incorpedAttribClearer ( attribs ) ) ;
} ) ;
var clearer = builder1 . toString ( ) ;
var builder2 = startBuilder ( ) ;
if ( shiftFinalNewlineToBeforeNewText )
{
builder2 . keep ( 1 , 1 ) ;
}
eachAttribRun ( newAttribs , function ( start , end , attribs )
{
builder2 . keepText ( newText . substring ( start , end ) , attribs ) ;
} ) ;
var styler = builder2 . toString ( ) ;
theChangeset = Changeset . compose ( clearer , styler , rep . apool ) ;
}
else
{
var builder = startBuilder ( ) ;
var spliceEndLine = rep . lines . indexOfOffset ( spliceEnd ) ;
var spliceEndLineStart = rep . lines . offsetOfIndex ( spliceEndLine ) ;
if ( spliceEndLineStart > spliceStart )
{
builder . remove ( spliceEndLineStart - spliceStart , spliceEndLine - spliceStartLine ) ;
builder . remove ( spliceEnd - spliceEndLineStart ) ;
}
else
{
builder . remove ( spliceEnd - spliceStart ) ;
}
2011-03-26 14:10:41 +01:00
var isNewTextMultiauthor = false ;
2011-07-07 19:59:34 +02:00
var authorAtt = Changeset . makeAttribsString ( '+' , ( thisAuthor ? [
[ 'author' , thisAuthor ]
] : [ ] ) , rep . apool ) ;
var authorizer = cachedStrFunc ( function ( 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 ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02: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
var foundDomAuthor = '' ;
2011-07-07 19:59:34 +02:00
eachAttribRun ( newAttribs , function ( start , end , attribs )
{
2011-03-26 14:10:41 +01:00
var a = Changeset . attribsAttributeValue ( attribs , 'author' , rep . apool ) ;
2011-07-07 19:59:34 +02:00
if ( a && a != foundDomAuthor )
{
if ( ! foundDomAuthor )
{
2011-03-26 14:10:41 +01:00
foundDomAuthor = a ;
}
2011-07-07 19:59:34 +02: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
2011-07-07 19:59:34 +02:00
if ( shiftFinalNewlineToBeforeNewText )
{
builder . insert ( '\n' , authorizer ( '' ) ) ;
}
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
eachAttribRun ( newAttribs , function ( start , end , attribs )
{
builder . insert ( newText . substring ( start , end ) , authorizer ( attribs ) ) ;
} ) ;
theChangeset = builder . toString ( ) ;
2011-03-26 14:10:41 +01:00
}
//dmesg(htmlPrettyEscape(theChangeset));
doRepApplyChangeset ( theChangeset ) ;
}
// do this no matter what, because we need to get the right
// line keys into the rep.
doRepLineSplice ( startLine , deleteCount , newLineEntries ) ;
checkALines ( ) ;
}
2011-07-07 19:59:34 +02:00
function cachedStrFunc ( func )
{
2011-03-26 14:10:41 +01:00
var cache = { } ;
2011-07-07 19:59:34 +02:00
return function ( s )
{
if ( ! cache [ s ] )
{
cache [ s ] = func ( s ) ;
2011-03-26 14:10:41 +01:00
}
return cache [ s ] ;
} ;
}
2011-07-07 19:59:34 +02:00
function 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
2011-07-07 19:59:34 +02:00
function incorpedAttribFilter ( anum )
{
2018-07-09 22:44:38 +02:00
return ! isDefaultLineAttribute ( rep . apool . getAttribKey ( anum ) ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function attribRuns ( attribs )
{
2011-03-26 14:10:41 +01:00
var lengs = [ ] ;
var atts = [ ] ;
var iter = Changeset . opIterator ( attribs ) ;
2011-07-07 19:59:34 +02:00
while ( iter . hasNext ( ) )
{
var op = iter . next ( ) ;
lengs . push ( op . chars ) ;
atts . push ( op . attribs ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
return [ lengs , atts ] ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function attribIterator ( runs , backward )
{
2011-03-26 14:10:41 +01:00
var lengs = runs [ 0 ] ;
var atts = runs [ 1 ] ;
2011-07-07 19:59:34 +02:00
var i = ( backward ? lengs . length - 1 : 0 ) ;
2011-03-26 14:10:41 +01:00
var j = 0 ;
2011-07-07 19:59:34 +02:00
return function next ( )
{
while ( j >= lengs [ i ] )
{
if ( backward ) i -- ;
else i ++ ;
j = 0 ;
}
var a = atts [ i ] ;
j ++ ;
return a ;
2011-03-26 14:10:41 +01:00
} ;
}
var oldLen = oldText . length ;
var newLen = newText . length ;
var minLen = Math . min ( oldLen , newLen ) ;
var oldARuns = attribRuns ( Changeset . filterAttribNumbers ( oldAttribs , incorpedAttribFilter ) ) ;
var newARuns = attribRuns ( Changeset . filterAttribNumbers ( newAttribs , incorpedAttribFilter ) ) ;
var commonStart = 0 ;
var oldStartIter = attribIterator ( oldARuns , false ) ;
var newStartIter = attribIterator ( newARuns , false ) ;
2011-07-07 19:59:34 +02:00
while ( commonStart < minLen )
{
if ( oldText . charAt ( commonStart ) == newText . charAt ( commonStart ) && oldStartIter ( ) == newStartIter ( ) )
{
commonStart ++ ;
2011-03-26 14:10:41 +01:00
}
else break ;
}
var commonEnd = 0 ;
var oldEndIter = attribIterator ( oldARuns , true ) ;
var newEndIter = attribIterator ( newARuns , true ) ;
2011-07-07 19:59:34 +02:00
while ( commonEnd < minLen )
{
2012-02-19 14:52:24 +01:00
if ( commonEnd === 0 )
2011-07-07 19:59:34 +02:00
{
// assume newline in common
oldEndIter ( ) ;
newEndIter ( ) ;
commonEnd ++ ;
}
else if ( oldText . charAt ( oldLen - 1 - commonEnd ) == newText . charAt ( newLen - 1 - commonEnd ) && oldEndIter ( ) == newEndIter ( ) )
{
commonEnd ++ ;
2011-03-26 14:10:41 +01:00
}
else break ;
}
var hintedCommonEnd = - 1 ;
2011-07-07 19:59:34 +02:00
if ( ( typeof optSelEndHint ) == "number" )
{
2011-03-26 14:10:41 +01:00
hintedCommonEnd = newLen - optSelEndHint ;
}
2011-07-07 19:59:34 +02:00
if ( commonStart + commonEnd > oldLen )
{
2011-03-26 14:10:41 +01:00
// ambiguous insertion
var minCommonEnd = oldLen - commonStart ;
var maxCommonEnd = commonEnd ;
2011-07-07 19:59:34 +02:00
if ( hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd )
{
commonEnd = hintedCommonEnd ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
commonEnd = minCommonEnd ;
2011-03-26 14:10:41 +01:00
}
commonStart = oldLen - commonEnd ;
}
2011-07-07 19:59:34 +02:00
if ( commonStart + commonEnd > newLen )
{
2011-03-26 14:10:41 +01:00
// ambiguous deletion
var minCommonEnd = newLen - commonStart ;
var maxCommonEnd = commonEnd ;
2011-07-07 19:59:34 +02:00
if ( hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd )
{
commonEnd = hintedCommonEnd ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
commonEnd = minCommonEnd ;
2011-03-26 14:10:41 +01:00
}
commonStart = newLen - commonEnd ;
}
return [ commonStart , commonEnd ] ;
}
2011-07-07 19:59:34 +02:00
function equalLineAndChars ( a , b )
{
2011-03-26 14:10:41 +01:00
if ( ! a ) return ! b ;
if ( ! b ) return ! a ;
return ( a [ 0 ] == b [ 0 ] && a [ 1 ] == b [ 1 ] ) ;
}
2011-07-07 19:59:34 +02:00
function performSelectionChange ( selectStart , selectEnd , focusAtStart )
{
if ( repSelectionChange ( selectStart , selectEnd , focusAtStart ) )
{
2011-03-26 14:10:41 +01:00
currentCallStack . selectionAffected = true ;
}
}
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
function repSelectionChange ( selectStart , selectEnd , focusAtStart )
{
2011-03-26 14:10:41 +01:00
focusAtStart = ! ! focusAtStart ;
2011-07-07 19:59:34 +02:00
var newSelFocusAtStart = ( focusAtStart && ( ( ! selectStart ) || ( ! selectEnd ) || ( selectStart [ 0 ] != selectEnd [ 0 ] ) || ( selectStart [ 1 ] != selectEnd [ 1 ] ) ) ) ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02: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' , {
rep : rep ,
2015-10-30 15:13:43 +01:00
callstack : currentCallStack ,
2015-07-11 17:32:19 +02:00
documentAttributeManager : documentAttributeManager ,
} ) ;
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
var docTextChanged = currentCallStack . docTextChanged ;
if ( ! docTextChanged ) {
var isScrollableEvent = ! isPadLoading ( currentCallStack . type ) && isScrollableEditEvent ( currentCallStack . type ) ;
var innerHeight = getInnerHeight ( ) ;
scroll . scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary ( rep , isScrollableEvent , innerHeight ) ;
}
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-04-22 22:19:40 +02:00
//top.console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd,
2011-03-26 14:10:41 +01:00
//String(!!rep.selFocusAtStart));
}
return false ;
2020-05-29 13:56:03 +02:00
// Do not uncomment this in production it will break iFrames.
//top.console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart);
2011-03-26 14:10:41 +01:00
}
2018-01-03 22:57:28 +01:00
function isPadLoading ( eventType )
{
return ( eventType === 'setup' ) || ( eventType === 'setBaseText' ) || ( eventType === 'importText' ) ;
}
2018-01-04 15:28:00 +01:00
function updateStyleButtonState ( attribName , hasStyleOnRepSelection ) {
var $formattingButton = parent . parent . $ ( '[data-key="' + attribName + '"]' ) . find ( 'a' ) ;
$formattingButton . toggleClass ( SELECT _BUTTON _CLASS , hasStyleOnRepSelection ) ;
}
function attribIsFormattingStyle ( attributeName ) {
return _ . contains ( FORMATTING _STYLES , attributeName ) ;
}
function selectFormattingButtonIfLineHasStyleApplied ( rep ) {
_ . each ( FORMATTING _STYLES , function ( style ) {
var hasStyleOnRepSelection = documentAttributeManager . hasAttributeOnSelectionOrCaretPosition ( style ) ;
updateStyleButtonState ( style , hasStyleOnRepSelection ) ;
} )
}
2011-07-07 19:59:34 +02:00
function doCreateDomLine ( nonEmpty )
{
2015-01-21 16:01:39 +01:00
if ( browser . msie && ( ! nonEmpty ) )
2011-07-07 19:59:34 +02:00
{
var result = {
node : null ,
appendSpan : noop ,
prepareForAdd : noop ,
notifyAdded : noop ,
clearSpans : noop ,
finishUpdate : noop ,
lineMarker : 0
} ;
2011-03-26 14:10:41 +01:00
var lineElem = doc . createElement ( "div" ) ;
result . node = lineElem ;
2011-07-07 19:59:34 +02:00
result . notifyAdded = function ( )
{
// magic -- settng an empty div's innerHTML to the empty string
// keeps it from collapsing. Apparently innerHTML must be set *after*
// adding the node to the DOM.
// Such a div is what IE 6 creates naturally when you make a blank line
// in a document of divs. However, when copy-and-pasted the div will
// contain a space, so we note its emptiness with a property.
2012-01-29 19:41:33 +01:00
lineElem . innerHTML = " " ; // Frist we set a value that isnt blank
2011-07-07 19:59:34 +02:00
// a primitive-valued property survives copy-and-paste
setAssoc ( lineElem , "shouldBeEmpty" , true ) ;
// an object property doesn't
setAssoc ( lineElem , "unpasted" , { } ) ;
2012-01-29 19:41:33 +01:00
lineElem . innerHTML = "" ; // Then we make it blank.. New line and no space = Awesome :)
2011-03-26 14:10:41 +01:00
} ;
var lineClass = 'ace-line' ;
2011-07-07 19:59:34 +02:00
result . appendSpan = function ( txt , cls )
{
if ( ( ! txt ) && cls )
{
// gain a whole-line style (currently to show insertion point in CSS)
lineClass = domline . addToLineClass ( lineClass , cls ) ;
}
// otherwise, ignore appendSpan, this is an empty line
2011-03-26 14:10:41 +01:00
} ;
2011-07-07 19:59:34 +02:00
result . clearSpans = function ( )
{
lineClass = '' ; // non-null to cause update
2011-03-26 14:10:41 +01:00
} ;
2011-07-07 19:59:34 +02:00
2012-02-19 14:52:24 +01:00
var writeClass = function ( )
2011-07-07 19:59:34 +02:00
{
if ( lineClass !== null ) lineElem . className = lineClass ;
2012-02-19 14:52:24 +01:00
} ;
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
result . prepareForAdd = writeClass ;
result . finishUpdate = writeClass ;
2011-07-07 19:59:34 +02:00
result . getInnerHTML = function ( )
{
return "" ;
} ;
2011-03-26 14:10:41 +01:00
return result ;
}
2011-07-07 19:59:34 +02:00
else
{
2015-01-21 16:01:39 +01:00
return domline . createDomLine ( nonEmpty , doesWrap , browser , doc ) ;
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
function textify ( str )
{
2011-03-26 14:10:41 +01:00
return str . replace ( /[\n\r ]/g , ' ' ) . replace ( /\xa0/g , ' ' ) . replace ( /\t/g , ' ' ) ;
}
2011-07-07 19:59:34 +02:00
var _blockElems = {
"div" : 1 ,
"p" : 1 ,
"pre" : 1 ,
"li" : 1 ,
"ol" : 1 ,
"ul" : 1
} ;
2012-04-07 01:02:50 +02:00
_ . each ( hooks . callAll ( 'aceRegisterBlockElements' ) , function ( element ) {
_blockElems [ element ] = 1 ;
2012-04-07 02:13:26 +02:00
} ) ;
2012-04-07 01:02:50 +02:00
2011-07-07 19:59:34 +02:00
function isBlockElement ( n )
{
2011-03-26 14:10:41 +01:00
return ! ! _blockElems [ ( n . tagName || "" ) . toLowerCase ( ) ] ;
}
2011-07-07 19:59:34 +02:00
function getDirtyRanges ( )
{
2011-03-26 14:10:41 +01:00
// based on observedChanges, return a list of ranges of original lines
// that need to be removed or replaced with new user content to incorporate
// the user's changes into the line representation. ranges may be zero-length,
// indicating inserted content. for example, [0,0] means content was inserted
// at the top of the document, while [3,4] means line 3 was deleted, modified,
// or replaced with one or more new lines of content. ranges do not touch.
var p = PROFILER ( "getDirtyRanges" , false ) ;
p . forIndices = 0 ;
p . consecutives = 0 ;
p . corrections = 0 ;
var cleanNodeForIndexCache = { } ;
var N = rep . lines . length ( ) ; // old number of lines
2011-07-07 19:59:34 +02:00
function cleanNodeForIndex ( i )
{
2011-03-26 14:10:41 +01:00
// if line (i) in the un-updated line representation maps to a clean node
// in the document, return that node.
// if (i) is out of bounds, return true. else return false.
2011-07-07 19:59:34 +02:00
if ( cleanNodeForIndexCache [ i ] === undefined )
{
p . forIndices ++ ;
var result ;
if ( i < 0 || i >= N )
{
result = true ; // truthy, but no actual node
}
else
{
var key = rep . lines . atIndex ( i ) . key ;
result = ( getCleanNodeByKey ( key ) || false ) ;
}
cleanNodeForIndexCache [ i ] = result ;
2011-03-26 14:10:41 +01:00
}
return cleanNodeForIndexCache [ i ] ;
}
var isConsecutiveCache = { } ;
2011-07-07 19:59:34 +02:00
function isConsecutive ( i )
{
if ( isConsecutiveCache [ i ] === undefined )
{
p . consecutives ++ ;
isConsecutiveCache [ i ] = ( function ( )
{
// returns whether line (i) and line (i-1), assumed to be map to clean DOM nodes,
// or document boundaries, are consecutive in the changed DOM
var a = cleanNodeForIndex ( i - 1 ) ;
var b = cleanNodeForIndex ( i ) ;
if ( ( ! a ) || ( ! b ) ) return false ; // violates precondition
if ( ( a === true ) && ( b === true ) ) return ! root . firstChild ;
if ( ( a === true ) && b . previousSibling ) return false ;
if ( ( b === true ) && a . nextSibling ) return false ;
if ( ( a === true ) || ( b === true ) ) return true ;
return a . nextSibling == b ;
} ) ( ) ;
2011-03-26 14:10:41 +01:00
}
return isConsecutiveCache [ i ] ;
}
2011-07-07 19:59:34 +02:00
function isClean ( i )
{
2011-03-26 14:10:41 +01:00
// returns whether line (i) in the un-updated representation maps to a clean node,
// or is outside the bounds of the document
2011-07-07 19:59:34 +02:00
return ! ! cleanNodeForIndex ( i ) ;
2011-03-26 14:10:41 +01:00
}
// list of pairs, each representing a range of lines that is clean and consecutive
// in the changed DOM. lines (-1) and (N) are always clean, but may or may not
// be consecutive with lines in the document. pairs are in sorted order.
2011-07-07 19:59:34 +02:00
var cleanRanges = [
[ - 1 , N + 1 ]
] ;
function rangeForLine ( i )
{
2011-03-26 14:10:41 +01:00
// returns index of cleanRange containing i, or -1 if none
var answer = - 1 ;
2012-03-17 13:36:42 +01:00
_ . each ( cleanRanges , function ( r , idx )
2011-07-07 19:59:34 +02:00
{
if ( i >= r [ 1 ] ) return false ; // keep looking
if ( i < r [ 0 ] ) return true ; // not found, stop looking
answer = idx ;
return true ; // found, stop looking
2011-03-26 14:10:41 +01:00
} ) ;
return answer ;
}
2011-07-07 19:59:34 +02:00
function removeLineFromRange ( rng , line )
{
2011-03-26 14:10:41 +01:00
// rng is index into cleanRanges, line is line number
// precond: line is in rng
var a = cleanRanges [ rng ] [ 0 ] ;
var b = cleanRanges [ rng ] [ 1 ] ;
2011-07-07 19:59:34 +02:00
if ( ( a + 1 ) == b ) cleanRanges . splice ( rng , 1 ) ;
2011-03-26 14:10:41 +01:00
else if ( line == a ) cleanRanges [ rng ] [ 0 ] ++ ;
2011-07-07 19:59:34 +02:00
else if ( line == ( b - 1 ) ) cleanRanges [ rng ] [ 1 ] -- ;
else cleanRanges . splice ( rng , 1 , [ a , line ] , [ line + 1 , b ] ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function splitRange ( rng , pt )
{
2011-03-26 14:10:41 +01:00
// precond: pt splits cleanRanges[rng] into two non-empty ranges
var a = cleanRanges [ rng ] [ 0 ] ;
var b = cleanRanges [ rng ] [ 1 ] ;
2011-07-07 19:59:34 +02:00
cleanRanges . splice ( rng , 1 , [ a , pt ] , [ pt , b ] ) ;
2011-03-26 14:10:41 +01:00
}
var correctedLines = { } ;
2011-07-07 19:59:34 +02:00
function correctlyAssignLine ( line )
{
2011-03-26 14:10:41 +01:00
if ( correctedLines [ line ] ) return true ;
p . corrections ++ ;
correctedLines [ line ] = true ;
// "line" is an index of a line in the un-updated rep.
// returns whether line was already correctly assigned (i.e. correctly
// clean or dirty, according to cleanRanges, and if clean, correctly
// attached or not attached (i.e. in the same range as) the prev and next lines).
var rng = rangeForLine ( line ) ;
var lineClean = isClean ( line ) ;
2011-07-07 19:59:34 +02:00
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 ;
}
if ( ! lineClean )
{
// a clean-range includes this dirty line, fix it
removeLineFromRange ( rng , line ) ;
return false ;
}
else
{
// line is clean, but could be wrongly connected to a clean line
// above or below
var a = cleanRanges [ rng ] [ 0 ] ;
var b = cleanRanges [ rng ] [ 1 ] ;
var didSomething = false ;
// we'll leave non-clean adjacent nodes in the clean range for the caller to
// detect and deal with. we deal with whether the range should be split
// just above or just below this line.
if ( a < line && isClean ( line - 1 ) && ! isConsecutive ( line ) )
{
splitRange ( rng , line ) ;
didSomething = true ;
}
if ( b > ( line + 1 ) && isClean ( line + 1 ) && ! isConsecutive ( line + 1 ) )
{
splitRange ( rng , line + 1 ) ;
didSomething = true ;
}
return ! didSomething ;
}
}
function detectChangesAroundLine ( line , reqInARow )
{
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.
var correctInARow = 0 ;
var currentIndex = line ;
2011-07-07 19:59:34 +02:00
while ( correctInARow < reqInARow && currentIndex >= 0 )
{
if ( correctlyAssignLine ( currentIndex ) )
{
correctInARow ++ ;
}
else correctInARow = 0 ;
currentIndex -- ;
2011-03-26 14:10:41 +01:00
}
correctInARow = 0 ;
currentIndex = line ;
2011-07-07 19:59:34 +02:00
while ( correctInARow < reqInARow && currentIndex < N )
{
if ( correctlyAssignLine ( currentIndex ) )
{
correctInARow ++ ;
}
else correctInARow = 0 ;
currentIndex ++ ;
2011-03-26 14:10:41 +01:00
}
}
2012-02-19 14:52:24 +01:00
if ( N === 0 )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
p . cancel ( ) ;
2011-07-07 19:59:34 +02:00
if ( ! isConsecutive ( 0 ) )
{
splitRange ( 0 , 0 ) ;
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
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
p . mark ( "obs" ) ;
2011-07-07 19:59:34 +02:00
for ( var k in observedChanges . cleanNodesNearChanges )
{
var key = k . substring ( 1 ) ;
if ( rep . lines . containsKey ( key ) )
{
var line = rep . lines . indexOfKey ( key ) ;
detectChangesAroundLine ( line , 2 ) ;
}
2011-03-26 14:10:41 +01:00
}
p . mark ( "stats&calc" ) ;
p . literal ( p . forIndices , "byidx" ) ;
p . literal ( p . consecutives , "cons" ) ;
p . literal ( p . corrections , "corr" ) ;
}
var dirtyRanges = [ ] ;
2011-07-07 19:59:34 +02:00
for ( var r = 0 ; r < cleanRanges . length - 1 ; r ++ )
{
dirtyRanges . push ( [ cleanRanges [ r ] [ 1 ] , cleanRanges [ r + 1 ] [ 0 ] ] ) ;
2011-03-26 14:10:41 +01:00
}
p . end ( ) ;
return dirtyRanges ;
}
2011-07-07 19:59:34 +02:00
function markNodeClean ( n )
{
2011-03-26 14:10:41 +01:00
// clean nodes have knownHTML that matches their innerHTML
var dirtiness = { } ;
dirtiness . nodeId = uniqueId ( n ) ;
dirtiness . knownHTML = n . innerHTML ;
2015-01-21 16:01:39 +01:00
if ( browser . msie )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
// adding a space to an "empty" div in IE designMode doesn't
// change the innerHTML of the div's parent; also, other
// browsers don't support innerText
dirtiness . knownText = n . innerText ;
}
setAssoc ( n , "dirtiness" , dirtiness ) ;
}
2011-07-07 19:59:34 +02:00
function isNodeDirty ( n )
{
2011-03-26 14:10:41 +01:00
var p = PROFILER ( "cleanCheck" , false ) ;
if ( n . parentNode != root ) return true ;
var data = getAssoc ( n , "dirtiness" ) ;
if ( ! data ) return true ;
if ( n . id !== data . nodeId ) return true ;
2015-01-21 16:01:39 +01:00
if ( browser . msie )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
if ( n . innerText !== data . knownText ) return true ;
}
if ( n . innerHTML !== data . knownHTML ) return true ;
p . end ( ) ;
return false ;
}
2011-07-07 19:59:34 +02:00
function getViewPortTopBottom ( )
{
2018-01-03 22:57:28 +01:00
var theTop = scroll . getScrollY ( ) ;
2011-03-26 14:10:41 +01:00
var doc = outerWin . document ;
2018-01-03 22:57:28 +01:00
var height = doc . documentElement . clientHeight ; // includes padding
// we have to get the exactly height of the viewport. So it has to subtract all the values which changes
// the viewport height (E.g. padding, position top)
var viewportExtraSpacesAndPosition = getEditorPositionTop ( ) + getPaddingTopAddedWhenPageViewIsEnable ( ) ;
2011-07-07 19:59:34 +02:00
return {
top : theTop ,
2018-01-03 22:57:28 +01:00
bottom : ( theTop + height - viewportExtraSpacesAndPosition )
2011-07-07 19:59:34 +02:00
} ;
2011-03-26 14:10:41 +01:00
}
2018-01-03 22:57:28 +01:00
function getEditorPositionTop ( )
2011-07-07 19:59:34 +02:00
{
2018-01-03 22:57:28 +01:00
var editor = parent . document . getElementsByTagName ( 'iframe' ) ;
var editorPositionTop = editor [ 0 ] . offsetTop ;
return editorPositionTop ;
2011-03-26 14:10:41 +01:00
}
2018-01-03 22:57:28 +01:00
// ep_page_view adds padding-top, which makes the viewport smaller
function getPaddingTopAddedWhenPageViewIsEnable ( )
2011-07-07 19:59:34 +02:00
{
2018-01-03 22:57:28 +01:00
var rootDocument = parent . parent . document ;
var aceOuter = rootDocument . getElementsByName ( "ace_outer" ) ;
var aceOuterPaddingTop = parseInt ( $ ( aceOuter ) . css ( "padding-top" ) ) ;
return aceOuterPaddingTop ;
2011-03-26 14:10:41 +01:00
}
2015-02-27 18:54:29 +01:00
function handleCut ( evt )
{
inCallStackIfNecessary ( "handleCut" , function ( )
{
doDeleteKey ( evt ) ;
} ) ;
2015-03-13 06:01:18 +01:00
return true ;
2015-02-27 18:54:29 +01:00
}
2011-07-07 19:59:34 +02:00
function handleClick ( evt )
{
2012-03-22 18:34:08 +01:00
inCallStackIfNecessary ( "handleClick" , function ( )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
idleWorkTimer . atMost ( 200 ) ;
} ) ;
2012-02-19 14:52:24 +01:00
function isLink ( n )
{
return ( 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
2011-07-07 19:59:34 +02:00
if ( ( ! evt . ctrlKey ) && ( evt . button != 2 ) && ( evt . button != 3 ) )
{
2011-03-26 14:10:41 +01:00
// find A tag with HREF
var n = evt . target ;
2011-07-07 19:59:34 +02:00
while ( n && n . parentNode && ! isLink ( n ) )
{
n = n . parentNode ;
}
if ( n && isLink ( n ) )
{
try
{
2020-03-31 10:02:46 +02:00
window . open ( n . href , '_blank' , 'noopener,noreferrer' ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02: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 ( ) ;
}
function hideEditBarDropdowns ( )
{
2012-11-22 01:12:30 +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
2012-07-13 08:24:02 +02:00
window . parent . parent . padeditbar . toggleDropDown ( "none" ) ;
2012-02-26 13:38:52 +01:00
}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function doReturnKey ( )
{
if ( ! ( rep . selStart && rep . selEnd ) )
{
2011-03-26 14:10:41 +01:00
return ;
}
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
var lineNum = rep . selStart [ 0 ] ;
var listType = getLineListType ( lineNum ) ;
2011-07-07 19:59:34 +02:00
if ( listType )
{
2012-01-15 18:20:20 +01:00
var text = rep . lines . atIndex ( lineNum ) . text ;
2014-12-27 13:18:58 +01:00
listType = /([a-z]+)([0-9]+)/ . exec ( listType ) ;
2012-01-15 18:20:20 +01:00
var type = listType [ 1 ] ;
var level = Number ( listType [ 2 ] ) ;
2013-02-18 02:40:34 +01:00
2012-01-15 18:20:20 +01:00
//detect empty list item; exclude indentation
if ( text === '*' && type !== "indent" )
2011-07-07 19:59:34 +02:00
{
2012-01-15 18:20:20 +01:00
//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
}
}
2015-06-19 20:03:22 +02:00
else if ( lineNum + 1 <= rep . lines . length ( ) )
2012-01-15 18:20:20 +01:00
{
performDocumentReplaceSelection ( '\n' ) ;
setLineListType ( lineNum + 1 , type + level ) ;
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
else
{
2012-01-15 18:20:20 +01:00
performDocumentReplaceSelection ( '\n' ) ;
2011-03-26 14:10:41 +01:00
handleReturnIndentation ( ) ;
}
}
2011-07-07 19:59:34 +02:00
function doIndentOutdent ( isOut )
{
2013-02-18 02:40:34 +01:00
if ( ! ( ( rep . selStart && rep . selEnd ) ||
( ( rep . selStart [ 0 ] == rep . selEnd [ 0 ] ) && ( rep . selStart [ 1 ] == rep . selEnd [ 1 ] ) && rep . selEnd [ 1 ] > 1 ) ) &&
( isOut != true )
)
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
return false ;
}
var firstLine , lastLine ;
firstLine = rep . selStart [ 0 ] ;
2012-02-19 14:52:24 +01:00
lastLine = Math . max ( firstLine , rep . selEnd [ 0 ] - ( ( rep . selEnd [ 1 ] === 0 ) ? 1 : 0 ) ) ;
2011-03-26 14:10:41 +01:00
var mods = [ ] ;
2011-07-07 19:59:34 +02:00
for ( var n = firstLine ; n <= lastLine ; n ++ )
{
2011-03-26 14:10:41 +01:00
var listType = getLineListType ( n ) ;
2011-11-25 10:29:31 +01:00
var t = 'indent' ;
var level = 0 ;
2011-07-07 19:59:34 +02:00
if ( listType )
{
2014-12-27 13:18:58 +01:00
listType = /([a-z]+)([0-9]+)/ . exec ( listType ) ;
2011-07-07 19:59:34 +02: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
}
}
2011-11-25 10:29:31 +01:00
var newLevel = Math . max ( 0 , Math . min ( MAX _LIST _LEVEL , level + ( isOut ? - 1 : 1 ) ) ) ;
if ( level != newLevel )
{
mods . push ( [ n , ( newLevel > 0 ) ? t + newLevel : '' ] ) ;
}
2011-03-26 14:10:41 +01:00
}
2012-04-05 00:50:04 +02:00
_ . each ( mods , function ( 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 ;
2011-03-26 14:10:41 +01:00
}
editorInfo . ace _doIndentOutdent = doIndentOutdent ;
2011-07-07 19:59:34 +02:00
function doTabKey ( shiftDown )
{
if ( ! doIndentOutdent ( shiftDown ) )
{
2011-03-26 14:10:41 +01:00
performDocumentReplaceSelection ( THE _TAB ) ;
}
}
2011-07-07 19:59:34 +02:00
function doDeleteKey ( optEvt )
{
2011-03-26 14:10:41 +01:00
var evt = optEvt || { } ;
var handled = false ;
2011-07-07 19:59:34 +02:00
if ( rep . selStart )
{
if ( isCaret ( ) )
{
var lineNum = caretLine ( ) ;
var col = caretColumn ( ) ;
2011-03-26 14:10:41 +01:00
var lineEntry = rep . lines . atIndex ( lineNum ) ;
2011-07-07 19:59:34 +02:00
var lineText = lineEntry . text ;
2011-03-26 14:10:41 +01:00
var lineMarker = lineEntry . lineMarker ;
2011-07-07 19:59:34 +02:00
if ( /^ +$/ . exec ( lineText . substring ( lineMarker , col ) ) )
{
2011-03-26 14:10:41 +01:00
var col2 = col - lineMarker ;
2011-07-07 19:59:34 +02:00
var tabSize = THE _TAB . length ;
var toDelete = ( ( col2 - 1 ) % tabSize ) + 1 ;
performDocumentReplaceRange ( [ lineNum , col - toDelete ] , [ lineNum , col ] , '' ) ;
//scrollSelectionIntoView();
handled = true ;
}
}
if ( ! handled )
{
if ( isCaret ( ) )
{
2011-03-26 14:10:41 +01:00
var theLine = caretLine ( ) ;
var lineEntry = rep . lines . atIndex ( theLine ) ;
2011-07-07 19:59:34 +02:00
if ( caretColumn ( ) <= lineEntry . lineMarker )
{
2011-03-26 14:10:41 +01:00
// delete at beginning of line
var action = 'delete_newline' ;
2011-07-07 19:59:34 +02:00
var prevLineListType = ( theLine > 0 ? getLineListType ( theLine - 1 ) : '' ) ;
2011-03-26 14:10:41 +01:00
var thisLineListType = getLineListType ( theLine ) ;
2011-07-07 19:59:34 +02:00
var prevLineEntry = ( theLine > 0 && rep . lines . atIndex ( theLine - 1 ) ) ;
var prevLineBlank = ( prevLineEntry && prevLineEntry . text . length == prevLineEntry . lineMarker ) ;
2013-06-14 19:37:41 +02:00
2012-04-07 01:07:25 +02:00
var thisLineHasMarker = documentAttributeManager . lineHasMarker ( theLine ) ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
if ( thisLineListType )
{
2011-03-26 14:10:41 +01:00
// this line is a list
2011-07-07 19:59:34 +02:00
if ( prevLineBlank && ! prevLineListType )
{
// previous line is blank, remove it
performDocumentReplaceRange ( [ theLine - 1 , prevLineEntry . text . length ] , [ theLine , 0 ] , '' ) ;
}
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
}
2012-04-07 01:07:25 +02:00
} else if ( thisLineHasMarker && prevLineEntry ) {
// If the line has any attributes assigned, remove them by removing the marker '*'
performDocumentReplaceRange ( [ theLine - 1 , prevLineEntry . text . length ] , [ theLine , lineEntry . lineMarker ] , '' ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else if ( theLine > 0 )
{
2011-03-26 14:10:41 +01:00
// remove newline
2011-07-07 19:59:34 +02:00
performDocumentReplaceRange ( [ theLine - 1 , prevLineEntry . text . length ] , [ theLine , 0 ] , '' ) ;
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
else
{
var docChar = caretDocChar ( ) ;
if ( docChar > 0 )
{
if ( evt . metaKey || evt . ctrlKey || evt . altKey )
{
// delete as many unicode "letters or digits" in a row as possible;
// always delete one char, delete further even if that first char
// isn't actually a word char.
var deleteBackTo = docChar - 1 ;
while ( deleteBackTo > lineEntry . lineMarker && isWordChar ( rep . alltext . charAt ( deleteBackTo - 1 ) ) )
{
deleteBackTo -- ;
}
performDocumentReplaceCharRange ( deleteBackTo , docChar , '' ) ;
}
else
{
// normal delete
performDocumentReplaceCharRange ( docChar - 1 , docChar , '' ) ;
}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
}
}
else
{
performDocumentReplaceSelection ( '' ) ;
}
2011-03-26 14:10:41 +01:00
}
}
2012-01-15 18:20:20 +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.
var line = caretLine ( ) ;
2012-02-19 14:52:24 +01:00
if ( line != - 1 && renumberList ( line + 1 ) === null )
2012-01-15 18:20:20 +01:00
{
renumberList ( line ) ;
}
2011-03-26 14:10:41 +01:00
}
// set of "letter or digit" chars is based on section 20.5.16 of the original Java Language Spec
var REGEX _WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/ ;
var REGEX _SPACE = /\s/ ;
2011-07-07 19:59:34 +02:00
function isWordChar ( c )
{
return ! ! REGEX _WORDCHAR . exec ( c ) ;
2011-03-26 14:10:41 +01:00
}
2012-09-08 20:45:33 +02:00
editorInfo . ace _isWordChar = isWordChar ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
function isSpaceChar ( c )
{
return ! ! REGEX _SPACE . exec ( c ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function moveByWordInLine ( lineText , initialIndex , forwardNotBack )
{
2011-03-26 14:10:41 +01:00
var i = initialIndex ;
2011-07-07 19:59:34 +02:00
function nextChar ( )
{
2011-03-26 14:10:41 +01:00
if ( forwardNotBack ) return lineText . charAt ( i ) ;
2011-07-07 19:59:34 +02:00
else return lineText . charAt ( i - 1 ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function advance ( )
{
if ( forwardNotBack ) i ++ ;
else i -- ;
}
function isDone ( )
{
2011-03-26 14:10:41 +01:00
if ( forwardNotBack ) return i >= lineText . length ;
else return i <= 0 ;
}
// On Mac and Linux, move right moves to end of word and move left moves to start;
// on Windows, always move to start of word.
// On Windows, Firefox and IE disagree on whether to stop for punctuation (FF says no).
2015-01-21 16:01:39 +01:00
if ( browser . msie && forwardNotBack )
2011-07-07 19:59:34 +02:00
{
while ( ( ! isDone ( ) ) && isWordChar ( nextChar ( ) ) )
{
advance ( ) ;
}
while ( ( ! isDone ( ) ) && ! isWordChar ( nextChar ( ) ) )
{
advance ( ) ;
}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
while ( ( ! isDone ( ) ) && ! isWordChar ( nextChar ( ) ) )
{
advance ( ) ;
}
while ( ( ! isDone ( ) ) && isWordChar ( nextChar ( ) ) )
{
advance ( ) ;
}
2011-03-26 14:10:41 +01:00
}
return i ;
}
2011-07-07 19:59:34 +02:00
function handleKeyEvent ( evt )
{
2011-03-27 12:46:45 +02:00
// if (DEBUG && window.DONT_INCORP) return;
2011-07-07 19:59:34 +02:00
if ( ! isEditable ) return ;
2011-03-26 14:10:41 +01:00
var type = evt . type ;
var charCode = evt . charCode ;
var keyCode = evt . keyCode ;
var which = evt . which ;
2015-03-26 17:58:13 +01:00
var altKey = evt . altKey ;
2015-04-02 16:13:16 +02:00
var shiftKey = evt . shiftKey ;
2011-03-26 14:10:41 +01:00
2013-09-29 09:19:57 +02:00
// Is caret potentially hidden by the chat button?
var myselection = document . getSelection ( ) ; // get the current caret selection
var caretOffsetTop = myselection . focusNode . parentNode . offsetTop | myselection . focusNode . offsetTop ; // get the carets selection offset in px IE 214
2015-10-13 23:39:23 +02:00
2013-09-29 09:19:57 +02:00
if ( myselection . focusNode . wholeText ) { // Is there any content? If not lineHeight will report wrong..
var lineHeight = myselection . focusNode . parentNode . offsetHeight ; // line height of populated links
} else {
var lineHeight = myselection . focusNode . offsetHeight ; // line height of blank lines
}
2015-05-06 01:32:36 +02:00
2011-03-26 14:10:41 +01:00
//dmesg("keyevent type: "+type+", which: "+which);
// Don't take action based on modifier keys going up and down.
// Modifier keys do not generate "keypress" events.
// 224 is the command-key under Mac Firefox.
// 91 is the Windows key in IE; it is ASCII for open-bracket but isn't the keycode for that key
// 20 is capslock in IE.
2011-07-07 19:59:34 +02:00
var 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 ;
2013-02-12 20:04:43 +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.
2015-01-21 16:01:39 +01:00
if ( keyCode == 13 && browser . opera && ( type == "keypress" ) ) {
2013-02-12 20:04:43 +01:00
return ; // This stops double enters in Opera but double Tabs still show on single tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice
}
2011-03-26 14:10:41 +01:00
var specialHandled = false ;
2018-06-28 09:41:06 +02:00
var isTypeForSpecialKey = ( ( browser . msie || browser . safari || browser . chrome || browser . firefox ) ? ( type == "keydown" ) : ( type == "keypress" ) ) ;
var isTypeForCmdKey = ( ( browser . msie || browser . safari || browser . chrome || browser . firefox ) ? ( type == "keydown" ) : ( type == "keypress" ) ) ;
2011-03-26 14:10:41 +01:00
var stopped = false ;
2012-03-22 18:34:08 +01:00
inCallStackIfNecessary ( "handleKeyEvent" , function ( )
2011-07-07 19:59:34 +02:00
{
2012-12-01 00:26:51 +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
if ( ! outsideKeyPress ( evt ) )
{
evt . preventDefault ( ) ;
stopped = true ;
}
}
2015-04-17 18:01:15 +02:00
else if ( evt . key === "Dead" ) {
// If it's a dead key we don't want to do any Etherpad behavior.
stopped = true ;
return true ;
}
2011-07-07 19:59:34 +02:00
else if ( type == "keydown" )
{
outsideKeyDown ( evt ) ;
}
if ( ! stopped )
{
2012-09-11 23:21:14 +02:00
var specialHandledInHook = hooks . callAll ( 'aceKeyEvent' , {
2012-09-08 20:45:33 +02:00
callstack : currentCallStack ,
editorInfo : editorInfo ,
rep : rep ,
documentAttributeManager : documentAttributeManager ,
evt : evt
} ) ;
2015-10-27 10:44:51 +01:00
// if any hook returned true, set specialHandled with true
if ( specialHandledInHook ) {
specialHandled = _ . contains ( specialHandledInHook , true ) ;
}
2017-05-11 17:26:14 +02:00
var padShortcutEnabled = parent . parent . clientVars . padShortcutEnabled ;
2016-01-21 13:38:41 +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....
2015-03-25 16:38:19 +01:00
// Focus on the editbar. -- TODO: Move Focus back to previous state (we know it so we can use it)
2015-03-25 12:03:45 +01:00
var 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
}
2016-01-21 13:38:41 +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 ( ) ;
2015-05-06 01:32:36 +02:00
parent . parent . $ ( "#chatinput" ) . focus ( ) ;
2015-03-26 17:44:22 +01:00
evt . preventDefault ( ) ;
}
2016-01-21 13:38:41 +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
2015-03-31 17:12:05 +02:00
var lineNumber = rep . selEnd [ 0 ] ;
var alineAttrs = rep . alines [ lineNumber ] ;
var apool = rep . apool ;
// TODO: support selection ranges
// TODO: Still work when authorship colors have been cleared
// TODO: i18n
// TODO: There appears to be a race condition or so.
var author = null ;
if ( alineAttrs ) {
2015-03-31 21:26:55 +02:00
var authors = [ ] ;
var authorNames = [ ] ;
2015-03-31 17:12:05 +02:00
var opIter = Changeset . opIterator ( alineAttrs ) ;
2015-03-31 21:26:55 +02:00
while ( opIter . hasNext ( ) ) {
2015-03-31 17:12:05 +02:00
var op = opIter . next ( ) ;
authorId = Changeset . opAttributeValue ( op , 'author' , apool ) ;
2015-03-31 21:26:55 +02:00
// Only push unique authors and ones with values
if ( authors . indexOf ( authorId ) === - 1 && authorId !== "" ) {
authors . push ( authorId ) ;
}
2015-03-31 17:12:05 +02:00
}
2015-03-31 21:26:55 +02:00
}
// No author information is available IE on a new pad.
if ( authors . length === 0 ) {
var authorString = "No author information is available" ;
}
else {
// Known authors info, both current and historical
var padAuthors = parent . parent . pad . userList ( ) ;
var authorObj = { } ;
authors . forEach ( function ( authorId ) {
padAuthors . forEach ( function ( padAuthor ) {
// If the person doing the lookup is the author..
if ( padAuthor . userId === authorId ) {
if ( parent . parent . clientVars . userId === authorId ) {
authorObj = {
name : "Me"
}
} else {
authorObj = padAuthor ;
}
}
} ) ;
if ( ! authorObj ) {
author = "Unknown" ;
return ;
}
author = authorObj . name ;
if ( ! author ) author = "Unknown" ;
authorNames . push ( author ) ;
} )
2015-03-31 17:12:05 +02:00
}
2015-03-31 21:26:55 +02:00
if ( authors . length === 1 ) {
var authorString = "The author of this line is " + authorNames ;
2015-03-31 17:12:05 +02:00
}
2015-03-31 21:26:55 +02:00
if ( authors . length > 1 ) {
var 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
time : '4000'
} ) ;
}
2016-01-21 13:38:41 +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
}
2016-01-21 13:38:41 +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 ( ) ;
//scrollSelectionIntoView();
2013-02-09 17:42:47 +01:00
scheduler . setTimeout ( function ( )
2011-07-07 19:59:34 +02:00
{
outerWin . scrollBy ( - 100 , 0 ) ;
} , 0 ) ;
specialHandled = true ;
}
2017-05-11 17:35:25 +02: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
}
2016-01-21 13:38:41 +01:00
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "s" && ( evt . metaKey || evt . ctrlKey ) && ! evt . altKey && padShortcutEnabled . cmdS ) /* Do a saved revision on ctrl S */
2012-11-28 18:17:35 +01:00
{
evt . preventDefault ( ) ;
2013-03-28 03:24:59 +01:00
var originalBackground = parent . parent . $ ( '#revisionlink' ) . css ( "background" )
2013-03-29 04:09:10 +01:00
parent . parent . $ ( '#revisionlink' ) . css ( { "background" : "lightyellow" } ) ;
2013-04-04 03:25:19 +02:00
scheduler . setTimeout ( function ( ) {
2013-03-29 04:09:10 +01:00
parent . parent . $ ( '#revisionlink' ) . css ( { "background" : originalBackground } ) ;
2013-03-28 16:39:33 +01:00
} , 1000 ) ;
2012-11-28 18:17:35 +01:00
parent . parent . pad . collabClient . sendMessage ( { "type" : "SAVE_REVISION" } ) ; /* The parent.parent part of this is BAD and I feel bad.. It may break something */
specialHandled = true ;
}
2016-01-21 13:38:41 +01:00
if ( ( ! specialHandled ) && isTypeForSpecialKey && keyCode == 9 && ! ( evt . metaKey || evt . ctrlKey ) && padShortcutEnabled . tab )
2011-07-07 19:59:34 +02:00
{
// tab
fastIncorp ( 5 ) ;
evt . preventDefault ( ) ;
2011-03-26 14:10:41 +01:00
doTabKey ( evt . shiftKey ) ;
2011-07-07 19:59:34 +02:00
//scrollSelectionIntoView();
specialHandled = true ;
}
2016-01-21 13:38:41 +01:00
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "z" && ( evt . metaKey || evt . ctrlKey ) && ! evt . altKey && padShortcutEnabled . cmdZ )
2011-07-07 19:59:34 +02:00
{
2014-11-22 20:13:23 +01:00
// cmd-Z (undo)
2011-07-07 19:59:34 +02:00
fastIncorp ( 6 ) ;
evt . preventDefault ( ) ;
if ( evt . shiftKey )
{
doUndoRedo ( "redo" ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else
{
doUndoRedo ( "undo" ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
specialHandled = true ;
}
2016-01-21 13:38:41 +01:00
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "y" && ( evt . metaKey || evt . ctrlKey ) && padShortcutEnabled . cmdY )
2011-07-07 19:59:34 +02:00
{
// cmd-Y (redo)
fastIncorp ( 10 ) ;
evt . preventDefault ( ) ;
doUndoRedo ( "redo" ) ;
specialHandled = true ;
}
2016-01-21 13:38:41 +01:00
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "b" && ( evt . metaKey || evt . ctrlKey ) && padShortcutEnabled . cmdB )
2011-07-07 19:59:34 +02:00
{
// cmd-B (bold)
fastIncorp ( 13 ) ;
evt . preventDefault ( ) ;
2011-03-26 14:10:41 +01:00
toggleAttributeOnSelection ( 'bold' ) ;
2011-07-07 19:59:34 +02:00
specialHandled = true ;
}
2016-01-21 13:38:41 +01:00
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "i" && ( evt . metaKey || evt . ctrlKey ) && padShortcutEnabled . cmdI )
2011-07-07 19:59:34 +02:00
{
// cmd-I (italic)
fastIncorp ( 14 ) ;
evt . preventDefault ( ) ;
2011-03-26 14:10:41 +01:00
toggleAttributeOnSelection ( 'italic' ) ;
2011-07-07 19:59:34 +02:00
specialHandled = true ;
}
2016-01-21 13:38:41 +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 ;
}
2016-01-21 13:38:41 +01:00
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "5" && ( evt . metaKey || evt . ctrlKey ) && evt . altKey !== true && padShortcutEnabled . cmd5 )
2014-10-11 19:16:01 +02:00
{
// cmd-5 (strikethrough)
fastIncorp ( 13 ) ;
evt . preventDefault ( ) ;
toggleAttributeOnSelection ( 'strikethrough' ) ;
specialHandled = true ;
}
2016-01-21 13:38:41 +01:00
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "l" && ( evt . metaKey || evt . ctrlKey ) && evt . shiftKey && padShortcutEnabled . cmdShiftL )
2014-10-11 19:47:03 +02:00
{
// cmd-shift-L (unorderedlist)
fastIncorp ( 9 ) ;
evt . preventDefault ( ) ;
doInsertUnorderedList ( )
specialHandled = true ;
}
2017-05-11 19:56:09 +02:00
if ( ( ! specialHandled ) && 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
{
2017-05-11 19:56:09 +02:00
// cmd-shift-N and cmd-shift-1 (orderedlist)
2014-10-11 19:47:03 +02:00
fastIncorp ( 9 ) ;
evt . preventDefault ( ) ;
doInsertOrderedList ( )
specialHandled = true ;
}
2016-01-21 13:38:41 +01:00
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "c" && ( evt . metaKey || evt . ctrlKey ) && evt . shiftKey && padShortcutEnabled . cmdShiftC ) {
2014-11-07 09:31:32 +01:00
// cmd-shift-C (clearauthorship)
fastIncorp ( 9 ) ;
evt . preventDefault ( ) ;
CMDS . clearauthorship ( ) ;
}
2016-01-21 13:38:41 +01:00
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "h" && ( evt . ctrlKey ) && padShortcutEnabled . cmdH )
2011-07-07 19:59:34 +02:00
{
// cmd-H (backspace)
fastIncorp ( 20 ) ;
evt . preventDefault ( ) ;
2011-03-26 14:10:41 +01:00
doDeleteKey ( ) ;
2011-07-07 19:59:34 +02:00
specialHandled = true ;
}
2018-01-03 22:57:28 +01:00
if ( ( evt . which == 36 && evt . ctrlKey == true ) && padShortcutEnabled . ctrlHome ) { scroll . setScrollY ( 0 ) ; } // Control Home send to Y = 0
2014-02-01 08:05:25 +01:00
if ( ( evt . which == 33 || evt . which == 34 ) && type == 'keydown' && ! evt . ctrlKey ) {
2013-03-18 19:03:37 +01:00
evt . preventDefault ( ) ; // This is required, browsers will try to do normal default behavior on page up / down and the default behavior SUCKS
2018-01-03 22:57:28 +01:00
var oldVisibleLineRange = scroll . getVisibleLineRange ( rep ) ;
2013-02-03 18:39:49 +01:00
var topOffset = rep . selStart [ 0 ] - oldVisibleLineRange [ 0 ] ;
if ( topOffset < 0 ) {
topOffset = 0 ;
}
var isPageDown = evt . which === 34 ;
var isPageUp = evt . which === 33 ;
2013-02-09 17:42:47 +01:00
scheduler . setTimeout ( function ( ) {
2018-01-03 22:57:28 +01:00
var newVisibleLineRange = scroll . getVisibleLineRange ( rep ) ; // the visible lines IE 1,10
2013-03-18 18:40:18 +01:00
var linesCount = rep . lines . length ( ) ; // total count of lines in pad IE 10
var numberOfLinesInViewport = newVisibleLineRange [ 1 ] - newVisibleLineRange [ 0 ] ; // How many lines are in the viewport right now?
2013-02-03 18:39:49 +01:00
2016-01-21 13:38:41 +01:00
if ( isPageUp && padShortcutEnabled . pageUp ) {
2013-03-18 19:03:37 +01:00
rep . selEnd [ 0 ] = rep . selEnd [ 0 ] - numberOfLinesInViewport ; // move to the bottom line +1 in the viewport (essentially skipping over a page)
rep . selStart [ 0 ] = rep . selStart [ 0 ] - numberOfLinesInViewport ; // move to the bottom line +1 in the viewport (essentially skipping over a page)
2013-02-03 18:39:49 +01:00
}
2016-01-21 13:38:41 +01:00
if ( isPageDown && padShortcutEnabled . pageDown ) { // if we hit page down
2013-03-18 19:03:37 +01:00
if ( rep . selEnd [ 0 ] >= oldVisibleLineRange [ 0 ] ) { // If the new viewpoint position is actually further than where we are right now
2013-03-18 18:40:18 +01:00
rep . selStart [ 0 ] = oldVisibleLineRange [ 1 ] - 1 ; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content
rep . selEnd [ 0 ] = oldVisibleLineRange [ 1 ] - 1 ; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content
}
2013-02-03 18:39:49 +01:00
}
//ensure min and max
2013-03-18 18:40:18 +01:00
if ( rep . selEnd [ 0 ] < 0 ) {
rep . selEnd [ 0 ] = 0 ;
2013-02-03 18:39:49 +01:00
}
2013-03-18 19:44:01 +01:00
if ( rep . selStart [ 0 ] < 0 ) {
rep . selStart [ 0 ] = 0 ;
}
2013-03-18 18:40:18 +01:00
if ( rep . selEnd [ 0 ] >= linesCount ) {
rep . selEnd [ 0 ] = linesCount - 1 ;
2013-02-03 18:39:49 +01:00
}
updateBrowserSelectionFromRep ( ) ;
2013-03-18 18:40:18 +01:00
var myselection = document . getSelection ( ) ; // get the current caret selection, can't use rep. here because that only gives us the start position not the current
2014-11-04 16:50:53 +01:00
var caretOffsetTop = myselection . focusNode . parentNode . offsetTop || myselection . focusNode . offsetTop ; // get the carets selection offset in px IE 214
2015-01-18 20:58:38 +01:00
// sometimes the first selection is -1 which causes problems (Especially with ep_page_view)
// so use focusNode.offsetTop value.
if ( caretOffsetTop === - 1 ) caretOffsetTop = myselection . focusNode . offsetTop ;
2018-01-03 22:57:28 +01:00
scroll . setScrollY ( caretOffsetTop ) ; // set the scrollY offset of the viewport on the document
2013-03-18 18:40:18 +01:00
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
if ( ( evt . which == 37 || evt . which == 38 || evt . which == 39 || evt . which == 40 ) ) {
// we use arrowKeyWasReleased to avoid triggering the animation when a key is continuously pressed
// this makes the scroll smooth
if ( ! continuouslyPressingArrowKey ( type ) ) {
// We use getSelection() instead of rep to get the caret position. This avoids errors like when
// the caret position is not synchronized with the rep. For example, when an user presses arrow
// down to scroll the pad without releasing the key. When the key is released the rep is not
// synchronized, so we don't get the right node where caret is.
var selection = getSelection ( ) ;
if ( selection ) {
var arrowUp = evt . which === 38 ;
var innerHeight = getInnerHeight ( ) ;
scroll . scrollWhenPressArrowKeys ( arrowUp , rep , innerHeight ) ;
2013-02-15 13:24:16 +01:00
}
}
}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
if ( type == "keydown" )
{
idleWorkTimer . atLeast ( 500 ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else if ( type == "keypress" )
{
2012-02-19 12:15:42 +01:00
if ( ( ! specialHandled ) && false /*parenModule.shouldNormalizeOnChar(charCode)*/ )
2011-07-07 19:59:34 +02:00
{
idleWorkTimer . atMost ( 0 ) ;
}
else
{
idleWorkTimer . atLeast ( 500 ) ;
}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
else if ( type == "keyup" )
{
2013-07-14 07:51:39 +02:00
var 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
2015-01-21 16:01:39 +01:00
var 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
2015-01-21 16:01:39 +01:00
var isSafariHalfCharacter = ( browser . safari && evt . altKey && keyCode == 229 ) ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
if ( thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter )
{
idleWorkTimer . atLeast ( 3000 ) ; // give user time to type
// if this is a keydown, e.g., the keyup shouldn't trigger a normalize
thisKeyDoesntTriggerNormalize = true ;
2011-03-26 14:10:41 +01:00
}
2012-10-11 16:39:01 +02:00
if ( ( ! specialHandled ) && ( ! thisKeyDoesntTriggerNormalize ) && ( ! inInternationalComposition ) )
2011-07-07 19:59:34 +02:00
{
2013-07-14 07:51:39 +02:00
if ( type != "keyup" )
2011-07-07 19:59:34 +02:00
{
observeChangesAroundSelection ( ) ;
}
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
if ( type == "keyup" )
{
thisKeyDoesntTriggerNormalize = false ;
2011-03-26 14:10:41 +01:00
}
} ) ;
}
var thisKeyDoesntTriggerNormalize = false ;
2018-01-03 22:57:28 +01:00
var arrowKeyWasReleased = true ;
function continuouslyPressingArrowKey ( type ) {
var firstTimeKeyIsContinuouslyPressed = false ;
if ( type == 'keyup' ) arrowKeyWasReleased = true ;
else if ( type == 'keydown' && arrowKeyWasReleased ) {
firstTimeKeyIsContinuouslyPressed = true ;
arrowKeyWasReleased = false ;
}
return ! firstTimeKeyIsContinuouslyPressed ;
}
2011-07-07 19:59:34 +02:00
function doUndoRedo ( which )
{
2011-03-26 14:10:41 +01:00
// precond: normalized DOM
2011-07-07 19:59:34 +02:00
if ( undoModule . enabled )
{
2011-03-26 14:10:41 +01:00
var whichMethod ;
if ( which == "undo" ) whichMethod = 'performUndo' ;
if ( which == "redo" ) whichMethod = 'performRedo' ;
2011-07-07 19:59:34 +02:00
if ( whichMethod )
{
var oldEventType = currentCallStack . editEvent . eventType ;
currentCallStack . startNewEvent ( which ) ;
undoModule [ whichMethod ] ( function ( backset , selectionInfo )
{
if ( backset )
{
performDocumentApplyChangeset ( backset ) ;
}
if ( selectionInfo )
{
performSelectionChange ( lineAndColumnFromChar ( selectionInfo . selStart ) , lineAndColumnFromChar ( selectionInfo . selEnd ) , selectionInfo . selFocusAtStart ) ;
}
var oldEvent = currentCallStack . startNewEvent ( oldEventType , true ) ;
return oldEvent ;
} ) ;
2011-03-26 14:10:41 +01:00
}
}
}
editorInfo . ace _doUndoRedo = doUndoRedo ;
2011-07-07 19:59:34 +02:00
function updateBrowserSelectionFromRep ( )
{
2011-03-26 14:10:41 +01:00
// requires normalized DOM!
2011-07-07 19:59:34 +02:00
var selStart = rep . selStart ,
selEnd = rep . selEnd ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
if ( ! ( selStart && selEnd ) )
{
2011-03-26 14:10:41 +01:00
setSelection ( null ) ;
return ;
}
var selection = { } ;
var ss = [ selStart [ 0 ] , selStart [ 1 ] ] ;
selection . startPoint = getPointForLineAndChar ( ss ) ;
var se = [ selEnd [ 0 ] , selEnd [ 1 ] ] ;
selection . endPoint = getPointForLineAndChar ( se ) ;
2011-07-07 19:59:34 +02:00
selection . focusAtStart = ! ! rep . selFocusAtStart ;
2011-03-26 14:10:41 +01:00
setSelection ( selection ) ;
}
2013-12-18 23:13:03 +01:00
editorInfo . ace _updateBrowserSelectionFromRep = updateBrowserSelectionFromRep ;
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
function nodeMaxIndex ( nd )
{
2011-03-26 14:10:41 +01:00
if ( isNodeText ( nd ) ) return nd . nodeValue . length ;
else return 1 ;
}
2011-07-07 19:59:34 +02:00
function hasIESelection ( )
{
2011-03-26 14:10:41 +01:00
var browserSelection ;
2011-07-07 19:59:34 +02:00
try
{
browserSelection = doc . selection ;
}
catch ( e )
{ }
if ( ! browserSelection ) return false ;
2011-03-26 14:10:41 +01:00
var origSelectionRange ;
2011-07-07 19:59:34 +02:00
try
{
origSelectionRange = browserSelection . createRange ( ) ;
}
catch ( e )
{ }
if ( ! origSelectionRange ) return false ;
2011-03-26 14:10:41 +01:00
return true ;
}
2011-07-07 19:59:34 +02:00
function getSelection ( )
{
2011-03-26 14:10:41 +01:00
// returns null, or a structure containing startPoint and endPoint,
// each of which has node (a magicdom node), index, and maxIndex. If the node
// is a text node, maxIndex is the length of the text; else maxIndex is 1.
// index is between 0 and maxIndex, inclusive.
2015-01-21 16:01:39 +01:00
if ( browser . msie )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
var browserSelection ;
2011-07-07 19:59:34 +02:00
try
{
browserSelection = doc . selection ;
}
catch ( e )
{ }
if ( ! browserSelection ) return null ;
2011-03-26 14:10:41 +01:00
var origSelectionRange ;
2011-07-07 19:59:34 +02:00
try
{
origSelectionRange = browserSelection . createRange ( ) ;
}
catch ( e )
{ }
if ( ! origSelectionRange ) return null ;
2011-03-26 14:10:41 +01:00
var selectionParent = origSelectionRange . parentElement ( ) ;
if ( selectionParent . ownerDocument != doc ) return null ;
2011-07-07 19:59:34 +02:00
2012-02-19 14:52:24 +01:00
var newRange = function ( )
2011-07-07 19:59:34 +02:00
{
return doc . body . createTextRange ( ) ;
2012-02-19 14:52:24 +01:00
} ;
2011-07-07 19:59:34 +02:00
2012-02-19 14:52:24 +01:00
var rangeForElementNode = function ( nd )
2011-07-07 19:59:34 +02:00
{
var rng = newRange ( ) ;
// doesn't work on text nodes
rng . moveToElementText ( nd ) ;
return rng ;
2012-02-19 14:52:24 +01:00
} ;
2011-07-07 19:59:34 +02:00
2012-02-19 14:52:24 +01:00
var pointFromCollapsedRange = function ( rng )
2011-07-07 19:59:34 +02:00
{
var parNode = rng . parentElement ( ) ;
var elemBelow = - 1 ;
var elemAbove = parNode . childNodes . length ;
var rangeWithin = rangeForElementNode ( parNode ) ;
2012-02-19 14:52:24 +01:00
if ( rng . compareEndPoints ( "StartToStart" , rangeWithin ) === 0 )
2011-07-07 19:59:34 +02:00
{
return {
node : parNode ,
index : 0 ,
maxIndex : 1
} ;
}
2012-02-19 14:52:24 +01:00
else if ( rng . compareEndPoints ( "EndToEnd" , rangeWithin ) === 0 )
2011-07-07 19:59:34 +02:00
{
if ( isBlockElement ( parNode ) && parNode . nextSibling )
{
// caret after block is not consistent across browsers
// (same line vs next) so put caret before next node
return {
node : parNode . nextSibling ,
index : 0 ,
maxIndex : 1
} ;
}
return {
node : parNode ,
index : 1 ,
maxIndex : 1
} ;
}
2012-02-19 14:52:24 +01:00
else if ( parNode . childNodes . length === 0 )
2011-07-07 19:59:34 +02:00
{
return {
node : parNode ,
index : 0 ,
maxIndex : 1
} ;
}
for ( var i = 0 ; i < parNode . childNodes . length ; i ++ )
{
var n = parNode . childNodes . item ( i ) ;
if ( ! isNodeText ( n ) )
{
var nodeRange = rangeForElementNode ( n ) ;
var startComp = rng . compareEndPoints ( "StartToStart" , nodeRange ) ;
var endComp = rng . compareEndPoints ( "EndToEnd" , nodeRange ) ;
if ( startComp >= 0 && endComp <= 0 )
{
var index = 0 ;
if ( startComp > 0 )
{
index = 1 ;
}
return {
node : n ,
index : index ,
maxIndex : 1
} ;
}
else if ( endComp > 0 )
{
if ( i > elemBelow )
{
elemBelow = i ;
rangeWithin . setEndPoint ( "StartToEnd" , nodeRange ) ;
}
}
else if ( startComp < 0 )
{
if ( i < elemAbove )
{
elemAbove = i ;
rangeWithin . setEndPoint ( "EndToStart" , nodeRange ) ;
}
}
}
}
if ( ( elemAbove - elemBelow ) == 1 )
{
if ( elemBelow >= 0 )
{
return {
node : parNode . childNodes . item ( elemBelow ) ,
index : 1 ,
maxIndex : 1
} ;
}
else
{
return {
node : parNode . childNodes . item ( elemAbove ) ,
index : 0 ,
maxIndex : 1
} ;
}
}
var idx = 0 ;
var r = rng . duplicate ( ) ;
// infinite stateful binary search! call function for values 0 to inf,
// expecting the answer to be about 40. return index of smallest
// true value.
var indexIntoRange = binarySearchInfinite ( 40 , function ( i )
{
// the search algorithm whips the caret back and forth,
// though it has to be moved relatively and may hit
// the end of the buffer
var delta = i - idx ;
var moved = Math . abs ( r . move ( "character" , - delta ) ) ;
// next line is work-around for fact that when moving left, the beginning
// of a text node is considered to be after the start of the parent element:
if ( r . move ( "character" , - 1 ) ) r . move ( "character" , 1 ) ;
if ( delta < 0 ) idx -= moved ;
else idx += moved ;
return ( r . compareEndPoints ( "StartToStart" , rangeWithin ) <= 0 ) ;
} ) ;
// iterate over consecutive text nodes, point is in one of them
var textNode = elemBelow + 1 ;
var indexLeft = indexIntoRange ;
while ( textNode < elemAbove )
{
var tn = parNode . childNodes . item ( textNode ) ;
if ( indexLeft <= tn . nodeValue . length )
{
return {
node : tn ,
index : indexLeft ,
maxIndex : tn . nodeValue . length
} ;
}
indexLeft -= tn . nodeValue . length ;
textNode ++ ;
}
var tn = parNode . childNodes . item ( textNode - 1 ) ;
return {
node : tn ,
index : tn . nodeValue . length ,
maxIndex : tn . nodeValue . length
} ;
2012-02-19 14:52:24 +01:00
} ;
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
var selection = { } ;
2012-02-19 14:52:24 +01:00
if ( origSelectionRange . compareEndPoints ( "StartToEnd" , origSelectionRange ) === 0 )
2011-07-07 19:59:34 +02:00
{
// collapsed
var pnt = pointFromCollapsedRange ( origSelectionRange ) ;
selection . startPoint = pnt ;
selection . endPoint = {
node : pnt . node ,
index : pnt . index ,
maxIndex : pnt . maxIndex
} ;
}
else
{
var start = origSelectionRange . duplicate ( ) ;
start . collapse ( true ) ;
var end = origSelectionRange . duplicate ( ) ;
end . collapse ( false ) ;
selection . startPoint = pointFromCollapsedRange ( start ) ;
selection . endPoint = pointFromCollapsedRange ( end ) ;
2011-03-26 14:10:41 +01:00
}
return selection ;
}
2011-07-07 19:59:34 +02:00
else
{
2011-03-26 14:10:41 +01:00
// non-IE browser
var browserSelection = window . getSelection ( ) ;
2011-07-07 19:59:34 +02:00
if ( browserSelection && browserSelection . type != "None" && browserSelection . rangeCount !== 0 )
{
var range = browserSelection . getRangeAt ( 0 ) ;
function isInBody ( n )
{
while ( n && ! ( n . tagName && n . tagName . toLowerCase ( ) == "body" ) )
{
n = n . parentNode ;
}
return ! ! n ;
}
function pointFromRangeBound ( container , offset )
{
if ( ! isInBody ( container ) )
{
// command-click in Firefox selects whole document, HEAD and BODY!
return {
node : root ,
index : 0 ,
maxIndex : 1
} ;
}
var n = container ;
var childCount = n . childNodes . length ;
if ( isNodeText ( n ) )
{
return {
node : n ,
index : offset ,
maxIndex : n . nodeValue . length
} ;
}
2012-02-19 14:52:24 +01:00
else if ( childCount === 0 )
2011-07-07 19:59:34 +02:00
{
return {
node : n ,
index : 0 ,
maxIndex : 1
} ;
}
// treat point between two nodes as BEFORE the second (rather than after the first)
// if possible; this way point at end of a line block-element is treated as
// at beginning of next line
else if ( offset == childCount )
{
var nd = n . childNodes . item ( childCount - 1 ) ;
var max = nodeMaxIndex ( nd ) ;
return {
node : nd ,
index : max ,
maxIndex : max
} ;
}
else
{
var nd = n . childNodes . item ( offset ) ;
var max = nodeMaxIndex ( nd ) ;
return {
node : nd ,
index : 0 ,
maxIndex : max
} ;
}
}
var selection = { } ;
selection . startPoint = pointFromRangeBound ( range . startContainer , range . startOffset ) ;
selection . endPoint = pointFromRangeBound ( range . endContainer , range . endOffset ) ;
selection . focusAtStart = ( ( ( range . startContainer != range . endContainer ) || ( range . startOffset != range . endOffset ) ) && browserSelection . anchorNode && ( browserSelection . anchorNode == range . endContainer ) && ( browserSelection . anchorOffset == range . endOffset ) ) ;
2013-06-14 19:37:41 +02:00
2013-02-03 14:44:37 +01:00
if ( selection . startPoint . node . ownerDocument !== window . document ) {
return null ;
}
2011-07-07 19:59:34 +02:00
return selection ;
2011-03-26 14:10:41 +01:00
}
else return null ;
}
}
2011-07-07 19:59:34 +02:00
function setSelection ( selection )
{
function copyPoint ( pt )
{
return {
node : pt . node ,
index : pt . index ,
maxIndex : pt . maxIndex
} ;
2011-03-26 14:10:41 +01:00
}
2015-01-21 16:01:39 +01:00
if ( browser . msie )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
// Oddly enough, accessing scrollHeight fixes return key handling on IE 8,
// presumably by forcing some kind of internal DOM update.
doc . body . scrollHeight ;
2011-07-07 19:59:34 +02:00
function moveToElementText ( s , n )
{
while ( n . firstChild && ! isNodeText ( n . firstChild ) )
{
2011-03-26 14:10:41 +01:00
n = n . firstChild ;
}
s . moveToElementText ( n ) ;
}
2011-07-07 19:59:34 +02:00
function newRange ( )
{
return doc . body . createTextRange ( ) ;
}
function setCollapsedBefore ( s , n )
{
// s is an IE TextRange, n is a dom node
if ( isNodeText ( n ) )
{
// previous node should not also be text, but prevent inf recurs
if ( n . previousSibling && ! isNodeText ( n . previousSibling ) )
{
setCollapsedAfter ( s , n . previousSibling ) ;
}
else
{
setCollapsedBefore ( s , n . parentNode ) ;
}
}
else
{
moveToElementText ( s , n ) ;
2011-03-26 14:10:41 +01:00
// work around for issue that caret at beginning of line
// somehow ends up at end of previous line
2011-07-07 19:59:34 +02:00
if ( s . move ( 'character' , 1 ) )
{
2011-03-26 14:10:41 +01:00
s . move ( 'character' , - 1 ) ;
}
2011-07-07 19:59:34 +02:00
s . collapse ( true ) ; // to start
}
}
function setCollapsedAfter ( s , n )
{
// s is an IE TextRange, n is a magicdom node
if ( isNodeText ( n ) )
{
// can't use end of container when no nextSibling (could be on next line),
// so use previousSibling or start of container and move forward.
setCollapsedBefore ( s , n ) ;
s . move ( "character" , n . nodeValue . length ) ;
}
else
{
moveToElementText ( s , n ) ;
s . collapse ( false ) ; // to end
}
}
function getPointRange ( point )
{
var s = newRange ( ) ;
var n = point . node ;
if ( isNodeText ( n ) )
{
setCollapsedBefore ( s , n ) ;
s . move ( "character" , point . index ) ;
}
2012-02-19 14:52:24 +01:00
else if ( point . index === 0 )
2011-07-07 19:59:34 +02:00
{
setCollapsedBefore ( s , n ) ;
}
else
{
setCollapsedAfter ( s , n ) ;
}
return s ;
}
if ( selection )
{
if ( ! hasIESelection ( ) )
{
return ; // don't steal focus
}
var startPoint = copyPoint ( selection . startPoint ) ;
var endPoint = copyPoint ( selection . endPoint ) ;
// fix issue where selection can't be extended past end of line
// with shift-rightarrow or shift-downarrow
if ( endPoint . index == endPoint . maxIndex && endPoint . node . nextSibling )
{
endPoint . node = endPoint . node . nextSibling ;
endPoint . index = 0 ;
endPoint . maxIndex = nodeMaxIndex ( endPoint . node ) ;
}
var range = getPointRange ( startPoint ) ;
range . setEndPoint ( "EndToEnd" , getPointRange ( endPoint ) ) ;
// setting the selection in IE causes everything to scroll
// so that the selection is visible. if setting the selection
// definitely accomplishes nothing, don't do it.
function isEqualToDocumentSelection ( rng )
{
var browserSelection ;
try
{
browserSelection = doc . selection ;
}
catch ( e )
{ }
if ( ! browserSelection ) return false ;
var rng2 = browserSelection . createRange ( ) ;
if ( rng2 . parentElement ( ) . ownerDocument != doc ) return false ;
if ( rng . compareEndPoints ( "StartToStart" , rng2 ) !== 0 ) return false ;
if ( rng . compareEndPoints ( "EndToEnd" , rng2 ) !== 0 ) return false ;
return true ;
}
if ( ! isEqualToDocumentSelection ( range ) )
{
//dmesg(toSource(selection));
//dmesg(escapeHTML(doc.body.innerHTML));
range . select ( ) ;
}
}
else
{
try
{
doc . selection . empty ( ) ;
}
catch ( e )
{ }
}
}
else
{
2011-03-26 14:10:41 +01:00
// non-IE browser
var isCollapsed ;
2011-07-07 19:59:34 +02:00
function pointToRangeBound ( pt )
{
var p = copyPoint ( pt ) ;
// Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level,
// and also problem where cut/copy of a whole line selected with fake arrow-keys
// copies the next line too.
if ( isCollapsed )
{
function diveDeep ( )
{
while ( p . node . childNodes . length > 0 )
{
//&& (p.node == root || p.node.parentNode == root)) {
2012-02-19 14:52:24 +01:00
if ( p . index === 0 )
2011-07-07 19:59:34 +02:00
{
p . node = p . node . firstChild ;
p . maxIndex = nodeMaxIndex ( p . node ) ;
}
else if ( p . index == p . maxIndex )
{
p . node = p . node . lastChild ;
p . maxIndex = nodeMaxIndex ( p . node ) ;
p . index = p . maxIndex ;
}
else break ;
}
}
// now fix problem where cursor at end of text node at end of span-like element
// with background doesn't seem to show up...
if ( isNodeText ( p . node ) && p . index == p . maxIndex )
{
var n = p . node ;
while ( ( ! n . nextSibling ) && ( n != root ) && ( n . parentNode != root ) )
{
n = n . parentNode ;
}
if ( n . nextSibling && ( ! ( ( typeof n . nextSibling . tagName ) == "string" && n . nextSibling . tagName . toLowerCase ( ) == "br" ) ) && ( n != p . node ) && ( n != root ) && ( n . parentNode != root ) )
{
// found a parent, go to next node and dive in
p . node = n . nextSibling ;
p . maxIndex = nodeMaxIndex ( p . node ) ;
p . index = 0 ;
diveDeep ( ) ;
}
}
// try to make sure insertion point is styled;
2011-03-26 14:10:41 +01:00
// also fixes other FF problems
2011-07-07 19:59:34 +02:00
if ( ! isNodeText ( p . node ) )
{
diveDeep ( ) ;
}
}
if ( isNodeText ( p . node ) )
{
return {
container : p . node ,
offset : p . index
} ;
}
else
{
// p.index in {0,1}
return {
container : p . node . parentNode ,
offset : childIndex ( p . node ) + p . index
} ;
}
2011-03-26 14:10:41 +01:00
}
var browserSelection = window . getSelection ( ) ;
2011-07-07 19:59:34 +02:00
if ( browserSelection )
{
browserSelection . removeAllRanges ( ) ;
if ( selection )
{
isCollapsed = ( selection . startPoint . node === selection . endPoint . node && selection . startPoint . index === selection . endPoint . index ) ;
var start = pointToRangeBound ( selection . startPoint ) ;
var end = pointToRangeBound ( selection . endPoint ) ;
if ( ( ! isCollapsed ) && selection . focusAtStart && browserSelection . collapse && browserSelection . extend )
{
// can handle "backwards"-oriented selection, shift-arrow-keys move start
// of selection
browserSelection . collapse ( end . container , end . offset ) ;
browserSelection . extend ( start . container , start . offset ) ;
}
else
{
var range = doc . createRange ( ) ;
range . setStart ( start . container , start . offset ) ;
range . setEnd ( end . container , end . offset ) ;
browserSelection . removeAllRanges ( ) ;
browserSelection . addRange ( range ) ;
}
}
}
}
}
function childIndex ( n )
{
2011-03-26 14:10:41 +01:00
var idx = 0 ;
2011-07-07 19:59:34 +02:00
while ( n . previousSibling )
{
2011-03-26 14:10:41 +01:00
idx ++ ;
n = n . previousSibling ;
}
return idx ;
}
2011-07-07 19:59:34 +02:00
function fixView ( )
{
2011-03-26 14:10:41 +01:00
// calling this method repeatedly should be fast
2012-02-19 14:52:24 +01:00
if ( getInnerWidth ( ) === 0 || getInnerHeight ( ) === 0 )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
return ;
}
var win = outerWin ;
enforceEditability ( ) ;
2012-03-14 01:41:05 +01:00
$ ( sideDiv ) . addClass ( 'sidedivdelayed' ) ;
2011-03-26 14:10:41 +01:00
}
var _teardownActions = [ ] ;
2011-07-07 19:59:34 +02:00
function teardown ( )
{
2012-03-17 13:36:42 +01:00
_ . each ( _teardownActions , function ( a )
2011-07-07 19:59:34 +02:00
{
a ( ) ;
} ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function setDesignMode ( newVal )
{
try
{
function setIfNecessary ( target , prop , val )
{
if ( String ( target [ prop ] ) . toLowerCase ( ) != val )
{
target [ prop ] = val ;
return true ;
}
return false ;
}
2015-01-21 16:01:39 +01:00
if ( browser . msie || browser . safari )
2011-07-07 19:59:34 +02:00
{
setIfNecessary ( root , 'contentEditable' , ( newVal ? 'true' : 'false' ) ) ;
}
else
{
var wasSet = setIfNecessary ( doc , 'designMode' , ( newVal ? 'on' : 'off' ) ) ;
2015-01-21 16:01:39 +01:00
if ( wasSet && newVal && browser . opera )
2011-07-07 19:59:34 +02:00
{
// turning on designMode clears event handlers
bindTheEventHandlers ( ) ;
}
2011-03-26 14:10:41 +01:00
}
return true ;
}
2011-07-07 19:59:34 +02:00
catch ( e )
{
2011-03-26 14:10:41 +01:00
return false ;
}
}
var iePastedLines = null ;
2011-07-07 19:59:34 +02:00
function handleIEPaste ( evt )
{
2011-03-26 14:10:41 +01:00
// Pasting in IE loses blank lines in a way that loses information;
// "one\n\ntwo\nthree" becomes "<p>one</p><p>two</p><p>three</p>",
// which becomes "one\ntwo\nthree". We can get the correct text
// from the clipboard directly, but we still have to let the paste
// happen to get the style information.
var clipText = window . clipboardData && window . clipboardData . getData ( "Text" ) ;
2011-07-07 19:59:34 +02:00
if ( clipText && doc . selection )
{
2011-03-26 14:10:41 +01:00
// this "paste" event seems to mess with the selection whether we try to
// stop it or not, so can't really do document-level manipulation now
// or in an idle call-stack. instead, use IE native manipulation
//function escapeLine(txt) {
//return processSpaces(escapeHTML(textify(txt)));
//}
//var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('<br>');
//doc.selection.createRange().pasteHTML(newHTML);
//evt.preventDefault();
//iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify);
}
}
2012-10-11 16:39:01 +02:00
var inInternationalComposition = false ;
2011-07-07 19:59:34 +02:00
function handleCompositionEvent ( evt )
{
2012-10-11 16:39:01 +02:00
// international input events, fired in FF3, at least; allow e.g. Japanese input
if ( evt . type == "compositionstart" )
{
inInternationalComposition = true ;
}
else if ( evt . type == "compositionend" )
{
inInternationalComposition = false ;
}
}
editorInfo . ace _getInInternationalComposition = function ( )
{
return inInternationalComposition ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function bindTheEventHandlers ( )
{
2012-03-14 01:41:05 +01:00
$ ( document ) . on ( "keydown" , handleKeyEvent ) ;
2013-02-12 20:04:43 +01:00
$ ( document ) . on ( "keypress" , handleKeyEvent ) ;
2012-03-14 01:41:05 +01:00
$ ( 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
$ ( 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
2012-03-14 01:41:05 +01:00
$ ( root ) . on ( "blur" , handleBlur ) ;
2015-01-21 16:01:39 +01:00
if ( browser . msie )
2011-07-07 19:59:34 +02:00
{
2012-03-14 01:41:05 +01:00
$ ( document ) . on ( "click" , handleIEOuterClick ) ;
2011-03-26 14:10:41 +01:00
}
2015-01-21 16:01:39 +01:00
if ( browser . msie ) $ ( root ) . on ( "paste" , handleIEPaste ) ;
2015-01-19 00:58:47 +01:00
// Don't paste on middle click of links
$ ( root ) . on ( "paste" , function ( e ) {
2015-10-13 23:39:23 +02:00
// TODO: this breaks pasting strings into URLS when using
2015-04-11 11:45:51 +02:00
// Control C and Control V -- the Event is never available
// here.. :(
if ( e . target . a || e . target . localName === "a" ) {
2015-01-19 00:58:47 +01:00
e . preventDefault ( ) ;
}
2015-11-06 14:21:25 +01:00
// Call paste hook
hooks . callAll ( 'acePaste' , {
editorInfo : editorInfo ,
rep : rep ,
2015-12-05 20:06:40 +01:00
documentAttributeManager : documentAttributeManager ,
e : e
2015-11-06 14:21:25 +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..
$ ( document ) . on ( "drop" , function ( e ) {
2015-12-05 19:50:51 +01:00
if ( e . target . a || e . target . localName === "a" ) {
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)
var selection = getSelection ( ) ;
if ( selection ) {
var firstLineSelected = topLevel ( selection . startPoint . node ) ;
var lastLineSelected = topLevel ( selection . endPoint . node ) ;
var lineBeforeSelection = firstLineSelected . previousSibling ;
var lineAfterSelection = lastLineSelected . nextSibling ;
var neighbor = lineBeforeSelection || lineAfterSelection ;
neighbor . appendChild ( document . createElement ( 'style' ) ) ;
}
2015-12-05 20:06:40 +01:00
// Call drop hook
2015-12-05 19:50:51 +01:00
hooks . callAll ( 'aceDrop' , {
editorInfo : editorInfo ,
rep : rep ,
2015-12-05 20:06:40 +01:00
documentAttributeManager : documentAttributeManager ,
e : e
2015-12-05 19:50:51 +01:00
} ) ;
} ) ;
2012-09-13 16:19:53 +02:00
// CompositionEvent is not implemented below IE version 8
2015-01-21 16:06:29 +01:00
if ( ! ( browser . msie && parseInt ( browser . version <= 9 ) ) && document . documentElement )
2011-07-07 19:59:34 +02:00
{
2012-03-14 01:41:05 +01:00
$ ( document . documentElement ) . on ( "compositionstart" , handleCompositionEvent ) ;
$ ( document . documentElement ) . on ( "compositionend" , handleCompositionEvent ) ;
2011-03-26 14:10:41 +01:00
}
}
2016-08-22 23:44:17 +02:00
function topLevel ( n )
{
if ( ( ! n ) || n == root ) return null ;
while ( n . parentNode != root )
{
n = n . parentNode ;
}
return n ;
}
2011-07-07 19:59:34 +02:00
function handleIEOuterClick ( evt )
{
if ( ( evt . target . tagName || '' ) . toLowerCase ( ) != "html" )
{
2011-03-26 14:10:41 +01:00
return ;
}
2011-07-07 19:59:34 +02:00
if ( ! ( evt . pageY > root . clientHeight ) )
{
2011-03-26 14:10:41 +01:00
return ;
}
// click below the body
2012-03-22 18:34:08 +01:00
inCallStackIfNecessary ( "handleOuterClick" , function ( )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
// put caret at bottom of doc
fastIncorp ( 11 ) ;
2011-07-07 19:59:34 +02:00
if ( isCaret ( ) )
{ // don't interfere with drag
var lastLine = rep . lines . length ( ) - 1 ;
var lastCol = rep . lines . atIndex ( lastLine ) . text . length ;
performSelectionChange ( [ lastLine , lastCol ] , [ lastLine , lastCol ] ) ;
2011-03-26 14:10:41 +01:00
}
} ) ;
}
2011-07-07 19:59:34 +02:00
function getClassArray ( elem , optFilter )
{
2011-03-26 14:10:41 +01:00
var bodyClasses = [ ] ;
2011-07-07 19:59:34 +02:00
( elem . className || '' ) . replace ( /\S+/g , function ( c )
{
if ( ( ! optFilter ) || ( optFilter ( c ) ) )
{
bodyClasses . push ( c ) ;
2011-03-26 14:10:41 +01:00
}
} ) ;
return bodyClasses ;
}
2011-07-07 19:59:34 +02:00
function setClassArray ( elem , array )
{
2011-03-26 14:10:41 +01:00
elem . className = array . join ( ' ' ) ;
}
2011-07-07 19:59:34 +02:00
function setClassPresence ( elem , className , present )
{
2012-03-14 01:41:05 +01:00
if ( present ) $ ( elem ) . addClass ( className ) ;
2012-04-13 23:21:07 +02:00
else $ ( elem ) . removeClass ( className ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function focus ( )
{
2011-03-26 14:10:41 +01:00
window . focus ( ) ;
}
2011-07-07 19:59:34 +02:00
function handleBlur ( evt )
{
2015-01-21 16:01:39 +01:00
if ( browser . msie )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
// a fix: in IE, clicking on a control like a button outside the
// iframe can "blur" the editor, causing it to stop getting
// events, though typing still affects it(!).
setSelection ( null ) ;
}
}
2011-07-07 19:59:34 +02:00
function getSelectionPointX ( point )
{
2011-03-26 14:10:41 +01:00
// doesn't work in wrap-mode
var node = point . node ;
var index = point . index ;
2011-07-07 19:59:34 +02:00
function leftOf ( n )
{
return n . offsetLeft ;
}
function rightOf ( n )
{
return n . offsetLeft + n . offsetWidth ;
}
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 ) ;
}
2011-07-07 19:59:34 +02: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.
var charsToLeft = index ;
var charsToRight = node . nodeValue . length - index ;
var n ;
2011-07-07 19:59:34 +02:00
for ( n = node . previousSibling ; n && isNodeText ( n ) ; n = n . previousSibling )
charsToLeft += n . nodeValue ;
2011-03-26 14:10:41 +01:00
var leftEdge = ( n ? rightOf ( n ) : leftOf ( node . parentNode ) ) ;
2011-07-07 19:59:34 +02:00
for ( n = node . nextSibling ; n && isNodeText ( n ) ; n = n . nextSibling )
charsToRight += n . nodeValue ;
2011-03-26 14:10:41 +01:00
var rightEdge = ( n ? leftOf ( n ) : rightOf ( node . parentNode ) ) ;
var frac = ( charsToLeft / ( charsToLeft + charsToRight ) ) ;
2011-07-07 19:59:34 +02:00
var pixLoc = leftEdge + frac * ( rightEdge - leftEdge ) ;
2011-03-26 14:10:41 +01:00
return Math . round ( pixLoc ) ;
}
}
2011-07-07 19:59:34 +02:00
function getPageHeight ( )
{
2011-03-26 14:10:41 +01:00
var win = outerWin ;
var odoc = win . document ;
if ( win . innerHeight && win . scrollMaxY ) return win . innerHeight + win . scrollMaxY ;
else if ( odoc . body . scrollHeight > odoc . body . offsetHeight ) return odoc . body . scrollHeight ;
else return odoc . body . offsetHeight ;
}
2011-07-07 19:59:34 +02:00
function getPageWidth ( )
{
2011-03-26 14:10:41 +01:00
var win = outerWin ;
var odoc = win . document ;
if ( win . innerWidth && win . scrollMaxX ) return win . innerWidth + win . scrollMaxX ;
else if ( odoc . body . scrollWidth > odoc . body . offsetWidth ) return odoc . body . scrollWidth ;
else return odoc . body . offsetWidth ;
}
2011-07-07 19:59:34 +02:00
function getInnerHeight ( )
{
2011-03-26 14:10:41 +01:00
var win = outerWin ;
var odoc = win . document ;
var h ;
2015-01-21 16:01:39 +01:00
if ( browser . opera ) h = win . innerHeight ;
2011-03-26 14:10:41 +01:00
else h = odoc . documentElement . clientHeight ;
if ( h ) return h ;
// deal with case where iframe is hidden, hope that
// style.height of iframe container is set in px
2011-07-07 19:59:34 +02:00
return Number ( editorInfo . frame . parentNode . style . height . replace ( /[^0-9]/g , '' ) || 0 ) ;
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function getInnerWidth ( )
{
2011-03-26 14:10:41 +01:00
var win = outerWin ;
var odoc = win . document ;
return odoc . documentElement . clientWidth ;
}
2011-07-07 19:59:34 +02:00
function scrollXHorizontallyIntoView ( pixelX )
{
2011-03-26 14:10:41 +01:00
var win = outerWin ;
var odoc = outerWin . document ;
var distInsideLeft = pixelX - win . scrollX ;
var distInsideRight = win . scrollX + getInnerWidth ( ) - pixelX ;
2011-07-07 19:59:34 +02:00
if ( distInsideLeft < 0 )
{
2011-03-26 14:10:41 +01:00
win . scrollBy ( distInsideLeft , 0 ) ;
}
2011-07-07 19:59:34 +02:00
else if ( distInsideRight < 0 )
{
win . scrollBy ( - distInsideRight + 1 , 0 ) ;
2011-03-26 14:10:41 +01:00
}
}
2011-07-07 19:59:34 +02:00
function scrollSelectionIntoView ( )
{
if ( ! rep . selStart ) return ;
2011-03-26 14:10:41 +01:00
fixView ( ) ;
2018-01-03 22:57:28 +01:00
var innerHeight = getInnerHeight ( ) ;
scroll . scrollNodeVerticallyIntoView ( rep , innerHeight ) ;
2011-07-07 19:59:34 +02:00
if ( ! doesWrap )
{
2011-03-26 14:10:41 +01:00
var browserSelection = getSelection ( ) ;
2011-07-07 19:59:34 +02:00
if ( browserSelection )
{
var focusPoint = ( browserSelection . focusAtStart ? browserSelection . startPoint : browserSelection . endPoint ) ;
var selectionPointX = getSelectionPointX ( focusPoint ) ;
scrollXHorizontallyIntoView ( selectionPointX ) ;
fixView ( ) ;
2011-03-26 14:10:41 +01:00
}
}
}
2013-06-14 19:37:41 +02:00
2012-04-05 00:50:04 +02:00
var listAttributeName = 'list' ;
2013-06-14 19:37:41 +02:00
2011-07-07 19:59:34 +02:00
function getLineListType ( lineNum )
{
2012-04-05 00:50:04 +02:00
return documentAttributeManager . getAttributeOnLine ( lineNum , listAttributeName )
2011-03-26 14:10:41 +01:00
}
2011-07-07 19:59:34 +02:00
function setLineListType ( lineNum , listType )
{
2012-04-05 00:50:04 +02:00
if ( listType == '' ) {
documentAttributeManager . removeAttributeOnLine ( lineNum , listAttributeName ) ;
2014-12-30 17:45:26 +01:00
documentAttributeManager . removeAttributeOnLine ( lineNum , 'start' ) ;
2012-04-05 00:50:04 +02:00
} else {
documentAttributeManager . setAttributeOnLine ( lineNum , listAttributeName , listType ) ;
}
2013-06-14 19:37:41 +02:00
2012-04-05 00:50:04 +02:00
//if the list has been removed, it is necessary to renumber
//starting from the *next* line because the list may have been
//separated. If it returns null, it means that the list was not cut, try
//from the current one.
if ( renumberList ( lineNum + 1 ) == null )
{
renumberList ( lineNum ) ;
}
2011-03-26 14:10:41 +01:00
}
2013-06-14 19:37:41 +02:00
2012-01-15 18:20:20 +01:00
function renumberList ( lineNum ) {
//1-check we are in a list
var type = getLineListType ( lineNum ) ;
if ( ! type )
{
return null ;
}
2015-01-19 01:28:32 +01:00
type = /([a-z]+)[0-9]+/ . exec ( type ) ;
2012-01-15 18:20:20 +01:00
if ( type [ 1 ] == "indent" )
{
return null ;
}
2013-06-14 19:37:41 +02:00
2012-01-15 18:20:20 +01:00
//2-find the first line of the list
while ( lineNum - 1 >= 0 && ( type = getLineListType ( lineNum - 1 ) ) )
{
2015-01-19 01:28:32 +01:00
type = /([a-z]+)[0-9]+/ . exec ( type ) ;
2012-01-15 18:20:20 +01:00
if ( type [ 1 ] == "indent" )
break ;
lineNum -- ;
}
2013-06-14 19:37:41 +02:00
2012-01-15 18:20:20 +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
var builder = Changeset . builder ( rep . lines . totalWidth ( ) ) ;
2014-12-14 22:01:28 +01:00
var loc = [ 0 , 0 ] ;
2012-01-15 18:20:20 +01:00
function applyNumberList ( line , level )
{
//init
var position = 1 ;
var curLevel = level ;
var listType ;
//loop over the lines
while ( listType = getLineListType ( line ) )
{
//apply new num
2015-01-19 01:28:32 +01:00
listType = /([a-z]+)([0-9]+)/ . exec ( listType ) ;
2012-01-15 18:20:20 +01:00
curLevel = Number ( listType [ 2 ] ) ;
if ( isNaN ( curLevel ) || listType [ 0 ] == "indent" )
{
return line ;
}
else if ( curLevel == level )
{
2012-04-05 00:50:04 +02:00
ChangesetUtils . buildKeepRange ( rep , builder , loc , ( loc = [ line , 0 ] ) ) ;
ChangesetUtils . buildKeepRange ( rep , builder , loc , ( loc = [ line , 1 ] ) , [
2012-01-15 18:20:20 +01:00
[ 'start' , position ]
] , rep . apool ) ;
2013-06-14 19:37:41 +02:00
2012-01-15 18:20:20 +01:00
position ++ ;
line ++ ;
}
else if ( curLevel < level )
{
return line ; //back to parent
}
else
{
line = applyNumberList ( line , level + 1 ) ; //recursive call
}
}
return line ;
}
2013-06-14 19:37:41 +02:00
2012-01-15 18:20:20 +01:00
applyNumberList ( lineNum , 1 ) ;
var cs = builder . toString ( ) ;
if ( ! Changeset . isIdentity ( cs ) )
{
performDocumentApplyChangeset ( cs ) ;
}
2013-06-14 19:37:41 +02:00
2012-01-15 18:20:20 +01:00
//4-apply the modifications
2013-06-14 19:37:41 +02:00
2012-01-15 18:20:20 +01:00
}
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
2012-01-15 18:20:20 +01:00
function doInsertList ( type )
2011-07-07 19:59:34 +02:00
{
if ( ! ( rep . selStart && rep . selEnd ) )
{
2011-03-26 14:10:41 +01:00
return ;
}
var firstLine , lastLine ;
firstLine = rep . selStart [ 0 ] ;
2012-02-19 14:52:24 +01:00
lastLine = Math . max ( firstLine , rep . selEnd [ 0 ] - ( ( rep . selEnd [ 1 ] === 0 ) ? 1 : 0 ) ) ;
2011-03-26 14:10:41 +01:00
var allLinesAreList = true ;
2011-07-07 19:59:34 +02:00
for ( var n = firstLine ; n <= lastLine ; n ++ )
{
2011-11-25 11:10:57 +01:00
var listType = getLineListType ( n ) ;
2012-01-15 18:20:20 +01:00
if ( ! listType || listType . slice ( 0 , type . length ) != type )
2011-07-07 19:59:34 +02:00
{
2011-03-26 14:10:41 +01:00
allLinesAreList = false ;
break ;
}
}
var mods = [ ] ;
2011-07-07 19:59:34 +02:00
for ( var n = firstLine ; n <= lastLine ; n ++ )
{
2011-11-25 11:10:57 +01:00
var t = '' ;
var level = 0 ;
2014-12-27 13:18:58 +01:00
var listType = /([a-z]+)([0-9]+)/ . exec ( getLineListType ( n ) ) ;
2020-06-07 10:51:12 +02:00
// Used to outdent if ol is removed
if ( allLinesAreList ) {
var togglingOn = false ;
} else {
var togglingOn = true ;
}
2011-11-25 11:10:57 +01:00
if ( listType )
{
t = listType [ 1 ] ;
level = Number ( listType [ 2 ] ) ;
}
2011-03-26 14:10:41 +01:00
var t = getLineListType ( n ) ;
2016-01-19 05:57:40 +01:00
2020-06-07 10:51:12 +02:00
if ( t === listType ) togglingOn = false ;
if ( togglingOn ) {
mods . push ( [ n , allLinesAreList ? 'indent' + level : ( t ? type + level : type + '1' ) ] ) ;
} else {
// scrap the entire indentation and list type
if ( level === 1 ) { // if outdending but are the first item in the list then outdent
setLineListType ( n , '' ) ; // outdent
}
// else change to indented not bullet
if ( level > 1 ) {
setLineListType ( n , '' ) ; // remove bullet
setLineListType ( n , "indent" + level ) ; // in/outdent
}
}
2016-01-19 05:57:40 +01:00
2011-03-26 14:10:41 +01:00
}
2013-06-14 19:37:41 +02:00
2012-04-05 00:50:04 +02:00
_ . each ( mods , function ( mod ) {
2012-04-05 01:07:47 +02:00
setLineListType ( mod [ 0 ] , mod [ 1 ] ) ;
2012-04-05 00:50:04 +02:00
} ) ;
2011-03-26 14:10:41 +01:00
}
2013-03-18 18:40:18 +01:00
2012-01-15 18:20:20 +01:00
function doInsertUnorderedList ( ) {
doInsertList ( 'bullet' ) ;
}
function doInsertOrderedList ( ) {
doInsertList ( 'number' ) ;
}
2011-03-26 14:10:41 +01:00
editorInfo . ace _doInsertUnorderedList = doInsertUnorderedList ;
2012-01-15 18:20:20 +01:00
editorInfo . ace _doInsertOrderedList = doInsertOrderedList ;
2013-06-14 19:37:41 +02:00
2011-03-26 14:10:41 +01:00
var lineNumbersShown ;
var sideDivInner ;
2011-07-07 19:59:34 +02:00
function initLineNumbers ( )
{
2011-03-26 14:10:41 +01:00
lineNumbersShown = 1 ;
2020-05-01 16:43:25 +02:00
sideDiv . innerHTML = '<div id="sidedivinner" class="sidedivinner"><div><span class="line-number">1</span></div></div>' ;
2011-03-26 14:10:41 +01:00
sideDivInner = outerWin . document . getElementById ( "sidedivinner" ) ;
2015-10-22 02:32:02 +02:00
$ ( sideDiv ) . addClass ( "sidediv" ) ;
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
2011-07-07 19:59:34 +02:00
function 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
var lineHeights = [ ] ;
var docLine = doc . body . firstChild ;
var currentLine = 0 ;
var h = null ;
// First loop to calculate the heights from doc body
while ( docLine )
{
if ( docLine . nextSibling ) {
if ( currentLine === 0 ) {
// It's the first line. For line number alignment purposes, its
// height is taken to be the top offset of the next line. If we
// didn't do this special case, we would miss out on any top margin
// included on the first line. The default stylesheet doesn't add
// extra margins/padding, but plugins might.
h = docLine . nextSibling . offsetTop - parseInt ( window . getComputedStyle ( doc . body ) . getPropertyValue ( "padding-top" ) . split ( 'px' ) [ 0 ] ) ;
} else {
h = docLine . nextSibling . offsetTop - docLine . offsetTop ;
}
} else {
// last line
h = ( docLine . clientHeight || docLine . offsetHeight ) ;
}
lineHeights . push ( h )
docLine = docLine . nextSibling ;
currentLine ++ ;
}
2011-03-26 14:10:41 +01:00
var newNumLines = rep . lines . length ( ) ;
if ( newNumLines < 1 ) newNumLines = 1 ;
2020-09-08 15:52:26 +02:00
var 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
currentLine = 0
while ( sidebarLine && currentLine <= lineNumbersShown ) {
if ( lineHeights [ currentLine ] ) {
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
}
2011-12-07 14:23:28 +01:00
if ( newNumLines != lineNumbersShown )
{
var container = sideDivInner ;
var odoc = outerWin . document ;
var fragment = odoc . createDocumentFragment ( ) ;
2020-09-08 15:52:26 +02:00
// Create missing line and apply height
2011-12-07 14:23:28 +01:00
while ( lineNumbersShown < newNumLines )
{
lineNumbersShown ++ ;
2013-06-14 19:37:41 +02:00
var div = odoc . createElement ( "DIV" ) ;
2020-09-08 15:52:26 +02:00
if ( lineHeights [ currentLine ] ) {
div . style . height = lineHeights [ currentLine ] + "px" ;
2013-04-07 20:06:15 +02:00
}
2020-09-08 15:52:26 +02: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
2011-12-07 14:23:28 +01:00
while ( lineNumbersShown > newNumLines )
{
container . removeChild ( container . lastChild ) ;
lineNumbersShown -- ;
2011-03-26 14:10:41 +01:00
}
}
2012-02-19 14:30:57 +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-04-09 16:22:09 +02:00
editorInfo . ace _performDocumentApplyAttributesToRange = function ( ) {
return documentAttributeManager . setAttributesOnRange . apply ( documentAttributeManager , arguments ) ;
} ;
2012-02-19 14:30:57 +01:00
2012-09-12 09:04:15 +02:00
this . init = function ( ) {
$ ( document ) . ready ( function ( ) {
doc = document ; // defined as a var in scope outside
inCallStack ( "setup" , function ( )
2012-03-14 01:41:05 +01:00
{
2012-09-12 09:04:15 +02:00
var body = doc . getElementById ( "innerdocbody" ) ;
root = body ; // defined as a var in scope outside
2015-01-21 16:01:39 +01:00
if ( browser . firefox ) $ ( root ) . addClass ( "mozilla" ) ;
if ( browser . safari ) $ ( root ) . addClass ( "safari" ) ;
if ( browser . msie ) $ ( root ) . addClass ( "msie" ) ;
2012-09-12 09:04:15 +02:00
setClassPresence ( root , "authorColors" , true ) ;
setClassPresence ( root , "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 ) ;
var oneEntry = createDomLineEntry ( "" ) ;
doRepLineSplice ( 0 , rep . lines . length ( ) , [ oneEntry ] ) ;
insertDomLines ( null , [ oneEntry . domInfo ] , null ) ;
rep . alines = Changeset . splitAttributionLines (
Changeset . makeAttribution ( "\n" ) , "\n" ) ;
2012-03-14 01:41:05 +01:00
2012-09-12 09:04:15 +02:00
bindTheEventHandlers ( ) ;
2012-03-14 01:41:05 +01:00
2012-09-12 09:04:15 +02:00
} ) ;
2013-06-14 19:37:41 +02:00
2012-09-12 09:04:15 +02:00
hooks . callAll ( 'aceInitialized' , {
editorInfo : editorInfo ,
rep : rep ,
documentAttributeManager : documentAttributeManager
} ) ;
2013-06-14 19:37:41 +02:00
2013-02-09 17:42:47 +01:00
scheduler . setTimeout ( function ( )
2012-09-12 09:04:15 +02:00
{
parent . readyFunc ( ) ; // defined in code that sets up the inner iframe
} , 0 ) ;
2012-03-14 01:41:05 +01:00
2012-09-12 09:04:15 +02:00
isSetUp = true ;
} ) ;
}
2012-03-14 01:41:05 +01:00
2012-02-24 20:22:32 +01:00
}
2011-03-26 14:10:41 +01:00
2012-09-12 09:04:15 +02:00
exports . init = function ( ) {
var editor = new Ace2Inner ( )
editor . init ( ) ;
} ;