2011-12-04 15:33:56 +00:00
/ * *
* This code is mostly from the old Etherpad . Please help us to comment this code .
* This helps other people to understand this code better and helps them to improve it .
* TL ; DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
* /
2011-03-26 13:10:41 +00:00
/ * *
* Copyright 2009 Google Inc .
2011-07-07 17:59:34 +00:00
*
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
*
2011-03-26 13:10:41 +00:00
* http : //www.apache.org/licenses/LICENSE-2.0
2011-07-07 17:59:34 +00:00
*
2011-03-26 13:10:41 +00: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 .
* /
2011-07-07 17:59:34 +00:00
function OUTER ( gscope )
{
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
var DEBUG = false ; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
// changed to false
2011-03-26 13:10:41 +00:00
var isSetUp = false ;
2011-07-07 17:59:34 +00:00
var THE _TAB = ' ' ; //4
2011-03-26 13:10:41 +00:00
var MAX _LIST _LEVEL = 8 ;
var LINE _NUMBER _PADDING _RIGHT = 4 ;
var LINE _NUMBER _PADDING _LEFT = 4 ;
var MIN _LINEDIV _WIDTH = 20 ;
var EDIT _BODY _PADDING _TOP = 8 ;
var EDIT _BODY _PADDING _LEFT = 8 ;
var caughtErrors = [ ] ;
var thisAuthor = '' ;
var disposed = false ;
var editorInfo = parent . editorInfo ;
var iframe = window . frameElement ;
var outerWin = iframe . ace _outerWin ;
iframe . ace _outerWin = null ; // prevent IE 6 memory leak
var sideDiv = iframe . nextSibling ;
var lineMetricsDiv = sideDiv . nextSibling ;
var overlaysdiv = lineMetricsDiv . nextSibling ;
initLineNumbers ( ) ;
2011-07-07 17:59:34 +00:00
var outsideKeyDown = function ( evt )
{ } ;
var outsideKeyPress = function ( evt )
{
return true ;
} ;
var outsideNotifyDirty = function ( )
{ } ;
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
var rep = {
lines : newSkipList ( ) ,
selStart : null ,
selEnd : null ,
selFocusAtStart : false ,
alltext : "" ,
alines : [ ] ,
apool : new AttribPool ( )
} ;
2011-03-26 13:10:41 +00:00
// lines, alltext, alines, and DOM are set up in setup()
2011-07-07 17:59:34 +00:00
if ( undoModule . enabled )
{
2011-03-26 13:10:41 +00:00
undoModule . apool = rep . apool ;
}
var root , doc ; // set in setup()
var isEditable = true ;
var doesWrap = true ;
var hasLineNumbers = true ;
var isStyled = true ;
2011-12-31 16:46:10 +00:00
2011-03-26 13:10:41 +00:00
// space around the innermost iframe element
var iframePadLeft = MIN _LINEDIV _WIDTH + LINE _NUMBER _PADDING _RIGHT + EDIT _BODY _PADDING _LEFT ;
var iframePadTop = EDIT _BODY _PADDING _TOP ;
2011-07-07 17:59:34 +00:00
var iframePadBottom = 0 ,
iframePadRight = 0 ;
2011-03-26 13:10:41 +00:00
2011-03-26 19:47:31 +00:00
var console = ( DEBUG && window . console ) ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
console = { } ;
for ( var i = 0 ; i < names . length ; ++ i )
2011-07-07 17:59:34 +00:00
console [ names [ i ] ] = function ( )
{ } ;
2011-03-26 13:10:41 +00:00
//console.error = function(str) { alert(str); };
}
2011-03-26 19:47:31 +00:00
2011-03-26 13:10:41 +00:00
var PROFILER = window . PROFILER ;
2011-07-07 17:59:34 +00:00
if ( ! PROFILER )
{
PROFILER = function ( )
{
return {
start : noop ,
mark : noop ,
literal : noop ,
end : noop ,
cancel : noop
} ;
} ;
}
function noop ( )
{ }
function identity ( x )
{
return x ;
2011-03-26 13:10:41 +00: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 ;
var scheduler = parent ;
var textFace = 'monospace' ;
var textSize = 12 ;
2011-07-07 17:59:34 +00:00
function textLineHeight ( )
{
return Math . round ( textSize * 4 / 3 ) ;
}
2011-03-26 13:10:41 +00:00
var dynamicCSS = null ;
2011-07-14 15:15:38 +00:00
var dynamicCSSTop = null ;
2011-07-07 17:59:34 +00:00
function initDynamicCSS ( )
{
2011-03-26 13:10:41 +00:00
dynamicCSS = makeCSSManager ( "dynamicsyntax" ) ;
2011-07-14 15:15:38 +00:00
dynamicCSSTop = makeCSSManager ( "dynamicsyntax" , true ) ;
2011-03-26 13:10:41 +00:00
}
var changesetTracker = makeChangesetTracker ( scheduler , rep . apool , {
2011-07-07 17:59:34 +00:00
withCallbacks : function ( operationName , f )
{
inCallStackIfNecessary ( operationName , function ( )
{
2011-03-26 13:10:41 +00:00
fastIncorp ( 1 ) ;
2011-07-07 17:59:34 +00:00
f (
{
setDocumentAttributedText : function ( atext )
{
setDocAText ( atext ) ;
2011-03-26 13:10:41 +00:00
} ,
2011-07-07 17:59:34 +00:00
applyChangesetToDocument : function ( changeset , preferInsertionAfterCaret )
{
var oldEventType = currentCallStack . editEvent . eventType ;
currentCallStack . startNewEvent ( "nonundoable" ) ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
performDocumentApplyChangeset ( changeset , preferInsertionAfterCaret ) ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
currentCallStack . startNewEvent ( oldEventType ) ;
2011-03-26 13:10:41 +00:00
}
} ) ;
} ) ;
}
} ) ;
var authorInfos = { } ; // presence of key determines if author is present in doc
2011-07-07 17:59:34 +00:00
function setAuthorInfo ( author , info )
{
if ( ( typeof author ) != "string" )
{
throw new Error ( "setAuthorInfo: author (" + author + ") is not a string" ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( ! info )
{
2011-03-26 13:10:41 +00:00
delete authorInfos [ author ] ;
2011-07-07 17:59:34 +00:00
if ( dynamicCSS )
{
dynamicCSS . removeSelectorStyle ( getAuthorColorClassSelector ( getAuthorClassName ( author ) ) ) ;
2011-07-14 15:15:38 +00:00
dynamicCSSTop . removeSelectorStyle ( getAuthorColorClassSelector ( getAuthorClassName ( author ) ) ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
authorInfos [ author ] = info ;
2011-07-07 17:59:34 +00:00
if ( info . bgcolor )
{
if ( dynamicCSS )
{
var bgcolor = info . bgcolor ;
if ( ( typeof info . fade ) == "number" )
{
bgcolor = fadeColor ( bgcolor , info . fade ) ;
}
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
dynamicCSS . selectorStyle ( getAuthorColorClassSelector (
getAuthorClassName ( author ) ) ) . backgroundColor = bgcolor ;
2011-07-14 15:15:38 +00:00
dynamicCSSTop . selectorStyle ( getAuthorColorClassSelector (
getAuthorClassName ( author ) ) ) . backgroundColor = bgcolor ;
2011-07-07 17:59:34 +00:00
}
2011-03-26 13:10:41 +00:00
}
}
}
2011-07-07 17:59:34 +00:00
function getAuthorClassName ( author )
{
return "author-" + author . replace ( /[^a-y0-9]/g , function ( c )
{
2011-03-26 13:10:41 +00:00
if ( c == "." ) return "-" ;
2011-07-07 17:59:34 +00:00
return 'z' + c . charCodeAt ( 0 ) + 'z' ;
2011-03-26 13:10:41 +00:00
} ) ;
}
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
if ( cc == '-' ) return '.' ;
2011-07-07 17:59:34 +00:00
else if ( cc . charAt ( 0 ) == 'z' )
{
return String . fromCharCode ( Number ( cc . slice ( 1 , - 1 ) ) ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
return cc ;
}
} ) ;
}
return null ;
}
2011-07-07 17:59:34 +00:00
function getAuthorColorClassSelector ( oneClassName )
{
return ".authorColors ." + oneClassName ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function setUpTrackingCSS ( )
{
if ( dynamicCSS )
{
2011-03-26 13:10:41 +00:00
var backgroundHeight = lineMetricsDiv . offsetHeight ;
var lineHeight = textLineHeight ( ) ;
var extraBodding = 0 ;
var extraTodding = 0 ;
2011-07-07 17:59:34 +00:00
if ( backgroundHeight < lineHeight )
{
extraBodding = Math . ceil ( ( lineHeight - backgroundHeight ) / 2 ) ;
extraTodding = lineHeight - backgroundHeight - extraBodding ;
2011-03-26 13:10:41 +00:00
}
var spanStyle = dynamicCSS . selectorStyle ( "#innerdocbody span" ) ;
2011-07-07 17:59:34 +00:00
spanStyle . paddingTop = extraTodding + "px" ;
spanStyle . paddingBottom = extraBodding + "px" ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
function boldColorFromColor ( lightColorCSS )
{
2011-03-26 13:10:41 +00:00
var color = colorutils . css2triple ( lightColorCSS ) ;
// amp up the saturation to full
color = colorutils . saturate ( color ) ;
// normalize brightness based on luminosity
color = colorutils . scaleColor ( color , 0 , 0.5 / colorutils . luminosity ( color ) ) ;
return colorutils . triple2css ( color ) ;
}
2011-07-07 17:59:34 +00:00
function fadeColor ( colorCSS , fadeFrac )
{
2011-03-26 13:10:41 +00:00
var color = colorutils . css2triple ( colorCSS ) ;
2011-07-07 17:59:34 +00:00
color = colorutils . blend ( color , [ 1 , 1 , 1 ] , fadeFrac ) ;
2011-03-26 13:10:41 +00:00
return colorutils . triple2css ( color ) ;
}
2011-07-07 17:59:34 +00:00
function doAlert ( str )
{
scheduler . setTimeout ( function ( )
{
alert ( str ) ;
} , 0 ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
editorInfo . ace _getRep = function ( )
{
2011-03-26 13:10:41 +00:00
return rep ;
}
var currentCallStack = null ;
2011-07-07 17:59:34 +00:00
function inCallStack ( type , action )
{
2011-03-26 13:10:41 +00:00
if ( disposed ) return ;
2011-07-07 17:59:34 +00:00
if ( currentCallStack )
{
console . error ( "Can't enter callstack " + type + ", already in " + currentCallStack . type ) ;
2011-03-26 13:10:41 +00:00
}
var profiling = false ;
2011-07-07 17:59:34 +00:00
function profileRest ( )
{
2011-03-26 13:10:41 +00:00
profiling = true ;
console . profile ( ) ;
}
2011-07-07 17:59:34 +00: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
{
if ( evt . eventType == "setup" || evt . eventType == "importText" || evt . eventType == "setBaseText" )
{
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 13:10:41 +00:00
var oldEvent = currentCallStack . editEvent ;
2011-07-07 17:59:34 +00:00
if ( ! dontSubmitOld )
{
submitOldEvent ( oldEvent ) ;
2011-03-26 13:10:41 +00:00
}
currentCallStack . editEvent = newEditEvent ( eventType ) ;
return oldEvent ;
}
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
var cleanExit = false ;
var result ;
2011-07-07 17:59:34 +00:00
try
{
2011-03-26 13:10:41 +00:00
result = action ( ) ;
//console.log("Just did action for: "+type);
cleanExit = true ;
}
2011-07-07 17:59:34 +00:00
catch ( e )
{
caughtErrors . push (
{
error : e ,
time : + new Date ( )
} ) ;
2011-03-26 13:10:41 +00:00
dmesg ( e . toString ( ) ) ;
throw e ;
}
2011-07-07 17:59:34 +00:00
finally
{
2011-03-26 13:10:41 +00:00
var cs = currentCallStack ;
//console.log("Finished action for: "+type);
2011-07-07 17:59:34 +00:00
if ( cleanExit )
{
submitOldEvent ( cs . editEvent ) ;
if ( cs . domClean && cs . type != "setup" )
{
if ( cs . isUserChange )
{
if ( cs . repChanged ) parenModule . notifyChange ( ) ;
else parenModule . notifyTick ( ) ;
}
recolorModule . recolorLines ( ) ;
if ( cs . selectionAffected )
{
updateBrowserSelectionFromRep ( ) ;
}
if ( ( cs . docTextChanged || cs . userChangedSelection ) && cs . type != "applyChangesToBase" )
{
scrollSelectionIntoView ( ) ;
}
if ( cs . docTextChanged && cs . type . indexOf ( "importText" ) < 0 )
{
outsideNotifyDirty ( ) ;
}
}
}
else
{
// non-clean exit
if ( currentCallStack . type == "idleWorkTimer" )
{
idleWorkTimer . atLeast ( 1000 ) ;
}
2011-03-26 13:10:41 +00:00
}
currentCallStack = null ;
if ( profiling ) console . profileEnd ( ) ;
}
return result ;
}
editorInfo . ace _inCallStack = inCallStack ;
2011-07-07 17:59:34 +00:00
function inCallStackIfNecessary ( type , action )
{
if ( ! currentCallStack )
{
2011-03-26 13:10:41 +00:00
inCallStack ( type , action ) ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
action ( ) ;
}
}
editorInfo . ace _inCallStackIfNecessary = inCallStackIfNecessary ;
2011-07-07 17:59:34 +00:00
function recolorLineByKey ( key )
{
if ( rep . lines . containsKey ( key ) )
{
2011-03-26 13:10:41 +00:00
var offset = rep . lines . offsetOfKey ( key ) ;
var width = rep . lines . atKey ( key ) . width ;
recolorLinesInRange ( offset , offset + width ) ;
}
}
2011-07-07 17:59:34 +00:00
function getLineKeyForOffset ( charOffset )
{
2011-03-26 13:10:41 +00:00
return rep . lines . atOffset ( charOffset ) . key ;
}
2011-07-07 17:59:34 +00:00
var recolorModule = ( function ( )
{
2011-03-26 13:10:41 +00:00
var dirtyLineKeys = { } ;
var module = { } ;
2011-07-07 17:59:34 +00:00
module . setCharNeedsRecoloring = function ( offset )
{
if ( offset >= rep . alltext . length )
{
offset = rep . alltext . length - 1 ;
2011-03-26 13:10:41 +00:00
}
dirtyLineKeys [ getLineKeyForOffset ( offset ) ] = true ;
}
2011-07-07 17:59:34 +00:00
module . setCharRangeNeedsRecoloring = function ( offset1 , offset2 )
{
if ( offset1 >= rep . alltext . length )
{
offset1 = rep . alltext . length - 1 ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( offset2 >= rep . alltext . length )
{
offset2 = rep . alltext . length - 1 ;
2011-03-26 13:10:41 +00:00
}
var firstEntry = rep . lines . atOffset ( offset1 ) ;
var lastKey = rep . lines . atOffset ( offset2 ) . key ;
dirtyLineKeys [ lastKey ] = true ;
var entry = firstEntry ;
2011-07-07 17:59:34 +00:00
while ( entry && entry . key != lastKey )
{
dirtyLineKeys [ entry . key ] = true ;
entry = rep . lines . next ( entry ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
module . recolorLines = function ( )
{
for ( var k in dirtyLineKeys )
{
recolorLineByKey ( k ) ;
2011-03-26 13:10:41 +00:00
}
dirtyLineKeys = { } ;
}
return module ;
} ) ( ) ;
2011-07-07 17:59:34 +00:00
var parenModule = ( function ( )
{
2011-03-26 13:10:41 +00:00
var module = { } ;
2011-07-07 17:59:34 +00:00
module . notifyTick = function ( )
{
handleFlashing ( false ) ;
} ;
module . notifyChange = function ( )
{
handleFlashing ( true ) ;
} ;
module . shouldNormalizeOnChar = function ( c )
{
if ( parenFlashRep . active )
{
// avoid highlight style from carrying on to typed text
return true ;
2011-03-26 13:10:41 +00:00
}
c = String . fromCharCode ( c ) ;
2011-07-07 17:59:34 +00:00
return ! ! ( bracketMap [ c ] ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
var parenFlashRep = {
active : false ,
whichChars : null ,
whichLineKeys : null ,
expireTime : null
} ;
var bracketMap = {
'(' : 1 ,
')' : - 1 ,
'[' : 2 ,
']' : - 2 ,
'{' : 3 ,
'}' : - 3
} ;
2011-03-26 13:10:41 +00:00
var bracketRegex = /[{}\[\]()]/g ;
2011-07-07 17:59:34 +00:00
function handleFlashing ( docChanged )
{
function getSearchRange ( aroundLoc )
{
var rng = getVisibleCharRange ( ) ;
var d = 100 ; // minimum radius
var e = 3000 ; // maximum radius;
if ( rng [ 0 ] > aroundLoc - d ) rng [ 0 ] = aroundLoc - d ;
if ( rng [ 0 ] < aroundLoc - e ) rng [ 0 ] = aroundLoc - e ;
if ( rng [ 0 ] < 0 ) rng [ 0 ] = 0 ;
if ( rng [ 1 ] < aroundLoc + d ) rng [ 1 ] = aroundLoc + d ;
if ( rng [ 1 ] > aroundLoc + e ) rng [ 1 ] = aroundLoc + e ;
if ( rng [ 1 ] > rep . lines . totalWidth ( ) ) rng [ 1 ] = rep . lines . totalWidth ( ) ;
return rng ;
}
function findMatchingVisibleBracket ( startLoc , forwards )
{
var rng = getSearchRange ( startLoc ) ;
var str = rep . alltext . substring ( rng [ 0 ] , rng [ 1 ] ) ;
var bstr = str . replace ( bracketRegex , '(' ) ; // handy for searching
var loc = startLoc - rng [ 0 ] ;
var bracketState = [ ] ;
var foundParen = false ;
var goodParen = false ;
function nextLoc ( )
{
if ( loc < 0 ) return ;
if ( forwards ) loc ++ ;
else loc -- ;
if ( loc < 0 || loc >= str . length ) loc = - 1 ;
if ( loc >= 0 )
{
if ( forwards ) loc = bstr . indexOf ( '(' , loc ) ;
else loc = bstr . lastIndexOf ( '(' , loc ) ;
}
}
while ( ( ! foundParen ) && ( loc >= 0 ) )
{
if ( getCharType ( loc + rng [ 0 ] ) == "p" )
{
var b = bracketMap [ str . charAt ( loc ) ] ; // -1, 1, -2, 2, -3, 3
var into = forwards ;
var typ = b ;
if ( typ < 0 )
{
into = ! into ;
typ = - typ ;
}
if ( into ) bracketState . push ( typ ) ;
else
{
var recent = bracketState . pop ( ) ;
if ( recent != typ )
{
foundParen = true ;
goodParen = false ;
}
else if ( bracketState . length == 0 )
{
foundParen = true ;
goodParen = true ;
}
}
}
//console.log(bracketState.toSource());
if ( ( ! foundParen ) && ( loc >= 0 ) ) nextLoc ( ) ;
}
if ( ! foundParen ) return null ;
return {
chr : ( loc + rng [ 0 ] ) ,
good : goodParen
} ;
2011-03-26 13:10:41 +00:00
}
var r = parenFlashRep ;
var charsToHighlight = null ;
var linesToUnhighlight = null ;
2011-07-07 17:59:34 +00:00
if ( r . active && ( docChanged || ( now ( ) > r . expireTime ) ) )
{
linesToUnhighlight = r . whichLineKeys ;
r . active = false ;
}
if ( ( ! r . active ) && docChanged && isCaret ( ) && caretColumn ( ) > 0 )
{
var caret = caretDocChar ( ) ;
if ( caret > 0 && getCharType ( caret - 1 ) == "p" )
{
var charBefore = rep . alltext . charAt ( caret - 1 ) ;
if ( bracketMap [ charBefore ] )
{
var lookForwards = ( bracketMap [ charBefore ] > 0 ) ;
var findResult = findMatchingVisibleBracket ( caret - 1 , lookForwards ) ;
if ( findResult )
{
var mateLoc = findResult . chr ;
var mateGood = findResult . good ;
r . active = true ;
charsToHighlight = { } ;
charsToHighlight [ caret - 1 ] = 'flash' ;
charsToHighlight [ mateLoc ] = ( mateGood ? 'flash' : 'flashbad' ) ;
r . whichLineKeys = [ ] ;
r . whichLineKeys . push ( getLineKeyForOffset ( caret - 1 ) ) ;
r . whichLineKeys . push ( getLineKeyForOffset ( mateLoc ) ) ;
r . expireTime = now ( ) + 4000 ;
newlyActive = true ;
}
}
}
}
if ( linesToUnhighlight )
{
recolorLineByKey ( linesToUnhighlight [ 0 ] ) ;
recolorLineByKey ( linesToUnhighlight [ 1 ] ) ;
}
if ( r . active && charsToHighlight )
{
function f ( txt , cls , next , ofst )
{
var flashClass = charsToHighlight [ ofst ] ;
if ( cls )
{
next ( txt , cls + " " + flashClass ) ;
}
else next ( txt , cls ) ;
}
for ( var c in charsToHighlight )
{
recolorLinesInRange ( ( + c ) , ( + c ) + 1 , null , f ) ;
}
2011-03-26 13:10:41 +00:00
}
}
return module ;
} ) ( ) ;
2011-07-07 17:59:34 +00:00
function dispose ( )
{
2011-03-26 13:10:41 +00:00
disposed = true ;
if ( idleWorkTimer ) idleWorkTimer . never ( ) ;
teardown ( ) ;
}
2011-07-07 17:59:34 +00:00
function checkALines ( )
{
2011-03-26 13:10:41 +00:00
return ; // disable for speed
2011-07-07 17:59:34 +00:00
function error ( )
{
throw new Error ( "checkALines" ) ;
}
if ( rep . alines . length != rep . lines . length ( ) )
{
2011-03-26 13:10:41 +00:00
error ( ) ;
}
2011-07-07 17:59:34 +00:00
for ( var i = 0 ; i < rep . alines . length ; i ++ )
{
2011-03-26 13:10:41 +00:00
var aline = rep . alines [ i ] ;
2011-07-07 17:59:34 +00:00
var lineText = rep . lines . atIndex ( i ) . text + "\n" ;
2011-03-26 13:10:41 +00:00
var lineTextLength = lineText . length ;
var opIter = Changeset . opIterator ( aline ) ;
var alineLength = 0 ;
2011-07-07 17:59:34 +00:00
while ( opIter . hasNext ( ) )
{
var o = opIter . next ( ) ;
alineLength += o . chars ;
if ( opIter . hasNext ( ) )
{
if ( o . lines != 0 ) error ( ) ;
}
else
{
if ( o . lines != 1 ) error ( ) ;
}
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( alineLength != lineTextLength )
{
error ( ) ;
2011-03-26 13:10:41 +00:00
}
}
}
2011-07-07 17:59:34 +00:00
function setWraps ( newVal )
{
2011-03-26 13:10:41 +00:00
doesWrap = newVal ;
var dwClass = "doesWrap" ;
setClassPresence ( root , "doesWrap" , doesWrap ) ;
2011-07-07 17:59:34 +00:00
scheduler . setTimeout ( function ( )
{
inCallStackIfNecessary ( "setWraps" , function ( )
{
fastIncorp ( 7 ) ;
recreateDOM ( ) ;
fixView ( ) ;
2011-03-26 13:10:41 +00:00
} ) ;
} , 0 ) ;
}
2011-07-07 17:59:34 +00:00
function setStyled ( newVal )
{
2011-03-26 13:10:41 +00:00
var oldVal = isStyled ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
}
}
2011-07-07 17:59:34 +00:00
function setTextFace ( face )
{
2011-03-26 13:10:41 +00:00
textFace = face ;
root . style . fontFamily = textFace ;
lineMetricsDiv . style . fontFamily = textFace ;
2011-07-07 17:59:34 +00:00
scheduler . setTimeout ( function ( )
{
2011-03-26 13:10:41 +00:00
setUpTrackingCSS ( ) ;
} , 0 ) ;
}
2011-07-07 17:59:34 +00:00
function setTextSize ( size )
{
2011-03-26 13:10:41 +00:00
textSize = size ;
2011-07-07 17:59:34 +00:00
root . style . fontSize = textSize + "px" ;
root . style . lineHeight = textLineHeight ( ) + "px" ;
sideDiv . style . lineHeight = textLineHeight ( ) + "px" ;
lineMetricsDiv . style . fontSize = textSize + "px" ;
scheduler . setTimeout ( function ( )
{
2011-03-26 13:10:41 +00:00
setUpTrackingCSS ( ) ;
} , 0 ) ;
}
2011-07-07 17:59:34 +00:00
function recreateDOM ( )
{
2011-03-26 13:10:41 +00:00
// precond: normalized
recolorLinesInRange ( 0 , rep . alltext . length ) ;
}
2011-07-07 17:59:34 +00:00
function setEditable ( newVal )
{
2011-03-26 13:10:41 +00:00
isEditable = newVal ;
// the following may fail, e.g. if iframe is hidden
2011-07-07 17:59:34 +00:00
if ( ! isEditable )
{
2011-03-26 13:10:41 +00:00
setDesignMode ( false ) ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
setDesignMode ( true ) ;
}
2011-07-07 17:59:34 +00:00
setClassPresence ( root , "static" , ! isEditable ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function enforceEditability ( )
{
2011-03-26 13:10:41 +00:00
setEditable ( isEditable ) ;
}
2011-07-07 17:59:34 +00:00
function importText ( text , undoable , dontProcess )
{
2011-03-26 13:10:41 +00:00
var lines ;
2011-07-07 17:59:34 +00:00
if ( dontProcess )
{
if ( text . charAt ( text . length - 1 ) != "\n" )
{
throw new Error ( "new raw text must end with newline" ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( /[\r\t\xa0]/ . exec ( text ) )
{
throw new Error ( "new raw text must not contain CR, tab, or nbsp" ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
lines = text . substring ( 0 , text . length - 1 ) . split ( '\n' ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
lines = map ( text . split ( '\n' ) , textify ) ;
}
var newText = "\n" ;
2011-07-07 17:59:34 +00:00
if ( lines . length > 0 )
{
newText = lines . join ( '\n' ) + '\n' ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
inCallStackIfNecessary ( "importText" + ( undoable ? "Undoable" : "" ) , function ( )
{
2011-03-26 13:10:41 +00:00
setDocText ( newText ) ;
} ) ;
2011-07-07 17:59:34 +00:00
if ( dontProcess && rep . alltext != text )
{
2011-03-26 13:10:41 +00:00
throw new Error ( "mismatch error setting raw text in importText" ) ;
}
}
2011-07-07 17:59:34 +00:00
function importAText ( atext , apoolJsonObj , undoable )
{
2011-03-26 13:10:41 +00:00
atext = Changeset . cloneAText ( atext ) ;
2011-07-07 17:59:34 +00:00
if ( apoolJsonObj )
{
2011-03-26 13:10:41 +00:00
var wireApool = ( new AttribPool ( ) ) . fromJsonable ( apoolJsonObj ) ;
atext . attribs = Changeset . moveOpsToNewPool ( atext . attribs , wireApool , rep . apool ) ;
}
2011-07-07 17:59:34 +00:00
inCallStackIfNecessary ( "importText" + ( undoable ? "Undoable" : "" ) , function ( )
{
2011-03-26 13:10:41 +00:00
setDocAText ( atext ) ;
} ) ;
}
2011-07-07 17:59:34 +00:00
function setDocAText ( atext )
{
2011-03-26 13:10:41 +00:00
fastIncorp ( 8 ) ;
var oldLen = rep . lines . totalWidth ( ) ;
var numLines = rep . lines . length ( ) ;
2011-07-07 17:59:34 +00:00
var upToLastLine = rep . lines . offsetOfIndex ( numLines - 1 ) ;
var lastLineLength = rep . lines . atIndex ( numLines - 1 ) . text . length ;
2011-03-26 13:10:41 +00:00
var assem = Changeset . smartOpAssembler ( ) ;
var o = Changeset . newOp ( '-' ) ;
o . chars = upToLastLine ;
2011-07-07 17:59:34 +00:00
o . lines = numLines - 1 ;
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
Changeset . pack ( oldLen , newLen , assem . toString ( ) , atext . text . slice ( 0 , - 1 ) ) ) ;
2011-03-26 13:10:41 +00:00
performDocumentApplyChangeset ( changeset ) ;
2011-07-07 17:59:34 +00:00
performSelectionChange ( [ 0 , rep . lines . atIndex ( 0 ) . lineMarker ] , [ 0 , rep . lines . atIndex ( 0 ) . lineMarker ] ) ;
2011-03-26 13:10:41 +00:00
idleWorkTimer . atMost ( 100 ) ;
2011-07-07 17:59:34 +00:00
if ( rep . alltext != atext . text )
{
2011-03-26 13:10:41 +00:00
dmesg ( htmlPrettyEscape ( rep . alltext ) ) ;
dmesg ( htmlPrettyEscape ( atext . text ) ) ;
throw new Error ( "mismatch error setting raw text in setDocAText" ) ;
}
}
2011-07-07 17:59:34 +00:00
function setDocText ( text )
{
2011-03-26 13:10:41 +00:00
setDocAText ( Changeset . makeAText ( text ) ) ;
}
2011-07-07 17:59:34 +00:00
function getDocText ( )
{
2011-03-26 13:10:41 +00:00
var alltext = rep . alltext ;
var len = alltext . length ;
if ( len > 0 ) len -- ; // final extra newline
return alltext . substring ( 0 , len ) ;
}
2011-07-07 17:59:34 +00:00
function exportText ( )
{
if ( currentCallStack && ! currentCallStack . domClean )
{
inCallStackIfNecessary ( "exportText" , function ( )
{
fastIncorp ( 2 ) ;
} ) ;
2011-03-26 13:10:41 +00:00
}
return getDocText ( ) ;
}
2011-07-07 17:59:34 +00:00
function editorChangedSize ( )
{
2011-03-26 13:10:41 +00:00
fixView ( ) ;
}
2011-07-07 17:59:34 +00:00
function setOnKeyPress ( handler )
{
2011-03-26 13:10:41 +00:00
outsideKeyPress = handler ;
}
2011-07-07 17:59:34 +00:00
function setOnKeyDown ( handler )
{
2011-03-26 13:10:41 +00:00
outsideKeyDown = handler ;
}
2011-07-07 17:59:34 +00:00
function setNotifyDirty ( handler )
{
2011-03-26 13:10:41 +00:00
outsideNotifyDirty = handler ;
}
2011-07-07 17:59:34 +00:00
function getFormattedCode ( )
{
if ( currentCallStack && ! currentCallStack . domClean )
{
2011-03-26 13:10:41 +00:00
inCallStackIfNecessary ( "getFormattedCode" , incorporateUserChanges ) ;
}
var buf = [ ] ;
2011-07-07 17:59:34 +00:00
if ( rep . lines . length ( ) > 0 )
{
2011-03-26 13:10:41 +00:00
// should be the case, even for empty file
var entry = rep . lines . atIndex ( 0 ) ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
return '<div class="syntax"><div>' + buf . join ( '</div>\n<div>' ) + '</div></div>' ;
2011-03-26 13:10:41 +00:00
}
var CMDS = {
2011-07-07 17:59:34 +00:00
clearauthorship : function ( prompt )
{
if ( ( ! ( rep . selStart && rep . selEnd ) ) || isCaret ( ) )
{
if ( prompt )
{
2011-03-26 13:10:41 +00:00
prompt ( ) ;
}
2011-07-07 17:59:34 +00:00
else
{
performDocumentApplyAttributesToCharRange ( 0 , rep . alltext . length , [
[ 'author' , '' ]
] ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
setAttributeOnSelection ( 'author' , '' ) ;
}
2011-03-27 10:46:45 +00:00
}
2011-03-26 13:10:41 +00:00
} ;
2011-07-07 17:59:34 +00:00
function execCommand ( cmd )
{
2011-03-26 13:10:41 +00:00
cmd = cmd . toLowerCase ( ) ;
var cmdArgs = Array . prototype . slice . call ( arguments , 1 ) ;
2011-07-07 17:59:34 +00:00
if ( CMDS [ cmd ] )
{
inCallStack ( cmd , function ( )
{
fastIncorp ( 9 ) ;
CMDS [ cmd ] . apply ( CMDS , cmdArgs ) ;
2011-03-26 13:10:41 +00:00
} ) ;
}
}
2011-07-07 17:59:34 +00:00
function replaceRange ( start , end , text )
{
inCallStack ( 'replaceRange' , function ( )
{
2011-03-26 13:10:41 +00: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 ;
2011-07-07 17:59:34 +00:00
editorInfo . ace _callWithAce = function ( fn , callStack , normalize )
{
var wrapper = function ( )
{
return fn ( editorInfo ) ;
}
if ( normalize !== undefined )
{
2011-03-26 13:10:41 +00:00
var wrapper1 = wrapper ;
2011-07-07 17:59:34 +00:00
wrapper = function ( )
{
2011-03-26 13:10:41 +00:00
editorInfo . ace _fastIncorp ( 9 ) ;
2011-07-07 17:59:34 +00:00
wrapper1 ( ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
if ( callStack !== undefined )
{
2011-03-26 13:10:41 +00:00
return editorInfo . ace _inCallStack ( callStack , wrapper ) ;
2011-07-07 17:59:34 +00:00
}
else
{
2011-03-26 13:10:41 +00:00
return wrapper ( ) ;
}
}
2011-07-07 17:59:34 +00:00
editorInfo . ace _setProperty = function ( key , value )
{
2011-03-26 13:10:41 +00:00
var k = key . toLowerCase ( ) ;
2011-07-07 17:59:34 +00:00
if ( k == "wraps" )
{
2011-03-26 13:10:41 +00:00
setWraps ( value ) ;
}
2011-07-07 17:59:34 +00:00
else if ( k == "showsauthorcolors" )
{
setClassPresence ( root , "authorColors" , ! ! value ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else if ( k == "showsuserselections" )
{
setClassPresence ( root , "userSelections" , ! ! value ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else if ( k == "showslinenumbers" )
{
hasLineNumbers = ! ! value ;
2011-12-31 16:46:10 +00:00
// disable line numbers on mobile devices
2012-01-21 23:13:00 +00:00
if ( browser . mobile ) hasLineNumbers = false ;
2011-07-07 17:59:34 +00:00
setClassPresence ( sideDiv , "sidedivhidden" , ! hasLineNumbers ) ;
2011-03-26 13:10:41 +00:00
fixView ( ) ;
}
2011-07-07 17:59:34 +00:00
else if ( k == "grayedout" )
{
setClassPresence ( outerWin . document . body , "grayedout" , ! ! value ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else if ( k == "dmesg" )
{
2011-03-26 13:10:41 +00:00
dmesg = value ;
window . dmesg = value ;
}
2011-07-07 17:59:34 +00:00
else if ( k == 'userauthor' )
{
2011-03-26 13:10:41 +00:00
thisAuthor = String ( value ) ;
}
2011-07-07 17:59:34 +00:00
else if ( k == 'styled' )
{
2011-03-26 13:10:41 +00:00
setStyled ( value ) ;
}
2011-07-07 17:59:34 +00:00
else if ( k == 'textface' )
{
2011-03-26 13:10:41 +00:00
setTextFace ( value ) ;
}
2011-07-07 17:59:34 +00:00
else if ( k == 'textsize' )
{
2011-03-26 13:10:41 +00:00
setTextSize ( value ) ;
}
2011-12-04 18:55:35 +00:00
else if ( k == 'rtlistrue' )
{
setClassPresence ( root , "rtl" , ! ! value ) ;
}
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
editorInfo . ace _setBaseText = function ( txt )
{
2011-03-26 13:10:41 +00:00
changesetTracker . setBaseText ( txt ) ;
} ;
2011-07-07 17:59:34 +00:00
editorInfo . ace _setBaseAttributedText = function ( atxt , apoolJsonObj )
{
2011-03-26 13:10:41 +00:00
setUpTrackingCSS ( ) ;
changesetTracker . setBaseAttributedText ( atxt , apoolJsonObj ) ;
} ;
2011-07-07 17:59:34 +00:00
editorInfo . ace _applyChangesToBase = function ( c , optAuthor , apoolJsonObj )
{
2011-03-26 13:10:41 +00:00
changesetTracker . applyChangesToBase ( c , optAuthor , apoolJsonObj ) ;
} ;
2011-07-07 17:59:34 +00:00
editorInfo . ace _prepareUserChangeset = function ( )
{
2011-03-26 13:10:41 +00:00
return changesetTracker . prepareUserChangeset ( ) ;
} ;
2011-07-07 17:59:34 +00:00
editorInfo . ace _applyPreparedChangesetToBase = function ( )
{
2011-03-26 13:10:41 +00:00
changesetTracker . applyPreparedChangesetToBase ( ) ;
} ;
2011-07-07 17:59:34 +00:00
editorInfo . ace _setUserChangeNotificationCallback = function ( f )
{
2011-03-26 13:10:41 +00:00
changesetTracker . setUserChangeNotificationCallback ( f ) ;
} ;
2011-07-07 17:59:34 +00:00
editorInfo . ace _setAuthorInfo = function ( author , info )
{
2011-03-26 13:10:41 +00:00
setAuthorInfo ( author , info ) ;
} ;
2011-07-07 17:59:34 +00:00
editorInfo . ace _setAuthorSelectionRange = function ( author , start , end )
{
2011-03-26 13:10:41 +00:00
changesetTracker . setAuthorSelectionRange ( author , start , end ) ;
} ;
2011-07-07 17:59:34 +00:00
editorInfo . ace _getUnhandledErrors = function ( )
{
2011-03-26 13:10:41 +00:00
return caughtErrors . slice ( ) ;
} ;
2011-07-07 17:59:34 +00:00
editorInfo . ace _getDebugProperty = function ( prop )
{
if ( prop == "debugger" )
{
2011-03-26 13:10:41 +00:00
// obfuscate "eval" so as not to scare yuicompressor
2011-07-07 17:59:34 +00:00
window [ 'ev' + 'al' ] ( "debugger" ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else if ( prop == "rep" )
{
2011-03-26 13:10:41 +00:00
return rep ;
}
2011-07-07 17:59:34 +00:00
else if ( prop == "window" )
{
2011-03-26 13:10:41 +00:00
return window ;
}
2011-07-07 17:59:34 +00:00
else if ( prop == "document" )
{
2011-03-26 13:10:41 +00:00
return document ;
}
return undefined ;
} ;
2011-07-07 17:59:34 +00:00
function now ( )
{
return ( new Date ( ) ) . getTime ( ) ;
}
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
function newTimeLimit ( ms )
{
2011-03-26 13:10:41 +00:00
//console.debug("new time limit");
var startTime = now ( ) ;
var lastElapsed = 0 ;
var exceededAlready = false ;
var printedTrace = false ;
2011-07-07 17:59:34 +00:00
var isTimeUp = function ( )
{
if ( exceededAlready )
{
if ( ( ! printedTrace ) )
{ // && now() - startTime - ms > 300) {
//console.trace();
printedTrace = true ;
}
return true ;
}
var elapsed = now ( ) - startTime ;
if ( elapsed > ms )
{
exceededAlready = true ;
//console.debug("time limit hit, before was %d/%d", lastElapsed, ms);
//console.trace();
return true ;
}
else
{
lastElapsed = elapsed ;
return false ;
}
}
isTimeUp . elapsed = function ( )
{
return now ( ) - startTime ;
}
2011-03-26 13:10:41 +00:00
return isTimeUp ;
}
2011-07-07 17:59:34 +00:00
function makeIdleAction ( func )
{
2011-03-26 13:10:41 +00:00
var scheduledTimeout = null ;
var scheduledTime = 0 ;
2011-07-07 17:59:34 +00:00
function unschedule ( )
{
if ( scheduledTimeout )
{
scheduler . clearTimeout ( scheduledTimeout ) ;
scheduledTimeout = null ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
function reschedule ( time )
{
2011-03-26 13:10:41 +00:00
unschedule ( ) ;
scheduledTime = time ;
var delay = time - now ( ) ;
if ( delay < 0 ) delay = 0 ;
scheduledTimeout = scheduler . setTimeout ( callback , delay ) ;
}
2011-07-07 17:59:34 +00:00
function callback ( )
{
2011-03-26 13:10:41 +00:00
scheduledTimeout = null ;
// func may reschedule the action
func ( ) ;
}
return {
2011-07-07 17:59:34 +00:00
atMost : function ( ms )
{
var latestTime = now ( ) + ms ;
if ( ( ! scheduledTimeout ) || scheduledTime > latestTime )
{
reschedule ( latestTime ) ;
}
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
atLeast : function ( ms )
{
var earliestTime = now ( ) + ms ;
if ( ( ! scheduledTimeout ) || scheduledTime < earliestTime )
{
reschedule ( earliestTime ) ;
}
2011-03-26 13:10:41 +00:00
} ,
2011-07-07 17:59:34 +00:00
never : function ( )
{
unschedule ( ) ;
2011-03-26 13:10:41 +00:00
}
}
}
2011-07-07 17:59:34 +00:00
function fastIncorp ( n )
{
2011-03-26 13:10:41 +00:00
// normalize but don't do any lexing or anything
incorporateUserChanges ( newTimeLimit ( 0 ) ) ;
}
editorInfo . ace _fastIncorp = fastIncorp ;
2011-07-07 17:59:34 +00:00
function incorpIfQuick ( )
{
2011-03-26 13:10:41 +00:00
var me = incorpIfQuick ;
var failures = ( me . failures || 0 ) ;
2011-07-07 17:59:34 +00:00
if ( failures < 5 )
{
2011-03-26 13:10:41 +00:00
var isTimeUp = newTimeLimit ( 40 ) ;
var madeChanges = incorporateUserChanges ( isTimeUp ) ;
2011-07-07 17:59:34 +00:00
if ( isTimeUp ( ) )
{
me . failures = failures + 1 ;
2011-03-26 13:10:41 +00:00
}
return true ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
var skipCount = ( me . skipCount || 0 ) ;
skipCount ++ ;
2011-07-07 17:59:34 +00:00
if ( skipCount == 20 )
{
skipCount = 0 ;
me . failures = 0 ;
2011-03-26 13:10:41 +00:00
}
me . skipCount = skipCount ;
}
return false ;
}
2011-07-07 17:59:34 +00:00
var idleWorkTimer = makeIdleAction ( function ( )
{
2011-03-26 13:10:41 +00:00
//if (! top.BEFORE) top.BEFORE = [];
//top.BEFORE.push(magicdom.root.dom.innerHTML);
//if (! isEditable) return; // and don't reschedule
2011-07-07 17:59:34 +00:00
if ( inInternationalComposition )
{
2011-03-26 13:10:41 +00:00
// don't do idle input incorporation during international input composition
idleWorkTimer . atLeast ( 500 ) ;
return ;
}
2011-07-07 17:59:34 +00:00
inCallStack ( "idleWorkTimer" , function ( )
{
2011-03-26 13:10:41 +00:00
var isTimeUp = newTimeLimit ( 250 ) ;
//console.time("idlework");
var finishedImportantWork = false ;
var finishedWork = false ;
2011-07-07 17:59:34 +00:00
try
{
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
if ( isTimeUp ( ) ) return ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
updateLineNumbers ( ) ; // update line numbers if any time left
if ( isTimeUp ( ) ) return ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
var visibleRange = getVisibleCharRange ( ) ;
var docRange = [ 0 , rep . lines . totalWidth ( ) ] ;
//console.log("%o %o", docRange, visibleRange);
finishedImportantWork = true ;
finishedWork = true ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
finally
{
//console.timeEnd("idlework");
if ( finishedWork )
{
idleWorkTimer . atMost ( 1000 ) ;
}
else if ( finishedImportantWork )
{
// if we've finished highlighting the view area,
// more highlighting could be counter-productive,
// e.g. if the user just opened a triple-quote and will soon close it.
idleWorkTimer . atMost ( 500 ) ;
}
else
{
var timeToWait = Math . round ( isTimeUp . elapsed ( ) / 2 ) ;
if ( timeToWait < 100 ) timeToWait = 100 ;
idleWorkTimer . atMost ( timeToWait ) ;
}
2011-03-26 13:10:41 +00:00
}
} ) ;
//if (! top.AFTER) top.AFTER = [];
//top.AFTER.push(magicdom.root.dom.innerHTML);
} ) ;
var _nextId = 1 ;
2011-07-07 17:59:34 +00:00
function uniqueId ( n )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
return ( n . id = "magicdomid" + ( _nextId ++ ) ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function recolorLinesInRange ( startChar , endChar , isTimeUp , optModFunc )
{
2011-03-26 13:10:41 +00:00
if ( endChar <= startChar ) return ;
if ( startChar < 0 || startChar >= rep . lines . totalWidth ( ) ) return ;
2011-07-07 17:59:34 +00:00
var lineEntry = rep . lines . atOffset ( startChar ) ; // rounds down to line boundary
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
var tokenFunc = function ( tokenText , tokenClass )
{
lineEntry . domInfo . appendSpan ( tokenText , tokenClass ) ;
} ;
if ( optModFunc )
{
2011-03-26 13:10:41 +00:00
var f = tokenFunc ;
2011-07-07 17:59:34 +00:00
tokenFunc = function ( tokenText , tokenClass )
{
optModFunc ( tokenText , tokenClass , f , curDocChar ) ;
curDocChar += tokenText . length ;
2011-03-26 13:10:41 +00:00
} ;
}
2011-07-07 17:59:34 +00:00
while ( lineEntry && lineStart < endChar && ! isTimeUp ( ) )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
if ( rep . selStart && rep . selStart [ 0 ] == lineIndex || rep . selEnd && rep . selEnd [ 0 ] == lineIndex )
{
selectionNeedsResetting = true ;
2011-03-26 13:10:41 +00:00
}
//if (timer()) console.dirxml(lineEntry.lineNode.dom);
if ( firstLine === null ) firstLine = lineIndex ;
lastLine = lineIndex ;
lineStart = lineEnd ;
lineEntry = rep . lines . next ( lineEntry ) ;
lineIndex ++ ;
}
2011-07-07 17:59:34 +00:00
if ( selectionNeedsResetting )
{
2011-03-26 13:10:41 +00:00
currentCallStack . selectionAffected = true ;
}
//console.debug("Recolored line range %d-%d", firstLine, lastLine);
}
// like getSpansForRange, but for a line, and the func takes (text,class)
// instead of (width,class); excludes the trailing '\n' from
// consideration by func
2011-07-07 17:59:34 +00:00
function getSpansForLine ( lineEntry , textAndClassFunc , lineEntryOffsetHint )
{
2011-03-26 13:10:41 +00:00
var lineEntryOffset = lineEntryOffsetHint ;
2011-07-07 17:59:34 +00:00
if ( ( typeof lineEntryOffset ) != "number" )
{
2011-03-26 13:10:41 +00:00
lineEntryOffset = rep . lines . offsetOfEntry ( lineEntry ) ;
}
var text = lineEntry . text ;
var width = lineEntry . width ; // text.length+1
2011-07-07 17:59:34 +00:00
if ( text . length == 0 )
{
2011-03-26 13:10:41 +00:00
// allow getLineStyleFilter to set line-div styles
var func = linestylefilter . getLineStyleFilter (
2011-07-07 17:59:34 +00:00
0 , '' , textAndClassFunc , rep . apool ) ;
2011-03-26 13:10:41 +00:00
func ( '' , '' ) ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
var offsetIntoLine = 0 ;
var filteredFunc = linestylefilter . getFilterStack ( text , textAndClassFunc , browser ) ;
var lineNum = rep . lines . indexOfEntry ( lineEntry ) ;
var aline = rep . alines [ lineNum ] ;
filteredFunc = linestylefilter . getLineStyleFilter (
2011-07-07 17:59:34 +00:00
text . length , aline , filteredFunc , rep . apool ) ;
2011-03-26 13:10:41 +00:00
filteredFunc ( text , '' ) ;
}
}
2011-07-07 17:59:34 +00:00
function getCharType ( charIndex )
{
2011-03-26 13:10:41 +00:00
return '' ;
}
var observedChanges ;
2011-07-07 17:59:34 +00:00
function clearObservedChanges ( )
{
observedChanges = {
cleanNodesNearChanges : { }
} ;
2011-03-26 13:10:41 +00:00
}
clearObservedChanges ( ) ;
2011-07-07 17:59:34 +00:00
function getCleanNodeByKey ( key )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
while ( n && isNodeDirty ( n ) )
{
2011-03-26 13:10:41 +00:00
p . extra ++ ;
n . id = "" ;
n = doc . getElementById ( key ) ;
}
p . literal ( p . extra , "extra" ) ;
p . end ( ) ;
return n ;
}
2011-07-07 17:59:34 +00:00
function observeChangesAroundNode ( node )
{
2011-03-26 13:10:41 +00: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).
//top.console.log("observeChangesAroundNode(%o)", node);
var cleanNode ;
var hasAdjacentDirtyness ;
2011-07-07 17:59:34 +00:00
if ( ! isNodeDirty ( node ) )
{
2011-03-26 13:10:41 +00:00
cleanNode = node ;
var prevSib = cleanNode . previousSibling ;
var nextSib = cleanNode . nextSibling ;
2011-07-07 17:59:34 +00:00
hasAdjacentDirtyness = ( ( prevSib && isNodeDirty ( prevSib ) ) || ( nextSib && isNodeDirty ( nextSib ) ) ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
// node is dirty, look for clean node above
var upNode = node . previousSibling ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
hasAdjacentDirtyness = true ;
}
2011-07-07 17:59:34 +00:00
if ( hasAdjacentDirtyness )
{
2011-03-26 13:10:41 +00:00
// previous or next line is dirty
2011-07-07 17:59:34 +00:00
observedChanges . cleanNodesNearChanges [ '$' + uniqueId ( cleanNode ) ] = true ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
if ( actualPrevKey != repPrevKey || actualNextKey != repNextKey )
{
observedChanges . cleanNodesNearChanges [ '$' + uniqueId ( cleanNode ) ] = true ;
2011-03-26 13:10:41 +00:00
}
}
}
2011-07-07 17:59:34 +00:00
function observeChangesAroundSelection ( )
{
2011-03-26 13:10:41 +00:00
if ( currentCallStack . observedSelection ) return ;
currentCallStack . observedSelection = true ;
var p = PROFILER ( "getSelection" , false ) ;
var selection = getSelection ( ) ;
p . end ( ) ;
2011-07-07 17:59:34 +00:00
if ( selection )
{
function topLevel ( n )
{
if ( ( ! n ) || n == root ) return null ;
while ( n . parentNode != root )
{
n = n . parentNode ;
}
return n ;
2011-03-26 13:10:41 +00:00
}
var node1 = topLevel ( selection . startPoint . node ) ;
var node2 = topLevel ( selection . endPoint . node ) ;
if ( node1 ) observeChangesAroundNode ( node1 ) ;
2011-07-07 17:59:34 +00:00
if ( node2 && node1 != node2 )
{
observeChangesAroundNode ( node2 ) ;
2011-03-26 13:10:41 +00:00
}
}
}
2011-07-07 17:59:34 +00:00
function observeSuspiciousNodes ( )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
if ( root . getElementsByTagName )
{
2011-03-26 13:10:41 +00:00
var nds = root . getElementsByTagName ( "style" ) ;
2011-07-07 17:59:34 +00:00
for ( var i = 0 ; i < nds . length ; i ++ )
{
var n = nds [ i ] ;
while ( n . parentNode && n . parentNode != root )
{
n = n . parentNode ;
}
if ( n . parentNode == root )
{
observeChangesAroundNode ( n ) ;
}
2011-03-26 13:10:41 +00:00
}
}
}
2011-07-07 17:59:34 +00:00
function incorporateUserChanges ( isTimeUp )
{
2011-03-26 13:10:41 +00:00
if ( currentCallStack . domClean ) return false ;
inInternationalComposition = false ; // if we need the document normalized, so be it
currentCallStack . isUserChange = true ;
2011-07-07 17:59:34 +00:00
isTimeUp = ( isTimeUp ||
function ( )
{
return false ;
} ) ;
2011-03-26 13:10:41 +00:00
2011-03-27 10:46:45 +00:00
if ( DEBUG && window . DONT _INCORP || window . DEBUG _DONT _INCORP ) return false ;
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
if ( ! root . firstChild )
{
2011-03-26 13:10:41 +00:00
root . innerHTML = "<div><!-- --></div>" ;
}
p . mark ( "obs" ) ;
observeChangesAroundSelection ( ) ;
observeSuspiciousNodes ( ) ;
p . mark ( "dirty" ) ;
var dirtyRanges = getDirtyRanges ( ) ;
//console.log("dirtyRanges: "+toSource(dirtyRanges));
var dirtyRangesCheckOut = true ;
var j = 0 ;
2011-07-07 17:59:34 +00:00
var a , b ;
while ( j < dirtyRanges . length )
{
2011-03-26 13:10:41 +00:00
a = dirtyRanges [ j ] [ 0 ] ;
b = dirtyRanges [ j ] [ 1 ] ;
2011-07-07 17:59:34 +00:00
if ( ! ( ( a == 0 || getCleanNodeByKey ( rep . lines . atIndex ( a - 1 ) . key ) ) && ( b == rep . lines . length ( ) || getCleanNodeByKey ( rep . lines . atIndex ( b ) . key ) ) ) )
{
2011-03-26 13:10:41 +00:00
dirtyRangesCheckOut = false ;
break ;
}
j ++ ;
}
2011-07-07 17:59:34 +00:00
if ( ! dirtyRangesCheckOut )
{
2011-03-26 13:10:41 +00:00
var numBodyNodes = root . childNodes . length ;
2011-07-07 17:59:34 +00:00
for ( var k = 0 ; k < numBodyNodes ; k ++ )
{
2011-03-26 13:10:41 +00:00
var bodyNode = root . childNodes . item ( k ) ;
2011-07-07 17:59:34 +00:00
if ( ( bodyNode . tagName ) && ( ( ! bodyNode . id ) || ( ! rep . lines . containsKey ( bodyNode . id ) ) ) )
{
2011-03-26 13:10:41 +00:00
observeChangesAroundNode ( bodyNode ) ;
}
}
dirtyRanges = getDirtyRanges ( ) ;
}
clearObservedChanges ( ) ;
p . mark ( "getsel" ) ;
var selection = getSelection ( ) ;
//console.log(magicdom.root.dom.innerHTML);
//console.log("got selection: %o", selection);
var selStart , selEnd ; // each one, if truthy, has [line,char] needed to set selection
var i = 0 ;
var splicesToDo = [ ] ;
var netNumLinesChangeSoFar = 0 ;
var toDeleteAtEnd = [ ] ;
p . mark ( "ranges" ) ;
p . literal ( dirtyRanges . length , "numdirt" ) ;
var domInsertsNeeded = [ ] ; // each entry is [nodeToInsertAfter, [info1, info2, ...]]
2011-07-07 17:59:34 +00:00
while ( i < dirtyRanges . length )
{
2011-03-26 13:10:41 +00:00
var range = dirtyRanges [ i ] ;
a = range [ 0 ] ;
b = range [ 1 ] ;
2011-07-07 17:59:34 +00:00
var firstDirtyNode = ( ( ( a == 0 ) && root . firstChild ) || getCleanNodeByKey ( rep . lines . atIndex ( a - 1 ) . key ) . nextSibling ) ;
2011-03-26 13:10:41 +00:00
firstDirtyNode = ( firstDirtyNode && isNodeDirty ( firstDirtyNode ) && firstDirtyNode ) ;
2011-07-07 17:59:34 +00:00
var lastDirtyNode = ( ( ( b == rep . lines . length ( ) ) && root . lastChild ) || getCleanNodeByKey ( rep . lines . atIndex ( b ) . key ) . previousSibling ) ;
2011-03-26 13:10:41 +00:00
lastDirtyNode = ( lastDirtyNode && isNodeDirty ( lastDirtyNode ) && lastDirtyNode ) ;
2011-07-07 17:59:34 +00:00
if ( firstDirtyNode && lastDirtyNode )
{
var cc = makeContentCollector ( isStyled , browser , rep . apool , null , className2Author ) ;
cc . notifySelection ( selection ) ;
var dirtyNodes = [ ] ;
for ( var n = firstDirtyNode ; n && ! ( n . previousSibling && n . previousSibling == lastDirtyNode ) ;
n = n . nextSibling )
{
if ( browser . msie )
{
2011-03-26 13:10:41 +00:00
// try to undo IE's pesky and overzealous linkification
2011-07-07 17:59:34 +00:00
try
{
n . createTextRange ( ) . execCommand ( "unlink" , false , null ) ;
}
catch ( e )
{ }
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00: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.
//console.log("inclusive of "+lastDirtyNode.next().dom.tagName);
b ++ ;
var cleanLine = lastDirtyNode . nextSibling ;
cc . collectContent ( cleanLine ) ;
toDeleteAtEnd . push ( cleanLine ) ;
cc . notifyNextNode ( cleanLine . nextSibling ) ;
}
2011-03-26 13:10:41 +00:00
var ccData = cc . finish ( ) ;
var ss = ccData . selStart ;
var se = ccData . selEnd ;
lines = ccData . lines ;
var lineAttribs = ccData . lineAttribs ;
var linesWrapped = ccData . linesWrapped ;
2011-07-07 17:59:34 +00:00
if ( linesWrapped > 0 )
{
doAlert ( "Editor warning: " + linesWrapped + " long line" + ( linesWrapped == 1 ? " was" : "s were" ) + " hard-wrapped into " + ccData . numLinesAfter + " lines." ) ;
}
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
var entries = [ ] ;
var nodeToAddAfter = lastDirtyNode ;
var lineNodeInfos = new Array ( lines . length ) ;
for ( var k = 0 ; k < lines . length ; k ++ )
{
2011-03-26 13:10:41 +00:00
var lineString = lines [ k ] ;
2011-07-07 17:59:34 +00:00
var newEntry = createDomLineEntry ( lineString ) ;
entries . push ( newEntry ) ;
lineNodeInfos [ k ] = newEntry . domInfo ;
}
//var fragment = magicdom.wrapDom(document.createDocumentFragment());
domInsertsNeeded . push ( [ nodeToAddAfter , lineNodeInfos ] ) ;
forEach ( dirtyNodes , function ( n )
{
toDeleteAtEnd . push ( n ) ;
} ) ;
var spliceHints = { } ;
if ( selStart ) spliceHints . selStart = selStart ;
if ( selEnd ) spliceHints . selEnd = selEnd ;
splicesToDo . push ( [ a + netNumLinesChangeSoFar , b - a , entries , lineAttribs , spliceHints ] ) ;
netNumLinesChangeSoFar += ( lines . length - ( b - a ) ) ;
}
else if ( b > a )
{
splicesToDo . push ( [ a + netNumLinesChangeSoFar , b - a , [ ] ,
[ ]
] ) ;
2011-03-26 13:10:41 +00:00
}
i ++ ;
}
var domChanges = ( splicesToDo . length > 0 ) ;
// update the representation
p . mark ( "splice" ) ;
2011-07-07 17:59:34 +00:00
forEach ( splicesToDo , function ( splice )
{
2011-03-26 13:10:41 +00:00
doIncorpLineSplice ( splice [ 0 ] , splice [ 1 ] , splice [ 2 ] , splice [ 3 ] , splice [ 4 ] ) ;
} ) ;
//p.mark("relex");
//rep.lexer.lexCharRange(getVisibleCharRange(), function() { return false; });
//var isTimeUp = newTimeLimit(100);
// do DOM inserts
p . mark ( "insert" ) ;
2011-07-07 17:59:34 +00:00
forEach ( domInsertsNeeded , function ( ins )
{
2011-03-26 13:10:41 +00:00
insertDomLines ( ins [ 0 ] , ins [ 1 ] , isTimeUp ) ;
} ) ;
p . mark ( "del" ) ;
// delete old dom nodes
2011-07-07 17:59:34 +00:00
forEach ( toDeleteAtEnd , function ( n )
{
2011-03-26 13:10:41 +00:00
//var id = n.uniqueId();
// parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf)
n . parentNode . removeChild ( n ) ;
//dmesg(htmlPrettyEscape(htmlForRemovedChild(n)));
//console.log("removed: "+id);
} ) ;
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 17:59:34 +00:00
if ( selection && ! selStart )
{
2011-03-26 13:10:41 +00:00
//if (domChanges) dmesg("selection not collected");
selStart = getLineAndCharForPoint ( selection . startPoint ) ;
}
2011-07-07 17:59:34 +00:00
if ( selection && ! selEnd )
{
2011-03-26 13:10:41 +00:00
selEnd = getLineAndCharForPoint ( selection . endPoint ) ;
}
// 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 17:59:34 +00:00
if ( selStart && selStart [ 0 ] >= numLines )
{
selStart [ 0 ] = numLines - 1 ;
2011-03-26 13:10:41 +00:00
selStart [ 1 ] = rep . lines . atIndex ( selStart [ 0 ] ) . text . length ;
}
2011-07-07 17:59:34 +00:00
if ( selEnd && selEnd [ 0 ] >= numLines )
{
selEnd [ 0 ] = numLines - 1 ;
2011-03-26 13:10:41 +00: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
// idea.
2011-07-07 17:59:34 +00:00
if ( selection ) repSelectionChange ( selStart , selEnd , selection && selection . focusAtStart ) ;
2011-03-26 13:10:41 +00:00
// update browser selection
p . mark ( "browsel" ) ;
2011-07-07 17:59:34 +00:00
if ( selection && ( domChanges || isCaret ( ) ) )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function htmlForRemovedChild ( n )
{
2011-03-26 13:10:41 +00:00
var div = doc . createElement ( "DIV" ) ;
div . appendChild ( n ) ;
return div . innerHTML ;
}
2011-07-07 17:59:34 +00:00
var STYLE _ATTRIBS = {
bold : true ,
italic : true ,
underline : true ,
strikethrough : true ,
list : true
} ;
var OTHER _INCORPED _ATTRIBS = {
insertorder : true ,
author : true
} ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
function isStyleAttribute ( aname )
{
return ! ! STYLE _ATTRIBS [ aname ] ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function isIncorpedAttribute ( aname )
{
return ( ! ! STYLE _ATTRIBS [ aname ] ) || ( ! ! OTHER _INCORPED _ATTRIBS [ aname ] ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function insertDomLines ( nodeToAddAfter , infoStructs , isTimeUp )
{
isTimeUp = ( isTimeUp ||
function ( )
{
return false ;
} ) ;
2011-03-26 13:10:41 +00:00
var lastEntry ;
var lineStartOffset ;
if ( infoStructs . length < 1 ) return ;
var startEntry = rep . lines . atKey ( uniqueId ( infoStructs [ 0 ] . node ) ) ;
2011-07-07 17:59:34 +00:00
var endEntry = rep . lines . atKey ( uniqueId ( infoStructs [ infoStructs . length - 1 ] . node ) ) ;
2011-03-26 13:10:41 +00:00
var charStart = rep . lines . offsetOfEntry ( startEntry ) ;
var charEnd = rep . lines . offsetOfEntry ( endEntry ) + endEntry . width ;
//rep.lexer.lexCharRange([charStart, charEnd], isTimeUp);
2011-07-07 17:59:34 +00:00
forEach ( infoStructs , function ( info )
{
2011-03-26 13:10:41 +00:00
var p2 = PROFILER ( "insertLine" , false ) ;
var node = info . node ;
var key = uniqueId ( node ) ;
var entry ;
p2 . mark ( "findEntry" ) ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
else p2 . literal ( 0 , "nonopt" ) ;
lastEntry = entry ;
p2 . mark ( "spans" ) ;
2011-07-07 17:59:34 +00:00
getSpansForLine ( entry , function ( tokenText , tokenClass )
{
info . appendSpan ( tokenText , tokenClass ) ;
2011-03-26 13:10:41 +00:00
} , lineStartOffset , isTimeUp ( ) ) ;
//else if (entry.text.length > 0) {
2011-07-07 17:59:34 +00:00
//info.appendSpan(entry.text, 'dirty');
2011-03-26 13:10:41 +00:00
//}
p2 . mark ( "addLine" ) ;
info . prepareForAdd ( ) ;
entry . lineMarker = info . lineMarker ;
2011-07-07 17:59:34 +00:00
if ( ! nodeToAddAfter )
{
root . insertBefore ( node , root . firstChild ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
root . insertBefore ( node , nodeToAddAfter . nextSibling ) ;
2011-03-26 13:10:41 +00:00
}
nodeToAddAfter = node ;
info . notifyAdded ( ) ;
p2 . mark ( "markClean" ) ;
markNodeClean ( node ) ;
p2 . end ( ) ;
} ) ;
}
2011-07-07 17:59:34 +00:00
function isCaret ( )
{
return ( rep . selStart && rep . selEnd && rep . selStart [ 0 ] == rep . selEnd [ 0 ] && rep . selStart [ 1 ] == rep . selEnd [ 1 ] ) ;
2011-03-26 13:10:41 +00:00
}
editorInfo . ace _isCaret = isCaret ;
// prereq: isCaret()
2011-07-07 17:59:34 +00:00
function caretLine ( )
{
return rep . selStart [ 0 ] ;
}
function caretColumn ( )
{
return rep . selStart [ 1 ] ;
}
function caretDocChar ( )
{
2011-03-26 13:10:41 +00:00
return rep . lines . offsetOfIndex ( caretLine ( ) ) + caretColumn ( ) ;
}
2011-07-07 17:59:34 +00:00
function handleReturnIndentation ( )
{
2011-03-26 13:10:41 +00:00
// on return, indent to level of previous line
2011-07-07 17:59:34 +00:00
if ( isCaret ( ) && caretColumn ( ) == 0 && caretLine ( ) > 0 )
{
2011-03-26 13:10:41 +00: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 ] ;
if ( /[\[\(\{]\s*$/ . exec ( prevLineText ) ) theIndent += THE _TAB ;
var cs = Changeset . builder ( rep . lines . totalWidth ( ) ) . keep (
2011-07-07 17:59:34 +00:00
rep . lines . offsetOfIndex ( lineNum ) , lineNum ) . insert (
theIndent , [
[ 'author' , thisAuthor ]
] , rep . apool ) . toString ( ) ;
2011-03-26 13:10:41 +00:00
performDocumentApplyChangeset ( cs ) ;
performSelectionChange ( [ lineNum , theIndent . length ] , [ lineNum , theIndent . length ] ) ;
}
}
2011-07-07 17:59:34 +00:00
function setupMozillaCaretHack ( lineNum )
{
2011-03-26 13:10:41 +00:00
// This is really ugly, but by god, it works!
// Fixes annoying Firefox caret artifact (observed in 2.0.0.12
// and unfixed in Firefox 2 as of now) where mutating the DOM
// and then moving the caret to the beginning of a line causes
// an image of the caret to be XORed at the top of the iframe.
// The previous solution involved remembering to set the selection
// later, in response to the next event in the queue, which was hugely
// annoying.
// This solution: add a space character (0x20) to the beginning of the line.
// After setting the selection, remove the space.
var lineNode = rep . lines . atIndex ( lineNum ) . lineNode ;
var fc = lineNode . firstChild ;
2011-07-07 17:59:34 +00:00
while ( isBlockElement ( fc ) && fc . firstChild )
{
2011-03-26 13:10:41 +00:00
fc = fc . firstChild ;
}
var textNode ;
2011-07-07 17:59:34 +00:00
if ( isNodeText ( fc ) )
{
fc . nodeValue = " " + fc . nodeValue ;
2011-03-26 13:10:41 +00:00
textNode = fc ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
textNode = doc . createTextNode ( " " ) ;
fc . parentNode . insertBefore ( textNode , fc ) ;
}
markNodeClean ( lineNode ) ;
2011-07-07 17:59:34 +00:00
return {
unhack : function ( )
{
if ( textNode . nodeValue == " " )
{
textNode . parentNode . removeChild ( textNode ) ;
}
else
{
textNode . nodeValue = textNode . nodeValue . substring ( 1 ) ;
}
markNodeClean ( lineNode ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
} ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function getPointForLineAndChar ( lineAndChar )
{
2011-03-26 13:10:41 +00:00
var line = lineAndChar [ 0 ] ;
var charsLeft = lineAndChar [ 1 ] ;
//console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key,
//getCleanNodeByKey(rep.lines.atIndex(line).key));
var lineEntry = rep . lines . atIndex ( line ) ;
charsLeft -= lineEntry . lineMarker ;
2011-07-07 17:59:34 +00:00
if ( charsLeft < 0 )
{
2011-03-26 13:10:41 +00:00
charsLeft = 0 ;
}
var lineNode = lineEntry . lineNode ;
var n = lineNode ;
var after = false ;
2011-07-07 17:59:34 +00:00
if ( charsLeft == 0 )
{
2011-03-26 13:10:41 +00:00
var index = 0 ;
2011-07-07 17:59:34 +00:00
if ( browser . msie && line == ( rep . lines . length ( ) - 1 ) && lineNode . childNodes . length == 0 )
{
// best to stay at end of last empty div in IE
index = 1 ;
}
return {
node : lineNode ,
index : index ,
maxIndex : 1
} ;
}
while ( ! ( n == lineNode && after ) )
{
if ( after )
{
if ( n . nextSibling )
{
n = n . nextSibling ;
after = false ;
}
else n = n . parentNode ;
}
else
{
if ( isNodeText ( n ) )
{
var len = n . nodeValue . length ;
if ( charsLeft <= len )
{
return {
node : n ,
index : charsLeft ,
maxIndex : len
} ;
}
charsLeft -= len ;
after = true ;
}
else
{
if ( n . firstChild ) n = n . firstChild ;
else after = true ;
}
}
}
return {
node : lineNode ,
index : 1 ,
maxIndex : 1
} ;
}
function nodeText ( n )
{
2011-03-26 13:10:41 +00:00
return n . innerText || n . textContent || n . nodeValue || '' ;
}
2011-07-07 17:59:34 +00:00
function getLineAndCharForPoint ( point )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
if ( point . node == root )
{
if ( point . index == 0 )
{
return [ 0 , 0 ] ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
var N = rep . lines . length ( ) ;
var ln = rep . lines . atIndex ( N - 1 ) ;
return [ N - 1 , ln . text . length ] ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
if ( isNodeText ( n ) )
{
col = point . index ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else if ( point . index > 0 )
{
col = nodeText ( n ) . length ;
2011-03-26 13:10:41 +00:00
}
var parNode , prevSib ;
2011-07-07 17:59:34 +00:00
while ( ( parNode = n . parentNode ) != root )
{
if ( ( prevSib = n . previousSibling ) )
{
n = prevSib ;
col += nodeText ( n ) . length ;
}
else
{
n = parNode ;
}
2011-03-26 13:10:41 +00:00
}
if ( n . id == "" ) console . debug ( "BAD" ) ;
2011-07-07 17:59:34 +00:00
if ( n . firstChild && isBlockElement ( n . firstChild ) )
{
2011-03-26 13:10:41 +00:00
col += 1 ; // lineMarker
}
var lineEntry = rep . lines . atKey ( n . id ) ;
var lineNum = rep . lines . indexOfEntry ( lineEntry ) ;
return [ lineNum , col ] ;
}
}
2011-07-07 17:59:34 +00:00
function createDomLineEntry ( lineString )
{
2011-03-26 13:10:41 +00:00
var info = doCreateDomLine ( lineString . length > 0 ) ;
var newNode = info . node ;
2011-07-07 17:59:34 +00:00
return {
key : uniqueId ( newNode ) ,
text : lineString ,
lineNode : newNode ,
domInfo : info ,
lineMarker : 0
} ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function canApplyChangesetToDocument ( changes )
{
2011-03-26 13:10:41 +00:00
return Changeset . oldLen ( changes ) == rep . alltext . length ;
}
2011-07-07 17:59:34 +00:00
function performDocumentApplyChangeset ( changes , insertsAfterSelection )
{
2011-03-26 13:10:41 +00:00
doRepApplyChangeset ( changes , insertsAfterSelection ) ;
var requiredSelectionSetting = null ;
2011-07-07 17:59:34 +00:00
if ( rep . selStart && rep . selEnd )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
var result = Changeset . characterRangeFollow ( changes , selStartChar , selEndChar , insertsAfterSelection ) ;
2011-03-26 13:10:41 +00:00
requiredSelectionSetting = [ result [ 0 ] , result [ 1 ] , rep . selFocusAtStart ] ;
}
var linesMutatee = {
2011-07-07 17:59:34 +00:00
splice : function ( start , numRemoved , newLinesVA )
{
domAndRepSplice ( start , numRemoved , map ( Array . prototype . slice . call ( arguments , 2 ) , function ( s )
{
return s . slice ( 0 , - 1 ) ;
} ) , null ) ;
} ,
get : function ( i )
{
return rep . lines . atIndex ( i ) . text + '\n' ;
} ,
length : function ( )
{
return rep . lines . length ( ) ;
2011-03-26 13:10:41 +00:00
} ,
2011-07-07 17:59:34 +00:00
slice _notused : function ( start , end )
{
return map ( rep . lines . slice ( start , end ) , function ( e )
{
return e . text + '\n' ;
} ) ;
2011-03-26 13:10:41 +00:00
}
} ;
Changeset . mutateTextLines ( changes , linesMutatee ) ;
checkALines ( ) ;
2011-07-07 17:59:34 +00:00
if ( requiredSelectionSetting )
{
performSelectionChange ( lineAndColumnFromChar ( requiredSelectionSetting [ 0 ] ) , lineAndColumnFromChar ( requiredSelectionSetting [ 1 ] ) , requiredSelectionSetting [ 2 ] ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function domAndRepSplice ( startLine , deleteCount , newLineStrings , isTimeUp )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00: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 13:10:41 +00:00
}
var lineEntries = map ( newLineStrings , createDomLineEntry ) ;
doRepLineSplice ( startLine , deleteCount , lineEntries ) ;
var nodeToAddAfter ;
2011-07-07 17:59:34 +00:00
if ( startLine > 0 )
{
nodeToAddAfter = getCleanNodeByKey ( rep . lines . atIndex ( startLine - 1 ) . key ) ;
2011-03-26 13:10:41 +00:00
}
else nodeToAddAfter = null ;
2011-07-07 17:59:34 +00:00
insertDomLines ( nodeToAddAfter , map ( lineEntries , function ( entry )
{
return entry . domInfo ;
} ) , isTimeUp ) ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
forEach ( keysToDelete , function ( k )
{
var n = doc . getElementById ( k ) ;
n . parentNode . removeChild ( n ) ;
2011-03-26 13:10:41 +00:00
} ) ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
}
}
2011-07-07 17:59:34 +00:00
function checkChangesetLineInformationAgainstRep ( changes )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
while ( opIter . hasNext ( ) )
{
2011-03-26 13:10:41 +00:00
var o = opIter . next ( ) ;
2011-07-07 17:59:34 +00:00
if ( o . opcode == '-' || o . opcode == '=' )
{
curOffset += o . chars ;
if ( o . lines )
{
curLine += o . lines ;
curCol = 0 ;
}
else
{
curCol += o . chars ;
}
2011-03-26 13:10:41 +00:00
}
var calcLine = rep . lines . indexOfOffset ( curOffset ) ;
var calcLineStart = rep . lines . offsetOfIndex ( calcLine ) ;
var calcCol = curOffset - calcLineStart ;
2011-07-07 17:59:34 +00:00
if ( calcCol != curCol || calcLine != curLine )
{
return false ;
2011-03-26 13:10:41 +00:00
}
}
return true ;
}
2011-07-07 17:59:34 +00:00
function doRepApplyChangeset ( changes , insertsAfterSelection )
{
2011-03-26 13:10:41 +00:00
Changeset . checkRep ( changes ) ;
2011-07-07 17:59:34 +00:00
if ( Changeset . oldLen ( changes ) != rep . alltext . length ) throw new Error ( "doRepApplyChangeset length mismatch: " + Changeset . oldLen ( changes ) + "/" + rep . alltext . length ) ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
if ( ! checkChangesetLineInformationAgainstRep ( changes ) )
{
2011-03-26 13:10:41 +00:00
throw new Error ( "doRepApplyChangeset line break mismatch" ) ;
}
2011-07-07 17:59:34 +00:00
( function doRecordUndoInformation ( changes )
{
2011-03-26 13:10:41 +00:00
var editEvent = currentCallStack . editEvent ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
} ) ( changes ) ;
//rep.alltext = Changeset.applyToText(changes, rep.alltext);
Changeset . mutateAttributionLines ( changes , rep . alines , rep . apool ) ;
2011-07-07 17:59:34 +00:00
if ( changesetTracker . isTracking ( ) )
{
2011-03-26 13:10:41 +00:00
changesetTracker . composeUserChangeset ( changes ) ;
}
}
2011-07-07 17:59:34 +00:00
function lineAndColumnFromChar ( x )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function performDocumentReplaceCharRange ( startChar , endChar , newText )
{
if ( startChar == endChar && newText . length == 0 )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00: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 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else if ( newText . length == 0 )
{
// a delete at end
startChar -- ;
endChar -- ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
// a replace at end
endChar -- ;
newText = newText . substring ( 0 , newText . length - 1 ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
performDocumentReplaceRange ( lineAndColumnFromChar ( startChar ) , lineAndColumnFromChar ( endChar ) , newText ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function performDocumentReplaceRange ( start , end , newText )
{
2011-03-26 13:10:41 +00:00
if ( start == undefined ) start = rep . selStart ;
if ( end == undefined ) end = rep . selEnd ;
//dmesg(String([start.toSource(),end.toSource(),newText.toSource()]));
// start[0]: <--- start[1] --->CCCCCCCCCCC\n
// CCCCCCCCCCCCCCCCCCCC\n
// CCCC\n
// end[0]: <CCC end[1] CCC>-------\n
var builder = Changeset . builder ( rep . lines . totalWidth ( ) ) ;
buildKeepToStartOfRange ( builder , start ) ;
buildRemoveRange ( builder , start , end ) ;
2011-07-07 17:59:34 +00:00
builder . insert ( newText , [
[ 'author' , thisAuthor ]
] , rep . apool ) ;
2011-03-26 13:10:41 +00:00
var cs = builder . toString ( ) ;
performDocumentApplyChangeset ( cs ) ;
}
2011-07-07 17:59:34 +00:00
function performDocumentApplyAttributesToCharRange ( start , end , attribs )
{
if ( end >= rep . alltext . length )
{
end = rep . alltext . length - 1 ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
performDocumentApplyAttributesToRange ( lineAndColumnFromChar ( start ) , lineAndColumnFromChar ( end ) , attribs ) ;
2011-03-26 13:10:41 +00:00
}
editorInfo . ace _performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange ;
2011-07-07 17:59:34 +00:00
function performDocumentApplyAttributesToRange ( start , end , attribs )
{
2011-03-26 13:10:41 +00:00
var builder = Changeset . builder ( rep . lines . totalWidth ( ) ) ;
buildKeepToStartOfRange ( builder , start ) ;
buildKeepRange ( builder , start , end , attribs , rep . apool ) ;
var cs = builder . toString ( ) ;
performDocumentApplyChangeset ( cs ) ;
}
2011-07-07 17:59:34 +00:00
function buildKeepToStartOfRange ( builder , start )
{
2011-03-26 13:10:41 +00:00
var startLineOffset = rep . lines . offsetOfIndex ( start [ 0 ] ) ;
builder . keep ( startLineOffset , start [ 0 ] ) ;
builder . keep ( start [ 1 ] ) ;
}
2011-07-07 17:59:34 +00:00
function buildRemoveRange ( builder , start , end )
{
2011-03-26 13:10:41 +00:00
var startLineOffset = rep . lines . offsetOfIndex ( start [ 0 ] ) ;
var endLineOffset = rep . lines . offsetOfIndex ( end [ 0 ] ) ;
2011-07-07 17:59:34 +00:00
if ( end [ 0 ] > start [ 0 ] )
{
2011-03-26 13:10:41 +00:00
builder . remove ( endLineOffset - startLineOffset - start [ 1 ] , end [ 0 ] - start [ 0 ] ) ;
builder . remove ( end [ 1 ] ) ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
builder . remove ( end [ 1 ] - start [ 1 ] ) ;
}
}
2011-07-07 17:59:34 +00:00
function buildKeepRange ( builder , start , end , attribs , pool )
{
2011-03-26 13:10:41 +00:00
var startLineOffset = rep . lines . offsetOfIndex ( start [ 0 ] ) ;
var endLineOffset = rep . lines . offsetOfIndex ( end [ 0 ] ) ;
2011-07-07 17:59:34 +00:00
if ( end [ 0 ] > start [ 0 ] )
{
2011-03-26 13:10:41 +00:00
builder . keep ( endLineOffset - startLineOffset - start [ 1 ] , end [ 0 ] - start [ 0 ] , attribs , pool ) ;
builder . keep ( end [ 1 ] , 0 , attribs , pool ) ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
builder . keep ( end [ 1 ] - start [ 1 ] , 0 , attribs , pool ) ;
}
}
2011-07-07 17:59:34 +00:00
function setAttributeOnSelection ( attributeName , attributeValue )
{
2011-03-26 13:10:41 +00:00
if ( ! ( rep . selStart && rep . selEnd ) ) return ;
2011-07-07 17:59:34 +00:00
performDocumentApplyAttributesToRange ( rep . selStart , rep . selEnd , [
[ attributeName , attributeValue ]
] ) ;
2011-03-26 13:10:41 +00:00
}
editorInfo . ace _setAttributeOnSelection = setAttributeOnSelection ;
2011-07-07 17:59:34 +00:00
function toggleAttributeOnSelection ( attributeName )
{
2011-03-26 13:10:41 +00:00
if ( ! ( rep . selStart && rep . selEnd ) ) return ;
var selectionAllHasIt = true ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
var selStartLine = rep . selStart [ 0 ] ;
var selEndLine = rep . selEnd [ 0 ] ;
2011-07-07 17:59:34 +00:00
for ( var n = selStartLine ; n <= selEndLine ; n ++ )
{
2011-03-26 13:10:41 +00:00
var opIter = Changeset . opIterator ( rep . alines [ n ] ) ;
var indexIntoLine = 0 ;
var selectionStartInLine = 0 ;
var selectionEndInLine = rep . lines . atIndex ( n ) . text . length ; // exclude newline
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( ! selectionAllHasIt )
{
break ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
if ( selectionAllHasIt )
{
performDocumentApplyAttributesToRange ( rep . selStart , rep . selEnd , [
[ attributeName , '' ]
] ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
performDocumentApplyAttributesToRange ( rep . selStart , rep . selEnd , [
[ attributeName , 'true' ]
] ) ;
2011-03-26 13:10:41 +00:00
}
}
editorInfo . ace _toggleAttributeOnSelection = toggleAttributeOnSelection ;
2011-07-07 17:59:34 +00:00
function performDocumentReplaceSelection ( newText )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function doRepLineSplice ( startLine , deleteCount , newLineEntries )
{
forEach ( newLineEntries , function ( entry )
{
entry . width = entry . text . length + 1 ;
} ) ;
2011-03-26 13:10:41 +00:00
var startOldChar = rep . lines . offsetOfIndex ( startLine ) ;
2011-07-07 17:59:34 +00:00
var endOldChar = rep . lines . offsetOfIndex ( startLine + deleteCount ) ;
2011-03-26 13:10:41 +00:00
var oldRegionStart = rep . lines . offsetOfIndex ( startLine ) ;
2011-07-07 17:59:34 +00:00
var oldRegionEnd = rep . lines . offsetOfIndex ( startLine + deleteCount ) ;
2011-03-26 13:10:41 +00:00
rep . lines . splice ( startLine , deleteCount , newLineEntries ) ;
currentCallStack . docTextChanged = true ;
currentCallStack . repChanged = true ;
var newRegionEnd = rep . lines . offsetOfIndex ( startLine + newLineEntries . length ) ;
2011-07-07 17:59:34 +00:00
var newText = map ( newLineEntries , function ( e )
{
return e . text + '\n' ;
} ) . join ( '' ) ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
rep . alltext = rep . alltext . substring ( 0 , startOldChar ) + newText + rep . alltext . substring ( endOldChar , rep . alltext . length ) ;
2011-03-26 13:10:41 +00:00
//var newTotalLength = rep.alltext.length;
//rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart,
//newRegionEnd - oldRegionStart);
}
2011-07-07 17:59:34 +00:00
function doIncorpLineSplice ( startLine , deleteCount , newLineEntries , lineAttribs , hints )
{
2011-03-26 13:10:41 +00:00
var startOldChar = rep . lines . offsetOfIndex ( startLine ) ;
2011-07-07 17:59:34 +00:00
var endOldChar = rep . lines . offsetOfIndex ( startLine + deleteCount ) ;
2011-03-26 13:10:41 +00:00
var oldRegionStart = rep . lines . offsetOfIndex ( startLine ) ;
var selStartHintChar , selEndHintChar ;
2011-07-07 17:59:34 +00:00
if ( hints && hints . selStart )
{
selStartHintChar = rep . lines . offsetOfIndex ( hints . selStart [ 0 ] ) + hints . selStart [ 1 ] - oldRegionStart ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( hints && hints . selEnd )
{
selEndHintChar = rep . lines . offsetOfIndex ( hints . selEnd [ 0 ] ) + hints . selEnd [ 1 ] - oldRegionStart ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
var newText = map ( newLineEntries , function ( e )
{
return e . text + '\n' ;
} ) . join ( '' ) ;
2011-03-26 13:10:41 +00:00
var oldText = rep . alltext . substring ( startOldChar , endOldChar ) ;
2011-07-07 17:59:34 +00: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 13:10:41 +00: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 17:59:34 +00:00
var spliceStart = startOldChar + commonStart ;
var spliceEnd = endOldChar - commonEnd ;
2011-03-26 13:10:41 +00:00
var shiftFinalNewlineToBeforeNewText = false ;
// adjust the splice to not involve the final newline of the document;
// be very defensive
2011-07-07 17:59:34 +00:00
if ( shortOldText . charAt ( shortOldText . length - 1 ) == '\n' && shortNewText . charAt ( shortNewText . length - 1 ) == '\n' )
{
2011-03-26 13:10:41 +00:00
// replacing text that ends in newline with text that also ends in newline
// (still, after analysis, somehow)
2011-07-07 17:59:34 +00:00
shortOldText = shortOldText . slice ( 0 , - 1 ) ;
shortNewText = shortNewText . slice ( 0 , - 1 ) ;
2011-03-26 13:10:41 +00:00
spliceEnd -- ;
commonEnd ++ ;
}
2011-07-07 17:59:34 +00:00
if ( shortOldText . length == 0 && spliceStart == rep . alltext . length && shortNewText . length > 0 )
{
2011-03-26 13:10:41 +00:00
// inserting after final newline, bad
spliceStart -- ;
spliceEnd -- ;
2011-07-07 17:59:34 +00:00
shortNewText = '\n' + shortNewText . slice ( 0 , - 1 ) ;
2011-03-26 13:10:41 +00:00
shiftFinalNewlineToBeforeNewText = true ;
}
2011-07-07 17:59:34 +00:00
if ( spliceEnd == rep . alltext . length && shortOldText . length > 0 && shortNewText . length == 0 )
{
2011-03-26 13:10:41 +00:00
// deletion at end of rep.alltext
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
if ( ! ( shortOldText . length == 0 && shortNewText . length == 0 ) )
{
2011-03-26 13:10:41 +00:00
var oldDocText = rep . alltext ;
var oldLen = oldDocText . length ;
var spliceStartLine = rep . lines . indexOfOffset ( spliceStart ) ;
var spliceStartLineStart = rep . lines . offsetOfIndex ( spliceStartLine ) ;
2011-07-07 17:59:34 +00:00
function startBuilder ( )
{
var builder = Changeset . builder ( oldLen ) ;
builder . keep ( spliceStartLineStart , spliceStartLine ) ;
builder . keep ( spliceStart - spliceStartLineStart ) ;
return builder ;
}
function eachAttribRun ( attribs , func /*(startInNewText, endInNewText, attribs)*/ )
{
var attribsIter = Changeset . opIterator ( attribs ) ;
var textIndex = 0 ;
var newTextStart = commonStart ;
var newTextEnd = newText . length - commonEnd - ( shiftFinalNewlineToBeforeNewText ? 1 : 0 ) ;
while ( attribsIter . hasNext ( ) )
{
var op = attribsIter . next ( ) ;
var nextIndex = textIndex + op . chars ;
if ( ! ( nextIndex <= newTextStart || textIndex >= newTextEnd ) )
{
func ( Math . max ( newTextStart , textIndex ) , Math . min ( newTextEnd , nextIndex ) , op . attribs ) ;
}
textIndex = nextIndex ;
}
2011-03-26 13:10:41 +00:00
}
var justApplyStyles = ( shortNewText == shortOldText ) ;
var theChangeset ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
var isNewTextMultiauthor = false ;
2011-07-07 17:59:34 +00:00
var authorAtt = Changeset . makeAttribsString ( '+' , ( thisAuthor ? [
[ 'author' , thisAuthor ]
] : [ ] ) , rep . apool ) ;
var authorizer = cachedStrFunc ( function ( oldAtts )
{
if ( isNewTextMultiauthor )
{
2011-03-26 13:10:41 +00:00
// prefer colors from DOM
2011-07-07 17:59:34 +00:00
return Changeset . composeAttributes ( authorAtt , oldAtts , true , rep . apool ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
// use this author's color
2011-07-07 17:59:34 +00:00
return Changeset . composeAttributes ( oldAtts , authorAtt , true , rep . apool ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
} ) ;
2011-03-26 13:10:41 +00:00
var foundDomAuthor = '' ;
2011-07-07 17:59:34 +00:00
eachAttribRun ( newAttribs , function ( start , end , attribs )
{
2011-03-26 13:10:41 +00:00
var a = Changeset . attribsAttributeValue ( attribs , 'author' , rep . apool ) ;
2011-07-07 17:59:34 +00:00
if ( a && a != foundDomAuthor )
{
if ( ! foundDomAuthor )
{
2011-03-26 13:10:41 +00:00
foundDomAuthor = a ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
isNewTextMultiauthor = true ; // multiple authors in DOM!
}
}
2011-07-07 17:59:34 +00:00
} ) ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
if ( shiftFinalNewlineToBeforeNewText )
{
builder . insert ( '\n' , authorizer ( '' ) ) ;
}
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
eachAttribRun ( newAttribs , function ( start , end , attribs )
{
builder . insert ( newText . substring ( start , end ) , authorizer ( attribs ) ) ;
} ) ;
theChangeset = builder . toString ( ) ;
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function cachedStrFunc ( func )
{
2011-03-26 13:10:41 +00:00
var cache = { } ;
2011-07-07 17:59:34 +00:00
return function ( s )
{
if ( ! cache [ s ] )
{
cache [ s ] = func ( s ) ;
2011-03-26 13:10:41 +00:00
}
return cache [ s ] ;
} ;
}
2011-07-07 17:59:34 +00:00
function analyzeChange ( oldText , newText , oldAttribs , newAttribs , optSelStartHint , optSelEndHint )
{
function incorpedAttribFilter ( anum )
{
2011-03-26 13:10:41 +00:00
return isStyleAttribute ( rep . apool . getAttribKey ( anum ) ) ;
}
2011-07-07 17:59:34 +00:00
function attribRuns ( attribs )
{
2011-03-26 13:10:41 +00:00
var lengs = [ ] ;
var atts = [ ] ;
var iter = Changeset . opIterator ( attribs ) ;
2011-07-07 17:59:34 +00:00
while ( iter . hasNext ( ) )
{
var op = iter . next ( ) ;
lengs . push ( op . chars ) ;
atts . push ( op . attribs ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
return [ lengs , atts ] ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function attribIterator ( runs , backward )
{
2011-03-26 13:10:41 +00:00
var lengs = runs [ 0 ] ;
var atts = runs [ 1 ] ;
2011-07-07 17:59:34 +00:00
var i = ( backward ? lengs . length - 1 : 0 ) ;
2011-03-26 13:10:41 +00:00
var j = 0 ;
2011-07-07 17:59:34 +00: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 13:10:41 +00: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 17:59:34 +00:00
while ( commonStart < minLen )
{
if ( oldText . charAt ( commonStart ) == newText . charAt ( commonStart ) && oldStartIter ( ) == newStartIter ( ) )
{
commonStart ++ ;
2011-03-26 13:10:41 +00:00
}
else break ;
}
var commonEnd = 0 ;
var oldEndIter = attribIterator ( oldARuns , true ) ;
var newEndIter = attribIterator ( newARuns , true ) ;
2011-07-07 17:59:34 +00:00
while ( commonEnd < minLen )
{
if ( commonEnd == 0 )
{
// assume newline in common
oldEndIter ( ) ;
newEndIter ( ) ;
commonEnd ++ ;
}
else if ( oldText . charAt ( oldLen - 1 - commonEnd ) == newText . charAt ( newLen - 1 - commonEnd ) && oldEndIter ( ) == newEndIter ( ) )
{
commonEnd ++ ;
2011-03-26 13:10:41 +00:00
}
else break ;
}
var hintedCommonEnd = - 1 ;
2011-07-07 17:59:34 +00:00
if ( ( typeof optSelEndHint ) == "number" )
{
2011-03-26 13:10:41 +00:00
hintedCommonEnd = newLen - optSelEndHint ;
}
2011-07-07 17:59:34 +00:00
if ( commonStart + commonEnd > oldLen )
{
2011-03-26 13:10:41 +00:00
// ambiguous insertion
var minCommonEnd = oldLen - commonStart ;
var maxCommonEnd = commonEnd ;
2011-07-07 17:59:34 +00:00
if ( hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd )
{
commonEnd = hintedCommonEnd ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
commonEnd = minCommonEnd ;
2011-03-26 13:10:41 +00:00
}
commonStart = oldLen - commonEnd ;
}
2011-07-07 17:59:34 +00:00
if ( commonStart + commonEnd > newLen )
{
2011-03-26 13:10:41 +00:00
// ambiguous deletion
var minCommonEnd = newLen - commonStart ;
var maxCommonEnd = commonEnd ;
2011-07-07 17:59:34 +00:00
if ( hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd )
{
commonEnd = hintedCommonEnd ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
commonEnd = minCommonEnd ;
2011-03-26 13:10:41 +00:00
}
commonStart = newLen - commonEnd ;
}
return [ commonStart , commonEnd ] ;
}
2011-07-07 17:59:34 +00:00
function equalLineAndChars ( a , b )
{
2011-03-26 13:10:41 +00:00
if ( ! a ) return ! b ;
if ( ! b ) return ! a ;
return ( a [ 0 ] == b [ 0 ] && a [ 1 ] == b [ 1 ] ) ;
}
2011-07-07 17:59:34 +00:00
function performSelectionChange ( selectStart , selectEnd , focusAtStart )
{
if ( repSelectionChange ( selectStart , selectEnd , focusAtStart ) )
{
2011-03-26 13:10:41 +00:00
currentCallStack . selectionAffected = true ;
}
}
// 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 17:59:34 +00:00
function repSelectionChange ( selectStart , selectEnd , focusAtStart )
{
2011-03-26 13:10:41 +00:00
focusAtStart = ! ! focusAtStart ;
2011-07-07 17:59:34 +00:00
var newSelFocusAtStart = ( focusAtStart && ( ( ! selectStart ) || ( ! selectEnd ) || ( selectStart [ 0 ] != selectEnd [ 0 ] ) || ( selectStart [ 1 ] != selectEnd [ 1 ] ) ) ) ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
if ( ( ! equalLineAndChars ( rep . selStart , selectStart ) ) || ( ! equalLineAndChars ( rep . selEnd , selectEnd ) ) || ( rep . selFocusAtStart != newSelFocusAtStart ) )
{
2011-03-26 13:10:41 +00:00
rep . selStart = selectStart ;
rep . selEnd = selectEnd ;
rep . selFocusAtStart = newSelFocusAtStart ;
if ( mozillaFakeArrows ) mozillaFakeArrows . notifySelectionChanged ( ) ;
currentCallStack . repChanged = true ;
return true ;
//console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd,
//String(!!rep.selFocusAtStart));
}
return false ;
//console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart);
}
2011-07-07 17:59:34 +00:00
function doCreateDomLine ( nonEmpty )
{
if ( browser . msie && ( ! nonEmpty ) )
{
var result = {
node : null ,
appendSpan : noop ,
prepareForAdd : noop ,
notifyAdded : noop ,
clearSpans : noop ,
finishUpdate : noop ,
lineMarker : 0
} ;
2011-03-26 13:10:41 +00:00
var lineElem = doc . createElement ( "div" ) ;
result . node = lineElem ;
2011-07-07 17:59:34 +00: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.
2011-11-26 00:10:21 +00:00
lineElem . innerHTML = "" ;
2011-07-07 17:59:34 +00:00
// a primitive-valued property survives copy-and-paste
setAssoc ( lineElem , "shouldBeEmpty" , true ) ;
// an object property doesn't
setAssoc ( lineElem , "unpasted" , { } ) ;
2011-03-26 13:10:41 +00:00
} ;
var lineClass = 'ace-line' ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
} ;
2011-07-07 17:59:34 +00:00
result . clearSpans = function ( )
{
lineClass = '' ; // non-null to cause update
2011-03-26 13:10:41 +00:00
} ;
2011-07-07 17:59:34 +00:00
function writeClass ( )
{
if ( lineClass !== null ) lineElem . className = lineClass ;
2011-03-26 13:10:41 +00:00
}
result . prepareForAdd = writeClass ;
result . finishUpdate = writeClass ;
2011-07-07 17:59:34 +00:00
result . getInnerHTML = function ( )
{
return "" ;
} ;
2011-03-26 13:10:41 +00:00
return result ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
return domline . createDomLine ( nonEmpty , doesWrap , browser , doc ) ;
}
}
2011-07-07 17:59:34 +00:00
function textify ( str )
{
2011-03-26 13:10:41 +00:00
return str . replace ( /[\n\r ]/g , ' ' ) . replace ( /\xa0/g , ' ' ) . replace ( /\t/g , ' ' ) ;
}
2011-07-07 17:59:34 +00:00
var _blockElems = {
"div" : 1 ,
"p" : 1 ,
"pre" : 1 ,
"li" : 1 ,
"ol" : 1 ,
"ul" : 1
} ;
function isBlockElement ( n )
{
2011-03-26 13:10:41 +00:00
return ! ! _blockElems [ ( n . tagName || "" ) . toLowerCase ( ) ] ;
}
2011-07-07 17:59:34 +00:00
function getDirtyRanges ( )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function cleanNodeForIndex ( i )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00: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 13:10:41 +00:00
}
return cleanNodeForIndexCache [ i ] ;
}
var isConsecutiveCache = { } ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
return isConsecutiveCache [ i ] ;
}
2011-07-07 17:59:34 +00:00
function isClean ( i )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
return ! ! cleanNodeForIndex ( i ) ;
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
var cleanRanges = [
[ - 1 , N + 1 ]
] ;
function rangeForLine ( i )
{
2011-03-26 13:10:41 +00:00
// returns index of cleanRange containing i, or -1 if none
var answer = - 1 ;
2011-07-07 17:59:34 +00:00
forEach ( cleanRanges , function ( r , idx )
{
if ( i >= r [ 1 ] ) return false ; // keep looking
if ( i < r [ 0 ] ) return true ; // not found, stop looking
answer = idx ;
return true ; // found, stop looking
2011-03-26 13:10:41 +00:00
} ) ;
return answer ;
}
2011-07-07 17:59:34 +00:00
function removeLineFromRange ( rng , line )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
if ( ( a + 1 ) == b ) cleanRanges . splice ( rng , 1 ) ;
2011-03-26 13:10:41 +00:00
else if ( line == a ) cleanRanges [ rng ] [ 0 ] ++ ;
2011-07-07 17:59:34 +00:00
else if ( line == ( b - 1 ) ) cleanRanges [ rng ] [ 1 ] -- ;
else cleanRanges . splice ( rng , 1 , [ a , line ] , [ line + 1 , b ] ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function splitRange ( rng , pt )
{
2011-03-26 13:10:41 +00:00
// precond: pt splits cleanRanges[rng] into two non-empty ranges
var a = cleanRanges [ rng ] [ 0 ] ;
var b = cleanRanges [ rng ] [ 1 ] ;
2011-07-07 17:59:34 +00:00
cleanRanges . splice ( rng , 1 , [ a , pt ] , [ pt , b ] ) ;
2011-03-26 13:10:41 +00:00
}
var correctedLines = { } ;
2011-07-07 17:59:34 +00:00
function correctlyAssignLine ( line )
{
2011-03-26 13:10:41 +00: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).
//console.log("correctly assigning: %d", line);
var rng = rangeForLine ( line ) ;
var lineClean = isClean ( line ) ;
2011-07-07 17:59:34 +00:00
if ( rng < 0 )
{
if ( lineClean )
{
console . debug ( "somehow lost clean line" ) ;
}
return true ;
}
if ( ! lineClean )
{
// a clean-range includes this dirty line, fix it
removeLineFromRange ( rng , line ) ;
return false ;
}
else
{
// line is clean, but could be wrongly connected to a clean line
// above or below
var a = cleanRanges [ rng ] [ 0 ] ;
var b = cleanRanges [ rng ] [ 1 ] ;
var didSomething = false ;
// we'll leave non-clean adjacent nodes in the clean range for the caller to
// detect and deal with. we deal with whether the range should be split
// just above or just below this line.
if ( a < line && isClean ( line - 1 ) && ! isConsecutive ( line ) )
{
splitRange ( rng , line ) ;
didSomething = true ;
}
if ( b > ( line + 1 ) && isClean ( line + 1 ) && ! isConsecutive ( line + 1 ) )
{
splitRange ( rng , line + 1 ) ;
didSomething = true ;
}
return ! didSomething ;
}
}
function detectChangesAroundLine ( line , reqInARow )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
while ( correctInARow < reqInARow && currentIndex >= 0 )
{
if ( correctlyAssignLine ( currentIndex ) )
{
correctInARow ++ ;
}
else correctInARow = 0 ;
currentIndex -- ;
2011-03-26 13:10:41 +00:00
}
correctInARow = 0 ;
currentIndex = line ;
2011-07-07 17:59:34 +00:00
while ( correctInARow < reqInARow && currentIndex < N )
{
if ( correctlyAssignLine ( currentIndex ) )
{
correctInARow ++ ;
}
else correctInARow = 0 ;
currentIndex ++ ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
if ( N == 0 )
{
2011-03-26 13:10:41 +00:00
p . cancel ( ) ;
2011-07-07 17:59:34 +00:00
if ( ! isConsecutive ( 0 ) )
{
splitRange ( 0 , 0 ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
p . mark ( "topbot" ) ;
2011-07-07 17:59:34 +00:00
detectChangesAroundLine ( 0 , 1 ) ;
detectChangesAroundLine ( N - 1 , 1 ) ;
2011-03-26 13:10:41 +00:00
p . mark ( "obs" ) ;
//console.log("observedChanges: "+toSource(observedChanges));
2011-07-07 17:59:34 +00: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 13:10:41 +00: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 17:59:34 +00:00
for ( var r = 0 ; r < cleanRanges . length - 1 ; r ++ )
{
dirtyRanges . push ( [ cleanRanges [ r ] [ 1 ] , cleanRanges [ r + 1 ] [ 0 ] ] ) ;
2011-03-26 13:10:41 +00:00
}
p . end ( ) ;
return dirtyRanges ;
}
2011-07-07 17:59:34 +00:00
function markNodeClean ( n )
{
2011-03-26 13:10:41 +00:00
// clean nodes have knownHTML that matches their innerHTML
var dirtiness = { } ;
dirtiness . nodeId = uniqueId ( n ) ;
dirtiness . knownHTML = n . innerHTML ;
2011-07-07 17:59:34 +00:00
if ( browser . msie )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function isNodeDirty ( n )
{
2011-03-26 13:10:41 +00: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 ;
2011-07-07 17:59:34 +00:00
if ( browser . msie )
{
2011-03-26 13:10:41 +00:00
if ( n . innerText !== data . knownText ) return true ;
}
if ( n . innerHTML !== data . knownHTML ) return true ;
p . end ( ) ;
return false ;
}
2011-07-07 17:59:34 +00:00
function getLineEntryTopBottom ( entry , destObj )
{
2011-03-26 13:10:41 +00:00
var dom = entry . lineNode ;
var top = dom . offsetTop ;
var height = dom . offsetHeight ;
var obj = ( destObj || { } ) ;
obj . top = top ;
2011-07-07 17:59:34 +00:00
obj . bottom = ( top + height ) ;
2011-03-26 13:10:41 +00:00
return obj ;
}
2011-07-07 17:59:34 +00:00
function getViewPortTopBottom ( )
{
2011-03-26 13:10:41 +00:00
var theTop = getScrollY ( ) ;
var doc = outerWin . document ;
var height = doc . documentElement . clientHeight ;
2011-07-07 17:59:34 +00:00
return {
top : theTop ,
bottom : ( theTop + height )
} ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function getVisibleLineRange ( )
{
2011-03-26 13:10:41 +00:00
var viewport = getViewPortTopBottom ( ) ;
//console.log("viewport top/bottom: %o", viewport);
var obj = { } ;
2011-07-07 17:59:34 +00:00
var start = rep . lines . search ( function ( e )
{
2011-03-26 13:10:41 +00:00
return getLineEntryTopBottom ( e , obj ) . bottom > viewport . top ;
} ) ;
2011-07-07 17:59:34 +00:00
var end = rep . lines . search ( function ( e )
{
2011-03-26 13:10:41 +00:00
return getLineEntryTopBottom ( e , obj ) . top >= viewport . bottom ;
} ) ;
if ( end < start ) end = start ; // unlikely
//console.log(start+","+end);
2011-07-07 17:59:34 +00:00
return [ start , end ] ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function getVisibleCharRange ( )
{
2011-03-26 13:10:41 +00:00
var lineRange = getVisibleLineRange ( ) ;
2011-07-07 17:59:34 +00:00
return [ rep . lines . offsetOfIndex ( lineRange [ 0 ] ) , rep . lines . offsetOfIndex ( lineRange [ 1 ] ) ] ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function handleClick ( evt )
{
2011-07-08 14:19:38 +00:00
//hide the dropdowns
2012-01-15 18:24:18 +00:00
if ( window . top . padeditbar ) { // required in case its in an iframe should probably use parent.. See Issue 327 https://github.com/Pita/etherpad-lite/issues/327
window . top . padeditbar . toogleDropDown ( "none" ) ;
}
2011-07-08 14:19:38 +00:00
2011-07-07 17:59:34 +00:00
inCallStack ( "handleClick" , function ( )
{
2011-03-26 13:10:41 +00:00
idleWorkTimer . atMost ( 200 ) ;
} ) ;
// only want to catch left-click
2011-07-07 17:59:34 +00:00
if ( ( ! evt . ctrlKey ) && ( evt . button != 2 ) && ( evt . button != 3 ) )
{
2011-03-26 13:10:41 +00:00
// find A tag with HREF
2011-07-07 17:59:34 +00:00
function isLink ( n )
{
return ( n . tagName || '' ) . toLowerCase ( ) == "a" && n . href ;
}
2011-03-26 13:10:41 +00:00
var n = evt . target ;
2011-07-07 17:59:34 +00:00
while ( n && n . parentNode && ! isLink ( n ) )
{
n = n . parentNode ;
}
if ( n && isLink ( n ) )
{
try
{
var newWindow = window . open ( n . href , '_blank' ) ;
newWindow . focus ( ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
catch ( e )
{
2011-03-26 13:10:41 +00:00
// absorb "user canceled" error in IE for certain prompts
}
2011-07-07 17:59:34 +00:00
evt . preventDefault ( ) ;
2011-03-26 13:10:41 +00:00
}
}
}
2011-07-07 17:59:34 +00:00
function doReturnKey ( )
{
if ( ! ( rep . selStart && rep . selEnd ) )
{
2011-03-26 13:10:41 +00:00
return ;
}
var lineNum = rep . selStart [ 0 ] ;
var listType = getLineListType ( lineNum ) ;
2011-07-07 17:59:34 +00:00
if ( listType )
{
2012-01-15 17:20:20 +00:00
var text = rep . lines . atIndex ( lineNum ) . text ;
listType = /([a-z]+)([12345678])/ . exec ( listType ) ;
var type = listType [ 1 ] ;
var level = Number ( listType [ 2 ] ) ;
//detect empty list item; exclude indentation
if ( text === '*' && type !== "indent" )
2011-07-07 17:59:34 +00:00
{
2012-01-15 17:20:20 +00: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
}
}
else if ( lineNum + 1 < rep . lines . length ( ) )
{
performDocumentReplaceSelection ( '\n' ) ;
setLineListType ( lineNum + 1 , type + level ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
else
{
2012-01-15 17:20:20 +00:00
performDocumentReplaceSelection ( '\n' ) ;
2011-03-26 13:10:41 +00:00
handleReturnIndentation ( ) ;
}
}
2011-07-07 17:59:34 +00:00
function doIndentOutdent ( isOut )
{
2011-11-25 23:02:25 +00:00
if ( ! ( rep . selStart && rep . selEnd ) ||
( ( rep . selStart [ 0 ] == rep . selEnd [ 0 ] ) && ( rep . selStart [ 1 ] == rep . selEnd [ 1 ] ) && rep . selEnd [ 1 ] > 1 ) )
2011-07-07 17:59:34 +00:00
{
2011-03-26 13:10:41 +00:00
return false ;
}
var firstLine , lastLine ;
firstLine = rep . selStart [ 0 ] ;
2011-07-07 17:59:34 +00:00
lastLine = Math . max ( firstLine , rep . selEnd [ 0 ] - ( ( rep . selEnd [ 1 ] == 0 ) ? 1 : 0 ) ) ;
2011-03-26 13:10:41 +00:00
var mods = [ ] ;
2011-07-07 17:59:34 +00:00
for ( var n = firstLine ; n <= lastLine ; n ++ )
{
2011-03-26 13:10:41 +00:00
var listType = getLineListType ( n ) ;
2011-11-25 09:29:31 +00:00
var t = 'indent' ;
var level = 0 ;
2011-07-07 17:59:34 +00:00
if ( listType )
{
2011-03-26 13:10:41 +00:00
listType = /([a-z]+)([12345678])/ . exec ( listType ) ;
2011-07-07 17:59:34 +00:00
if ( listType )
{
2011-11-25 09:29:31 +00:00
t = listType [ 1 ] ;
level = Number ( listType [ 2 ] ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-11-25 09:29:31 +00: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 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( mods . length > 0 )
{
2011-03-26 13:10:41 +00:00
setLineListTypes ( mods ) ;
}
2011-11-25 09:29:31 +00:00
return true ;
2011-03-26 13:10:41 +00:00
}
editorInfo . ace _doIndentOutdent = doIndentOutdent ;
2011-07-07 17:59:34 +00:00
function doTabKey ( shiftDown )
{
if ( ! doIndentOutdent ( shiftDown ) )
{
2011-03-26 13:10:41 +00:00
performDocumentReplaceSelection ( THE _TAB ) ;
}
}
2011-07-07 17:59:34 +00:00
function doDeleteKey ( optEvt )
{
2011-03-26 13:10:41 +00:00
var evt = optEvt || { } ;
var handled = false ;
2011-07-07 17:59:34 +00:00
if ( rep . selStart )
{
if ( isCaret ( ) )
{
var lineNum = caretLine ( ) ;
var col = caretColumn ( ) ;
2011-03-26 13:10:41 +00:00
var lineEntry = rep . lines . atIndex ( lineNum ) ;
2011-07-07 17:59:34 +00:00
var lineText = lineEntry . text ;
2011-03-26 13:10:41 +00:00
var lineMarker = lineEntry . lineMarker ;
2011-07-07 17:59:34 +00:00
if ( /^ +$/ . exec ( lineText . substring ( lineMarker , col ) ) )
{
2011-03-26 13:10:41 +00:00
var col2 = col - lineMarker ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
var theLine = caretLine ( ) ;
var lineEntry = rep . lines . atIndex ( theLine ) ;
2011-07-07 17:59:34 +00:00
if ( caretColumn ( ) <= lineEntry . lineMarker )
{
2011-03-26 13:10:41 +00:00
// delete at beginning of line
var action = 'delete_newline' ;
2011-07-07 17:59:34 +00:00
var prevLineListType = ( theLine > 0 ? getLineListType ( theLine - 1 ) : '' ) ;
2011-03-26 13:10:41 +00:00
var thisLineListType = getLineListType ( theLine ) ;
2011-07-07 17:59:34 +00:00
var prevLineEntry = ( theLine > 0 && rep . lines . atIndex ( theLine - 1 ) ) ;
var prevLineBlank = ( prevLineEntry && prevLineEntry . text . length == prevLineEntry . lineMarker ) ;
if ( thisLineListType )
{
2011-03-26 13:10:41 +00:00
// this line is a list
2011-07-07 17:59:34 +00:00
if ( prevLineBlank && ! prevLineListType )
{
// previous line is blank, remove it
performDocumentReplaceRange ( [ theLine - 1 , prevLineEntry . text . length ] , [ theLine , 0 ] , '' ) ;
}
else
{
2011-03-26 13:10:41 +00:00
// delistify
2011-07-07 17:59:34 +00:00
performDocumentReplaceRange ( [ theLine , 0 ] , [ theLine , lineEntry . lineMarker ] , '' ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
else if ( theLine > 0 )
{
2011-03-26 13:10:41 +00:00
// remove newline
2011-07-07 17:59:34 +00:00
performDocumentReplaceRange ( [ theLine - 1 , prevLineEntry . text . length ] , [ theLine , 0 ] , '' ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
}
}
else
{
performDocumentReplaceSelection ( '' ) ;
}
2011-03-26 13:10:41 +00:00
}
}
2012-01-15 17:20:20 +00: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 ( ) ;
if ( line != - 1 && renumberList ( line + 1 ) == null )
{
renumberList ( line ) ;
}
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function isWordChar ( c )
{
return ! ! REGEX _WORDCHAR . exec ( c ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function isSpaceChar ( c )
{
return ! ! REGEX _SPACE . exec ( c ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function moveByWordInLine ( lineText , initialIndex , forwardNotBack )
{
2011-03-26 13:10:41 +00:00
var i = initialIndex ;
2011-07-07 17:59:34 +00:00
function nextChar ( )
{
2011-03-26 13:10:41 +00:00
if ( forwardNotBack ) return lineText . charAt ( i ) ;
2011-07-07 17:59:34 +00:00
else return lineText . charAt ( i - 1 ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function advance ( )
{
if ( forwardNotBack ) i ++ ;
else i -- ;
}
function isDone ( )
{
2011-03-26 13:10:41 +00: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).
2011-07-07 17:59:34 +00:00
if ( browser . windows && forwardNotBack )
{
while ( ( ! isDone ( ) ) && isWordChar ( nextChar ( ) ) )
{
advance ( ) ;
}
while ( ( ! isDone ( ) ) && ! isWordChar ( nextChar ( ) ) )
{
advance ( ) ;
}
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
while ( ( ! isDone ( ) ) && ! isWordChar ( nextChar ( ) ) )
{
advance ( ) ;
}
while ( ( ! isDone ( ) ) && isWordChar ( nextChar ( ) ) )
{
advance ( ) ;
}
2011-03-26 13:10:41 +00:00
}
return i ;
}
2011-07-07 17:59:34 +00:00
function handleKeyEvent ( evt )
{
2011-03-27 10:46:45 +00:00
// if (DEBUG && window.DONT_INCORP) return;
2011-07-07 17:59:34 +00:00
if ( ! isEditable ) return ;
2011-03-26 13:10:41 +00:00
var type = evt . type ;
var charCode = evt . charCode ;
var keyCode = evt . keyCode ;
var which = evt . which ;
//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 17:59:34 +00:00
var isModKey = ( ( ! charCode ) && ( ( type == "keyup" ) || ( type == "keydown" ) ) && ( keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 20 || keyCode == 224 || keyCode == 91 ) ) ;
2011-03-26 13:10:41 +00:00
if ( isModKey ) return ;
var specialHandled = false ;
2011-07-07 17:59:34 +00:00
var isTypeForSpecialKey = ( ( browser . msie || browser . safari ) ? ( type == "keydown" ) : ( type == "keypress" ) ) ;
2011-03-26 13:10:41 +00:00
var isTypeForCmdKey = ( ( browser . msie || browser . safari ) ? ( type == "keydown" ) : ( type == "keypress" ) ) ;
var stopped = false ;
2011-07-07 17:59:34 +00:00
inCallStack ( "handleKeyEvent" , function ( )
{
if ( type == "keypress" || ( isTypeForSpecialKey && keyCode == 13 /*return*/ ) )
{
// in IE, special keys don't send keypress, the keydown does the action
if ( ! outsideKeyPress ( evt ) )
{
evt . preventDefault ( ) ;
stopped = true ;
}
}
else if ( type == "keydown" )
{
outsideKeyDown ( evt ) ;
}
if ( ! stopped )
{
if ( isTypeForSpecialKey && keyCode == 8 )
{
// "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 13:10:41 +00:00
doDeleteKey ( evt ) ;
specialHandled = true ;
2011-07-07 17:59:34 +00:00
}
if ( ( ! specialHandled ) && isTypeForSpecialKey && keyCode == 13 )
{
// return key, handle specially;
// note that in mozilla we need to do an incorporation for proper return behavior anyway.
fastIncorp ( 4 ) ;
evt . preventDefault ( ) ;
doReturnKey ( ) ;
//scrollSelectionIntoView();
scheduler . setTimeout ( function ( )
{
outerWin . scrollBy ( - 100 , 0 ) ;
} , 0 ) ;
specialHandled = true ;
}
if ( ( ! specialHandled ) && isTypeForSpecialKey && keyCode == 9 && ! ( evt . metaKey || evt . ctrlKey ) )
{
// tab
fastIncorp ( 5 ) ;
evt . preventDefault ( ) ;
2011-03-26 13:10:41 +00:00
doTabKey ( evt . shiftKey ) ;
2011-07-07 17:59:34 +00:00
//scrollSelectionIntoView();
specialHandled = true ;
}
2012-01-17 16:50:35 +00:00
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "z" && ( evt . metaKey || evt . ctrlKey ) && ! evt . altKey )
2011-07-07 17:59:34 +00:00
{
// cmd-Z (undo)
fastIncorp ( 6 ) ;
evt . preventDefault ( ) ;
if ( evt . shiftKey )
{
doUndoRedo ( "redo" ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
doUndoRedo ( "undo" ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
specialHandled = true ;
}
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "y" && ( evt . metaKey || evt . ctrlKey ) )
{
// cmd-Y (redo)
fastIncorp ( 10 ) ;
evt . preventDefault ( ) ;
doUndoRedo ( "redo" ) ;
specialHandled = true ;
}
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "b" && ( evt . metaKey || evt . ctrlKey ) )
{
// cmd-B (bold)
fastIncorp ( 13 ) ;
evt . preventDefault ( ) ;
2011-03-26 13:10:41 +00:00
toggleAttributeOnSelection ( 'bold' ) ;
2011-07-07 17:59:34 +00:00
specialHandled = true ;
}
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "i" && ( evt . metaKey || evt . ctrlKey ) )
{
// cmd-I (italic)
fastIncorp ( 14 ) ;
evt . preventDefault ( ) ;
2011-03-26 13:10:41 +00:00
toggleAttributeOnSelection ( 'italic' ) ;
2011-07-07 17:59:34 +00:00
specialHandled = true ;
}
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "u" && ( evt . metaKey || evt . ctrlKey ) )
{
// cmd-U (underline)
fastIncorp ( 15 ) ;
evt . preventDefault ( ) ;
2011-03-26 13:10:41 +00:00
toggleAttributeOnSelection ( 'underline' ) ;
2011-07-07 17:59:34 +00:00
specialHandled = true ;
}
if ( ( ! specialHandled ) && isTypeForCmdKey && String . fromCharCode ( which ) . toLowerCase ( ) == "h" && ( evt . ctrlKey ) )
{
// cmd-H (backspace)
fastIncorp ( 20 ) ;
evt . preventDefault ( ) ;
2011-03-26 13:10:41 +00:00
doDeleteKey ( ) ;
2011-07-07 17:59:34 +00:00
specialHandled = true ;
}
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
if ( mozillaFakeArrows && mozillaFakeArrows . handleKeyEvent ( evt ) )
{
evt . preventDefault ( ) ;
specialHandled = true ;
}
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( type == "keydown" )
{
idleWorkTimer . atLeast ( 500 ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else if ( type == "keypress" )
{
if ( ( ! specialHandled ) && parenModule . shouldNormalizeOnChar ( charCode ) )
{
idleWorkTimer . atMost ( 0 ) ;
}
else
{
idleWorkTimer . atLeast ( 500 ) ;
}
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else if ( type == "keyup" )
{
var wait = 200 ;
idleWorkTimer . atLeast ( wait ) ;
idleWorkTimer . atMost ( wait ) ;
2011-03-26 13:10:41 +00:00
}
// Is part of multi-keystroke international character on Firefox Mac
2011-07-07 17:59:34 +00:00
var isFirefoxHalfCharacter = ( browser . mozilla && evt . altKey && charCode == 0 && keyCode == 0 ) ;
2011-03-26 13:10:41 +00:00
// Is part of multi-keystroke international character on Safari Mac
2011-07-07 17:59:34 +00:00
var isSafariHalfCharacter = ( browser . safari && evt . altKey && keyCode == 229 ) ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( ( ! specialHandled ) && ( ! thisKeyDoesntTriggerNormalize ) && ( ! inInternationalComposition ) )
{
if ( type != "keyup" || ! incorpIfQuick ( ) )
{
observeChangesAroundSelection ( ) ;
}
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( type == "keyup" )
{
thisKeyDoesntTriggerNormalize = false ;
2011-03-26 13:10:41 +00:00
}
} ) ;
}
var thisKeyDoesntTriggerNormalize = false ;
2011-07-07 17:59:34 +00:00
function doUndoRedo ( which )
{
2011-03-26 13:10:41 +00:00
// precond: normalized DOM
2011-07-07 17:59:34 +00:00
if ( undoModule . enabled )
{
2011-03-26 13:10:41 +00:00
var whichMethod ;
if ( which == "undo" ) whichMethod = 'performUndo' ;
if ( which == "redo" ) whichMethod = 'performRedo' ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
}
}
editorInfo . ace _doUndoRedo = doUndoRedo ;
2011-07-07 17:59:34 +00:00
function updateBrowserSelectionFromRep ( )
{
2011-03-26 13:10:41 +00:00
// requires normalized DOM!
2011-07-07 17:59:34 +00:00
var selStart = rep . selStart ,
selEnd = rep . selEnd ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
if ( ! ( selStart && selEnd ) )
{
2011-03-26 13:10:41 +00:00
setSelection ( null ) ;
return ;
}
2011-07-07 17:59:34 +00:00
var mozillaCaretHack = ( false && browser . mozilla && selStart && selEnd && selStart [ 0 ] == selEnd [ 0 ] && selStart [ 1 ] == rep . lines . atIndex ( selStart [ 0 ] ) . lineMarker && selEnd [ 1 ] == rep . lines . atIndex ( selEnd [ 0 ] ) . lineMarker && setupMozillaCaretHack ( selStart [ 0 ] ) ) ;
2011-03-26 13:10:41 +00:00
var selection = { } ;
var ss = [ selStart [ 0 ] , selStart [ 1 ] ] ;
if ( mozillaCaretHack ) ss [ 1 ] += 1 ;
selection . startPoint = getPointForLineAndChar ( ss ) ;
var se = [ selEnd [ 0 ] , selEnd [ 1 ] ] ;
if ( mozillaCaretHack ) se [ 1 ] += 1 ;
selection . endPoint = getPointForLineAndChar ( se ) ;
2011-07-07 17:59:34 +00:00
selection . focusAtStart = ! ! rep . selFocusAtStart ;
2011-03-26 13:10:41 +00:00
setSelection ( selection ) ;
2011-07-07 17:59:34 +00:00
if ( mozillaCaretHack )
{
2011-03-26 13:10:41 +00:00
mozillaCaretHack . unhack ( ) ;
}
}
2011-07-07 17:59:34 +00:00
function getRepHTML ( )
{
return map ( rep . lines . slice ( ) , function ( entry )
{
2011-03-26 13:10:41 +00:00
var text = entry . text ;
var content ;
2011-07-07 17:59:34 +00:00
if ( text . length == 0 )
{
content = '<span style="color: #aaa">--</span>' ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
content = htmlPrettyEscape ( text ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
return '<div><code>' + content + '</div></code>' ;
2011-03-26 13:10:41 +00:00
} ) . join ( '' ) ;
}
2011-07-07 17:59:34 +00:00
function nodeMaxIndex ( nd )
{
2011-03-26 13:10:41 +00:00
if ( isNodeText ( nd ) ) return nd . nodeValue . length ;
else return 1 ;
}
2011-07-07 17:59:34 +00:00
function hasIESelection ( )
{
2011-03-26 13:10:41 +00:00
var browserSelection ;
2011-07-07 17:59:34 +00:00
try
{
browserSelection = doc . selection ;
}
catch ( e )
{ }
if ( ! browserSelection ) return false ;
2011-03-26 13:10:41 +00:00
var origSelectionRange ;
2011-07-07 17:59:34 +00:00
try
{
origSelectionRange = browserSelection . createRange ( ) ;
}
catch ( e )
{ }
if ( ! origSelectionRange ) return false ;
2011-03-26 13:10:41 +00:00
return true ;
}
2011-07-07 17:59:34 +00:00
function getSelection ( )
{
2011-03-26 13:10:41 +00: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.
2011-07-07 17:59:34 +00:00
if ( browser . msie )
{
2011-03-26 13:10:41 +00:00
var browserSelection ;
2011-07-07 17:59:34 +00:00
try
{
browserSelection = doc . selection ;
}
catch ( e )
{ }
if ( ! browserSelection ) return null ;
2011-03-26 13:10:41 +00:00
var origSelectionRange ;
2011-07-07 17:59:34 +00:00
try
{
origSelectionRange = browserSelection . createRange ( ) ;
}
catch ( e )
{ }
if ( ! origSelectionRange ) return null ;
2011-03-26 13:10:41 +00:00
var selectionParent = origSelectionRange . parentElement ( ) ;
if ( selectionParent . ownerDocument != doc ) return null ;
2011-07-07 17:59:34 +00:00
function newRange ( )
{
return doc . body . createTextRange ( ) ;
}
function rangeForElementNode ( nd )
{
var rng = newRange ( ) ;
// doesn't work on text nodes
rng . moveToElementText ( nd ) ;
return rng ;
}
function pointFromCollapsedRange ( rng )
{
var parNode = rng . parentElement ( ) ;
var elemBelow = - 1 ;
var elemAbove = parNode . childNodes . length ;
var rangeWithin = rangeForElementNode ( parNode ) ;
if ( rng . compareEndPoints ( "StartToStart" , rangeWithin ) == 0 )
{
return {
node : parNode ,
index : 0 ,
maxIndex : 1
} ;
}
else if ( rng . compareEndPoints ( "EndToEnd" , rangeWithin ) == 0 )
{
if ( isBlockElement ( parNode ) && parNode . nextSibling )
{
// caret after block is not consistent across browsers
// (same line vs next) so put caret before next node
return {
node : parNode . nextSibling ,
index : 0 ,
maxIndex : 1
} ;
}
return {
node : parNode ,
index : 1 ,
maxIndex : 1
} ;
}
else if ( parNode . childNodes . length == 0 )
{
return {
node : parNode ,
index : 0 ,
maxIndex : 1
} ;
}
for ( var i = 0 ; i < parNode . childNodes . length ; i ++ )
{
var n = parNode . childNodes . item ( i ) ;
if ( ! isNodeText ( n ) )
{
var nodeRange = rangeForElementNode ( n ) ;
var startComp = rng . compareEndPoints ( "StartToStart" , nodeRange ) ;
var endComp = rng . compareEndPoints ( "EndToEnd" , nodeRange ) ;
if ( startComp >= 0 && endComp <= 0 )
{
var index = 0 ;
if ( startComp > 0 )
{
index = 1 ;
}
return {
node : n ,
index : index ,
maxIndex : 1
} ;
}
else if ( endComp > 0 )
{
if ( i > elemBelow )
{
elemBelow = i ;
rangeWithin . setEndPoint ( "StartToEnd" , nodeRange ) ;
}
}
else if ( startComp < 0 )
{
if ( i < elemAbove )
{
elemAbove = i ;
rangeWithin . setEndPoint ( "EndToStart" , nodeRange ) ;
}
}
}
}
if ( ( elemAbove - elemBelow ) == 1 )
{
if ( elemBelow >= 0 )
{
return {
node : parNode . childNodes . item ( elemBelow ) ,
index : 1 ,
maxIndex : 1
} ;
}
else
{
return {
node : parNode . childNodes . item ( elemAbove ) ,
index : 0 ,
maxIndex : 1
} ;
}
}
var idx = 0 ;
var r = rng . duplicate ( ) ;
// infinite stateful binary search! call function for values 0 to inf,
// expecting the answer to be about 40. return index of smallest
// true value.
var indexIntoRange = binarySearchInfinite ( 40 , function ( i )
{
// the search algorithm whips the caret back and forth,
// though it has to be moved relatively and may hit
// the end of the buffer
var delta = i - idx ;
var moved = Math . abs ( r . move ( "character" , - delta ) ) ;
// next line is work-around for fact that when moving left, the beginning
// of a text node is considered to be after the start of the parent element:
if ( r . move ( "character" , - 1 ) ) r . move ( "character" , 1 ) ;
if ( delta < 0 ) idx -= moved ;
else idx += moved ;
return ( r . compareEndPoints ( "StartToStart" , rangeWithin ) <= 0 ) ;
} ) ;
// iterate over consecutive text nodes, point is in one of them
var textNode = elemBelow + 1 ;
var indexLeft = indexIntoRange ;
while ( textNode < elemAbove )
{
var tn = parNode . childNodes . item ( textNode ) ;
if ( indexLeft <= tn . nodeValue . length )
{
return {
node : tn ,
index : indexLeft ,
maxIndex : tn . nodeValue . length
} ;
}
indexLeft -= tn . nodeValue . length ;
textNode ++ ;
}
var tn = parNode . childNodes . item ( textNode - 1 ) ;
return {
node : tn ,
index : tn . nodeValue . length ,
maxIndex : tn . nodeValue . length
} ;
2011-03-26 13:10:41 +00:00
}
var selection = { } ;
2011-07-07 17:59:34 +00:00
if ( origSelectionRange . compareEndPoints ( "StartToEnd" , origSelectionRange ) == 0 )
{
// collapsed
var pnt = pointFromCollapsedRange ( origSelectionRange ) ;
selection . startPoint = pnt ;
selection . endPoint = {
node : pnt . node ,
index : pnt . index ,
maxIndex : pnt . maxIndex
} ;
}
else
{
var start = origSelectionRange . duplicate ( ) ;
start . collapse ( true ) ;
var end = origSelectionRange . duplicate ( ) ;
end . collapse ( false ) ;
selection . startPoint = pointFromCollapsedRange ( start ) ;
selection . endPoint = pointFromCollapsedRange ( end ) ;
/ * i f ( ( ! s e l e c t i o n . s t a r t P o i n t . n o d e . i s T e x t ) & & ( ! s e l e c t i o n . e n d P o i n t . n o d e . i s T e x t ) ) {
2011-03-26 13:10:41 +00:00
console . log ( selection . startPoint . node . uniqueId ( ) + "," +
selection . startPoint . index + " / " +
selection . endPoint . node . uniqueId ( ) + "," +
selection . endPoint . index ) ;
} * /
}
return selection ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
// non-IE browser
var browserSelection = window . getSelection ( ) ;
2011-07-07 17:59:34 +00: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
} ;
}
else if ( childCount == 0 )
{
return {
node : n ,
index : 0 ,
maxIndex : 1
} ;
}
// treat point between two nodes as BEFORE the second (rather than after the first)
// if possible; this way point at end of a line block-element is treated as
// at beginning of next line
else if ( offset == childCount )
{
var nd = n . childNodes . item ( childCount - 1 ) ;
var max = nodeMaxIndex ( nd ) ;
return {
node : nd ,
index : max ,
maxIndex : max
} ;
}
else
{
var nd = n . childNodes . item ( offset ) ;
var max = nodeMaxIndex ( nd ) ;
return {
node : nd ,
index : 0 ,
maxIndex : max
} ;
}
}
var selection = { } ;
selection . startPoint = pointFromRangeBound ( range . startContainer , range . startOffset ) ;
selection . endPoint = pointFromRangeBound ( range . endContainer , range . endOffset ) ;
selection . focusAtStart = ( ( ( range . startContainer != range . endContainer ) || ( range . startOffset != range . endOffset ) ) && browserSelection . anchorNode && ( browserSelection . anchorNode == range . endContainer ) && ( browserSelection . anchorOffset == range . endOffset ) ) ;
return selection ;
2011-03-26 13:10:41 +00:00
}
else return null ;
}
}
2011-07-07 17:59:34 +00:00
function setSelection ( selection )
{
function copyPoint ( pt )
{
return {
node : pt . node ,
index : pt . index ,
maxIndex : pt . maxIndex
} ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( browser . msie )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function moveToElementText ( s , n )
{
while ( n . firstChild && ! isNodeText ( n . firstChild ) )
{
2011-03-26 13:10:41 +00:00
n = n . firstChild ;
}
s . moveToElementText ( n ) ;
}
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
// work around for issue that caret at beginning of line
// somehow ends up at end of previous line
2011-07-07 17:59:34 +00:00
if ( s . move ( 'character' , 1 ) )
{
2011-03-26 13:10:41 +00:00
s . move ( 'character' , - 1 ) ;
}
2011-07-07 17:59:34 +00: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 ) ;
}
else if ( point . index == 0 )
{
setCollapsedBefore ( s , n ) ;
}
else
{
setCollapsedAfter ( s , n ) ;
}
return s ;
}
if ( selection )
{
if ( ! hasIESelection ( ) )
{
return ; // don't steal focus
}
var startPoint = copyPoint ( selection . startPoint ) ;
var endPoint = copyPoint ( selection . endPoint ) ;
// fix issue where selection can't be extended past end of line
// with shift-rightarrow or shift-downarrow
if ( endPoint . index == endPoint . maxIndex && endPoint . node . nextSibling )
{
endPoint . node = endPoint . node . nextSibling ;
endPoint . index = 0 ;
endPoint . maxIndex = nodeMaxIndex ( endPoint . node ) ;
}
var range = getPointRange ( startPoint ) ;
range . setEndPoint ( "EndToEnd" , getPointRange ( endPoint ) ) ;
// setting the selection in IE causes everything to scroll
// so that the selection is visible. if setting the selection
// definitely accomplishes nothing, don't do it.
function isEqualToDocumentSelection ( rng )
{
var browserSelection ;
try
{
browserSelection = doc . selection ;
}
catch ( e )
{ }
if ( ! browserSelection ) return false ;
var rng2 = browserSelection . createRange ( ) ;
if ( rng2 . parentElement ( ) . ownerDocument != doc ) return false ;
if ( rng . compareEndPoints ( "StartToStart" , rng2 ) !== 0 ) return false ;
if ( rng . compareEndPoints ( "EndToEnd" , rng2 ) !== 0 ) return false ;
return true ;
}
if ( ! isEqualToDocumentSelection ( range ) )
{
//dmesg(toSource(selection));
//dmesg(escapeHTML(doc.body.innerHTML));
range . select ( ) ;
}
}
else
{
try
{
doc . selection . empty ( ) ;
}
catch ( e )
{ }
}
}
else
{
2011-03-26 13:10:41 +00:00
// non-IE browser
var isCollapsed ;
2011-07-07 17:59:34 +00: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)) {
if ( p . index == 0 )
{
p . node = p . node . firstChild ;
p . maxIndex = nodeMaxIndex ( p . node ) ;
}
else if ( p . index == p . maxIndex )
{
p . node = p . node . lastChild ;
p . maxIndex = nodeMaxIndex ( p . node ) ;
p . index = p . maxIndex ;
}
else break ;
}
}
// now fix problem where cursor at end of text node at end of span-like element
// with background doesn't seem to show up...
if ( isNodeText ( p . node ) && p . index == p . maxIndex )
{
var n = p . node ;
while ( ( ! n . nextSibling ) && ( n != root ) && ( n . parentNode != root ) )
{
n = n . parentNode ;
}
if ( n . nextSibling && ( ! ( ( typeof n . nextSibling . tagName ) == "string" && n . nextSibling . tagName . toLowerCase ( ) == "br" ) ) && ( n != p . node ) && ( n != root ) && ( n . parentNode != root ) )
{
// found a parent, go to next node and dive in
p . node = n . nextSibling ;
p . maxIndex = nodeMaxIndex ( p . node ) ;
p . index = 0 ;
diveDeep ( ) ;
}
}
// try to make sure insertion point is styled;
2011-03-26 13:10:41 +00:00
// also fixes other FF problems
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
var browserSelection = window . getSelection ( ) ;
2011-07-07 17:59:34 +00: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 ) ;
//console.trace();
//console.log(htmlPrettyEscape(rep.alltext));
//console.log("%o %o", rep.selStart, rep.selEnd);
//console.log("%o %d", start.container, start.offset);
browserSelection . extend ( start . container , start . offset ) ;
}
else
{
var range = doc . createRange ( ) ;
range . setStart ( start . container , start . offset ) ;
range . setEnd ( end . container , end . offset ) ;
browserSelection . removeAllRanges ( ) ;
browserSelection . addRange ( range ) ;
}
}
}
}
}
function childIndex ( n )
{
2011-03-26 13:10:41 +00:00
var idx = 0 ;
2011-07-07 17:59:34 +00:00
while ( n . previousSibling )
{
2011-03-26 13:10:41 +00:00
idx ++ ;
n = n . previousSibling ;
}
return idx ;
}
2011-07-07 17:59:34 +00:00
function fixView ( )
{
2011-03-26 13:10:41 +00:00
// calling this method repeatedly should be fast
2011-07-07 17:59:34 +00:00
if ( getInnerWidth ( ) == 0 || getInnerHeight ( ) == 0 )
{
2011-03-26 13:10:41 +00:00
return ;
}
2011-07-07 17:59:34 +00:00
function setIfNecessary ( obj , prop , value )
{
if ( obj [ prop ] != value )
{
obj [ prop ] = value ;
2011-03-26 13:10:41 +00:00
}
}
var lineNumberWidth = sideDiv . firstChild . offsetWidth ;
var newSideDivWidth = lineNumberWidth + LINE _NUMBER _PADDING _LEFT ;
if ( newSideDivWidth < MIN _LINEDIV _WIDTH ) newSideDivWidth = MIN _LINEDIV _WIDTH ;
iframePadLeft = EDIT _BODY _PADDING _LEFT ;
if ( hasLineNumbers ) iframePadLeft += newSideDivWidth + LINE _NUMBER _PADDING _RIGHT ;
2011-07-07 17:59:34 +00:00
setIfNecessary ( iframe . style , "left" , iframePadLeft + "px" ) ;
setIfNecessary ( sideDiv . style , "width" , newSideDivWidth + "px" ) ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
for ( var i = 0 ; i < 2 ; i ++ )
{
2011-03-26 13:10:41 +00:00
var newHeight = root . clientHeight ;
var newWidth = ( browser . msie ? root . createTextRange ( ) . boundingWidth : root . clientWidth ) ;
var viewHeight = getInnerHeight ( ) - iframePadBottom - iframePadTop ;
var viewWidth = getInnerWidth ( ) - iframePadLeft - iframePadRight ;
2011-07-07 17:59:34 +00:00
if ( newHeight < viewHeight )
{
newHeight = viewHeight ;
if ( browser . msie ) setIfNecessary ( outerWin . document . documentElement . style , 'overflowY' , 'auto' ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
if ( browser . msie ) setIfNecessary ( outerWin . document . documentElement . style , 'overflowY' , 'scroll' ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
if ( doesWrap )
{
newWidth = viewWidth ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
if ( newWidth < viewWidth ) newWidth = viewWidth ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
setIfNecessary ( iframe . style , "height" , newHeight + "px" ) ;
setIfNecessary ( iframe . style , "width" , newWidth + "px" ) ;
setIfNecessary ( sideDiv . style , "height" , newHeight + "px" ) ;
}
if ( browser . mozilla )
{
if ( ! doesWrap )
{
// the body:display:table-cell hack makes mozilla do scrolling
// correctly by shrinking the <body> to fit around its content,
// but mozilla won't act on clicks below the body. We keep the
// style.height property set to the viewport height (editor height
// not including scrollbar), so it will never shrink so that part of
// the editor isn't clickable.
var body = root ;
var styleHeight = viewHeight + "px" ;
setIfNecessary ( body . style , "height" , styleHeight ) ;
}
else
{
setIfNecessary ( root . style , "height" , "" ) ;
2011-03-26 13:10:41 +00:00
}
}
// if near edge, scroll to edge
var scrollX = getScrollX ( ) ;
var scrollY = getScrollY ( ) ;
var win = outerWin ;
var r = 20 ;
enforceEditability ( ) ;
addClass ( sideDiv , 'sidedivdelayed' ) ;
}
2011-07-07 17:59:34 +00:00
function getScrollXY ( )
{
2011-03-26 13:10:41 +00:00
var win = outerWin ;
var odoc = outerWin . document ;
2011-07-07 17:59:34 +00:00
if ( typeof ( win . pageYOffset ) == "number" )
{
return {
x : win . pageXOffset ,
y : win . pageYOffset
} ;
2011-03-26 13:10:41 +00:00
}
var docel = odoc . documentElement ;
2011-07-07 17:59:34 +00:00
if ( docel && typeof ( docel . scrollTop ) == "number" )
{
return {
x : docel . scrollLeft ,
y : docel . scrollTop
} ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
function getScrollX ( )
{
2011-03-26 13:10:41 +00:00
return getScrollXY ( ) . x ;
}
2011-07-07 17:59:34 +00:00
function getScrollY ( )
{
2011-03-26 13:10:41 +00:00
return getScrollXY ( ) . y ;
}
2011-07-07 17:59:34 +00:00
function setScrollX ( x )
{
2011-03-26 13:10:41 +00:00
outerWin . scrollTo ( x , getScrollY ( ) ) ;
}
2011-07-07 17:59:34 +00:00
function setScrollY ( y )
{
2011-03-26 13:10:41 +00:00
outerWin . scrollTo ( getScrollX ( ) , y ) ;
}
2011-07-07 17:59:34 +00:00
function setScrollXY ( x , y )
{
2011-03-26 13:10:41 +00:00
outerWin . scrollTo ( x , y ) ;
}
var _teardownActions = [ ] ;
2011-07-07 17:59:34 +00:00
function teardown ( )
{
forEach ( _teardownActions , function ( a )
{
a ( ) ;
} ) ;
2011-03-26 13:10:41 +00:00
}
bindEventHandler ( window , "load" , setup ) ;
2011-07-07 17:59:34 +00:00
function setDesignMode ( newVal )
{
try
{
function setIfNecessary ( target , prop , val )
{
if ( String ( target [ prop ] ) . toLowerCase ( ) != val )
{
target [ prop ] = val ;
return true ;
}
return false ;
}
if ( browser . msie || browser . safari )
{
setIfNecessary ( root , 'contentEditable' , ( newVal ? 'true' : 'false' ) ) ;
}
else
{
var wasSet = setIfNecessary ( doc , 'designMode' , ( newVal ? 'on' : 'off' ) ) ;
if ( wasSet && newVal && browser . opera )
{
// turning on designMode clears event handlers
bindTheEventHandlers ( ) ;
}
2011-03-26 13:10:41 +00:00
}
return true ;
}
2011-07-07 17:59:34 +00:00
catch ( e )
{
2011-03-26 13:10:41 +00:00
return false ;
}
}
var iePastedLines = null ;
2011-07-07 17:59:34 +00:00
function handleIEPaste ( evt )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
if ( clipText && doc . selection )
{
2011-03-26 13:10:41 +00: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);
}
}
var inInternationalComposition = false ;
2011-07-07 17:59:34 +00:00
function handleCompositionEvent ( evt )
{
2011-03-26 13:10:41 +00:00
// international input events, fired in FF3, at least; allow e.g. Japanese input
2011-07-07 17:59:34 +00:00
if ( evt . type == "compositionstart" )
{
2011-03-26 13:10:41 +00:00
inInternationalComposition = true ;
}
2011-07-07 17:59:34 +00:00
else if ( evt . type == "compositionend" )
{
2011-03-26 13:10:41 +00:00
inInternationalComposition = false ;
}
}
2011-07-07 17:59:34 +00:00
function bindTheEventHandlers ( )
{
2011-03-26 13:10:41 +00:00
bindEventHandler ( window , "unload" , teardown ) ;
bindEventHandler ( document , "keydown" , handleKeyEvent ) ;
bindEventHandler ( document , "keypress" , handleKeyEvent ) ;
bindEventHandler ( document , "keyup" , handleKeyEvent ) ;
bindEventHandler ( document , "click" , handleClick ) ;
bindEventHandler ( root , "blur" , handleBlur ) ;
2011-07-07 17:59:34 +00:00
if ( browser . msie )
{
2011-03-26 13:10:41 +00:00
bindEventHandler ( document , "click" , handleIEOuterClick ) ;
}
if ( browser . msie ) bindEventHandler ( root , "paste" , handleIEPaste ) ;
2011-07-07 17:59:34 +00:00
if ( ( ! browser . msie ) && document . documentElement )
{
2011-03-26 13:10:41 +00:00
bindEventHandler ( document . documentElement , "compositionstart" , handleCompositionEvent ) ;
bindEventHandler ( document . documentElement , "compositionend" , handleCompositionEvent ) ;
}
}
2011-07-07 17:59:34 +00:00
function handleIEOuterClick ( evt )
{
if ( ( evt . target . tagName || '' ) . toLowerCase ( ) != "html" )
{
2011-03-26 13:10:41 +00:00
return ;
}
2011-07-07 17:59:34 +00:00
if ( ! ( evt . pageY > root . clientHeight ) )
{
2011-03-26 13:10:41 +00:00
return ;
}
// click below the body
2011-07-07 17:59:34 +00:00
inCallStack ( "handleOuterClick" , function ( )
{
2011-03-26 13:10:41 +00:00
// put caret at bottom of doc
fastIncorp ( 11 ) ;
2011-07-07 17:59:34 +00: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 13:10:41 +00:00
}
} ) ;
}
2011-07-07 17:59:34 +00:00
function getClassArray ( elem , optFilter )
{
2011-03-26 13:10:41 +00:00
var bodyClasses = [ ] ;
2011-07-07 17:59:34 +00:00
( elem . className || '' ) . replace ( /\S+/g , function ( c )
{
if ( ( ! optFilter ) || ( optFilter ( c ) ) )
{
bodyClasses . push ( c ) ;
2011-03-26 13:10:41 +00:00
}
} ) ;
return bodyClasses ;
}
2011-07-07 17:59:34 +00:00
function setClassArray ( elem , array )
{
2011-03-26 13:10:41 +00:00
elem . className = array . join ( ' ' ) ;
}
2011-07-07 17:59:34 +00:00
function addClass ( elem , className )
{
2011-03-26 13:10:41 +00:00
var seen = false ;
2011-07-07 17:59:34 +00:00
var cc = getClassArray ( elem , function ( c )
{
if ( c == className ) seen = true ;
return true ;
} ) ;
if ( ! seen )
{
2011-03-26 13:10:41 +00:00
cc . push ( className ) ;
setClassArray ( elem , cc ) ;
}
}
2011-07-07 17:59:34 +00:00
function removeClass ( elem , className )
{
2011-03-26 13:10:41 +00:00
var seen = false ;
2011-07-07 17:59:34 +00:00
var cc = getClassArray ( elem , function ( c )
{
if ( c == className )
{
seen = true ;
return false ;
}
return true ;
} ) ;
if ( seen )
{
2011-03-26 13:10:41 +00:00
setClassArray ( elem , cc ) ;
}
}
2011-07-07 17:59:34 +00:00
function setClassPresence ( elem , className , present )
{
2011-03-26 13:10:41 +00:00
if ( present ) addClass ( elem , className ) ;
else removeClass ( elem , className ) ;
}
2011-07-07 17:59:34 +00:00
function setup ( )
{
2011-03-26 13:10:41 +00:00
doc = document ; // defined as a var in scope outside
2011-07-07 17:59:34 +00:00
inCallStack ( "setup" , function ( )
{
2011-03-26 13:10:41 +00:00
var body = doc . getElementById ( "innerdocbody" ) ;
root = body ; // defined as a var in scope outside
if ( browser . mozilla ) addClass ( root , "mozilla" ) ;
if ( browser . safari ) addClass ( root , "safari" ) ;
if ( browser . msie ) addClass ( root , "msie" ) ;
2011-07-07 17:59:34 +00:00
if ( browser . msie )
{
// cache CSS background images
try
{
doc . execCommand ( "BackgroundImageCache" , false , true ) ;
}
catch ( e )
{ /* throws an error in some IE 6 but not others! */
}
2011-03-26 13:10:41 +00:00
}
setClassPresence ( root , "authorColors" , true ) ;
setClassPresence ( root , "doesWrap" , doesWrap ) ;
initDynamicCSS ( ) ;
enforceEditability ( ) ;
// set up dom and rep
while ( root . firstChild ) root . removeChild ( root . firstChild ) ;
var oneEntry = createDomLineEntry ( "" ) ;
doRepLineSplice ( 0 , rep . lines . length ( ) , [ oneEntry ] ) ;
insertDomLines ( null , [ oneEntry . domInfo ] , null ) ;
rep . alines = Changeset . splitAttributionLines (
2011-07-07 17:59:34 +00:00
Changeset . makeAttribution ( "\n" ) , "\n" ) ;
2011-03-26 13:10:41 +00:00
bindTheEventHandlers ( ) ;
} ) ;
2011-07-07 17:59:34 +00:00
scheduler . setTimeout ( function ( )
{
2011-03-26 13:10:41 +00:00
parent . readyFunc ( ) ; // defined in code that sets up the inner iframe
} , 0 ) ;
isSetUp = true ;
}
2011-07-07 17:59:34 +00:00
function focus ( )
{
2011-03-26 13:10:41 +00:00
window . focus ( ) ;
}
2011-07-07 17:59:34 +00:00
function handleBlur ( evt )
{
if ( browser . msie )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function bindEventHandler ( target , type , func )
{
2011-03-26 13:10:41 +00:00
var handler ;
2011-07-07 17:59:34 +00:00
if ( ( typeof func . _wrapper ) != "function" )
{
func . _wrapper = function ( event )
{
func ( fixEvent ( event || window . event || { } ) ) ;
2011-03-26 13:10:41 +00:00
}
}
var handler = func . _wrapper ;
2011-07-07 17:59:34 +00:00
if ( target . addEventListener ) target . addEventListener ( type , handler , false ) ;
else target . attachEvent ( "on" + type , handler ) ;
_teardownActions . push ( function ( )
{
2011-03-26 13:10:41 +00:00
unbindEventHandler ( target , type , func ) ;
} ) ;
}
2011-07-07 17:59:34 +00:00
function unbindEventHandler ( target , type , func )
{
2011-03-26 13:10:41 +00:00
var handler = func . _wrapper ;
2011-07-07 17:59:34 +00:00
if ( target . removeEventListener ) target . removeEventListener ( type , handler , false ) ;
else target . detachEvent ( "on" + type , handler ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function getSelectionPointX ( point )
{
2011-03-26 13:10:41 +00:00
// doesn't work in wrap-mode
var node = point . node ;
var index = point . index ;
2011-07-07 17:59:34 +00:00
function leftOf ( n )
{
return n . offsetLeft ;
}
function rightOf ( n )
{
return n . offsetLeft + n . offsetWidth ;
}
if ( ! isNodeText ( node ) )
{
2011-03-26 13:10:41 +00:00
if ( index == 0 ) return leftOf ( node ) ;
else return rightOf ( node ) ;
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
for ( n = node . previousSibling ; n && isNodeText ( n ) ; n = n . previousSibling )
charsToLeft += n . nodeValue ;
2011-03-26 13:10:41 +00:00
var leftEdge = ( n ? rightOf ( n ) : leftOf ( node . parentNode ) ) ;
2011-07-07 17:59:34 +00:00
for ( n = node . nextSibling ; n && isNodeText ( n ) ; n = n . nextSibling )
charsToRight += n . nodeValue ;
2011-03-26 13:10:41 +00:00
var rightEdge = ( n ? leftOf ( n ) : rightOf ( node . parentNode ) ) ;
var frac = ( charsToLeft / ( charsToLeft + charsToRight ) ) ;
2011-07-07 17:59:34 +00:00
var pixLoc = leftEdge + frac * ( rightEdge - leftEdge ) ;
2011-03-26 13:10:41 +00:00
return Math . round ( pixLoc ) ;
}
}
2011-07-07 17:59:34 +00:00
function getPageHeight ( )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function getPageWidth ( )
{
2011-03-26 13:10:41 +00: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 17:59:34 +00:00
function getInnerHeight ( )
{
2011-03-26 13:10:41 +00:00
var win = outerWin ;
var odoc = win . document ;
var h ;
if ( browser . opera ) h = win . innerHeight ;
else h = odoc . documentElement . clientHeight ;
if ( h ) return h ;
// deal with case where iframe is hidden, hope that
// style.height of iframe container is set in px
2011-07-07 17:59:34 +00:00
return Number ( editorInfo . frame . parentNode . style . height . replace ( /[^0-9]/g , '' ) || 0 ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
function getInnerWidth ( )
{
2011-03-26 13:10:41 +00:00
var win = outerWin ;
var odoc = win . document ;
return odoc . documentElement . clientWidth ;
}
2011-07-07 17:59:34 +00:00
function scrollNodeVerticallyIntoView ( node )
{
2011-03-26 13:10:41 +00:00
// requires element (non-text) node;
// if node extends above top of viewport or below bottom of viewport (or top of scrollbar),
// scroll it the minimum distance needed to be completely in view.
var win = outerWin ;
var odoc = outerWin . document ;
var distBelowTop = node . offsetTop + iframePadTop - win . scrollY ;
2011-07-07 17:59:34 +00:00
var distAboveBottom = win . scrollY + getInnerHeight ( ) - ( node . offsetTop + iframePadTop + node . offsetHeight ) ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
if ( distBelowTop < 0 )
{
2011-03-26 13:10:41 +00:00
win . scrollBy ( 0 , distBelowTop ) ;
}
2011-07-07 17:59:34 +00:00
else if ( distAboveBottom < 0 )
{
2011-03-26 13:10:41 +00:00
win . scrollBy ( 0 , - distAboveBottom ) ;
}
}
2011-07-07 17:59:34 +00:00
function scrollXHorizontallyIntoView ( pixelX )
{
2011-03-26 13:10:41 +00:00
var win = outerWin ;
var odoc = outerWin . document ;
pixelX += iframePadLeft ;
var distInsideLeft = pixelX - win . scrollX ;
var distInsideRight = win . scrollX + getInnerWidth ( ) - pixelX ;
2011-07-07 17:59:34 +00:00
if ( distInsideLeft < 0 )
{
2011-03-26 13:10:41 +00:00
win . scrollBy ( distInsideLeft , 0 ) ;
}
2011-07-07 17:59:34 +00:00
else if ( distInsideRight < 0 )
{
win . scrollBy ( - distInsideRight + 1 , 0 ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
function scrollSelectionIntoView ( )
{
if ( ! rep . selStart ) return ;
2011-03-26 13:10:41 +00:00
fixView ( ) ;
var focusLine = ( rep . selFocusAtStart ? rep . selStart [ 0 ] : rep . selEnd [ 0 ] ) ;
scrollNodeVerticallyIntoView ( rep . lines . atIndex ( focusLine ) . lineNode ) ;
2011-07-07 17:59:34 +00:00
if ( ! doesWrap )
{
2011-03-26 13:10:41 +00:00
var browserSelection = getSelection ( ) ;
2011-07-07 17:59:34 +00:00
if ( browserSelection )
{
var focusPoint = ( browserSelection . focusAtStart ? browserSelection . startPoint : browserSelection . endPoint ) ;
var selectionPointX = getSelectionPointX ( focusPoint ) ;
scrollXHorizontallyIntoView ( selectionPointX ) ;
fixView ( ) ;
2011-03-26 13:10:41 +00:00
}
}
}
2011-07-07 17:59:34 +00:00
function getLineListType ( lineNum )
{
2011-03-26 13:10:41 +00:00
// get "list" attribute of first char of line
var aline = rep . alines [ lineNum ] ;
2011-07-07 17:59:34 +00:00
if ( aline )
{
2011-03-26 13:10:41 +00:00
var opIter = Changeset . opIterator ( aline ) ;
2011-07-07 17:59:34 +00:00
if ( opIter . hasNext ( ) )
{
2011-03-26 13:10:41 +00:00
return Changeset . opAttributeValue ( opIter . next ( ) , 'list' , rep . apool ) || '' ;
}
}
return '' ;
}
2011-07-07 17:59:34 +00:00
function setLineListType ( lineNum , listType )
{
setLineListTypes ( [
[ lineNum , listType ]
] ) ;
2011-03-26 13:10:41 +00:00
}
2012-01-15 17:20:20 +00:00
function renumberList ( lineNum ) {
//1-check we are in a list
var type = getLineListType ( lineNum ) ;
if ( ! type )
{
return null ;
}
type = /([a-z]+)[12345678]/ . exec ( type ) ;
if ( type [ 1 ] == "indent" )
{
return null ;
}
//2-find the first line of the list
while ( lineNum - 1 >= 0 && ( type = getLineListType ( lineNum - 1 ) ) )
{
type = /([a-z]+)[12345678]/ . exec ( type ) ;
if ( type [ 1 ] == "indent" )
break ;
lineNum -- ;
}
//3-renumber every list item of the same level from the beginning, level 1
//IMPORTANT: never skip a level because there imbrication may be arbitrary
var builder = Changeset . builder ( rep . lines . totalWidth ( ) ) ;
loc = [ 0 , 0 ] ;
function applyNumberList ( line , level )
{
//init
var position = 1 ;
var curLevel = level ;
var listType ;
//loop over the lines
while ( listType = getLineListType ( line ) )
{
//apply new num
listType = /([a-z]+)([12345678])/ . exec ( listType ) ;
curLevel = Number ( listType [ 2 ] ) ;
if ( isNaN ( curLevel ) || listType [ 0 ] == "indent" )
{
return line ;
}
else if ( curLevel == level )
{
buildKeepRange ( builder , loc , ( loc = [ line , 0 ] ) ) ;
buildKeepRange ( builder , loc , ( loc = [ line , 1 ] ) , [
[ 'start' , position ]
] , rep . apool ) ;
position ++ ;
line ++ ;
}
else if ( curLevel < level )
{
return line ; //back to parent
}
else
{
line = applyNumberList ( line , level + 1 ) ; //recursive call
}
}
return line ;
}
applyNumberList ( lineNum , 1 ) ;
var cs = builder . toString ( ) ;
if ( ! Changeset . isIdentity ( cs ) )
{
performDocumentApplyChangeset ( cs ) ;
}
//4-apply the modifications
}
2011-07-07 17:59:34 +00:00
function setLineListTypes ( lineNumTypePairsInOrder )
{
var loc = [ 0 , 0 ] ;
2011-03-26 13:10:41 +00:00
var builder = Changeset . builder ( rep . lines . totalWidth ( ) ) ;
2011-07-07 17:59:34 +00:00
for ( var i = 0 ; i < lineNumTypePairsInOrder . length ; i ++ )
{
2011-03-26 13:10:41 +00:00
var pair = lineNumTypePairsInOrder [ i ] ;
var lineNum = pair [ 0 ] ;
var listType = pair [ 1 ] ;
2011-07-07 17:59:34 +00:00
buildKeepRange ( builder , loc , ( loc = [ lineNum , 0 ] ) ) ;
if ( getLineListType ( lineNum ) )
{
2011-03-26 13:10:41 +00:00
// already a line marker
2011-07-07 17:59:34 +00:00
if ( listType )
{
2011-03-26 13:10:41 +00:00
// make different list type
2011-07-07 17:59:34 +00:00
buildKeepRange ( builder , loc , ( loc = [ lineNum , 1 ] ) , [
[ 'list' , listType ]
] , rep . apool ) ;
2011-03-26 13:10:41 +00:00
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
// remove list marker
2011-07-07 17:59:34 +00:00
buildRemoveRange ( builder , loc , ( loc = [ lineNum , 1 ] ) ) ;
2011-03-26 13:10:41 +00:00
}
}
2011-07-07 17:59:34 +00:00
else
{
2011-03-26 13:10:41 +00:00
// currently no line marker
2011-07-07 17:59:34 +00:00
if ( listType )
{
2011-03-26 13:10:41 +00:00
// add a line marker
2011-07-07 17:59:34 +00:00
builder . insert ( '*' , [
[ 'author' , thisAuthor ] ,
[ 'insertorder' , 'first' ] ,
[ 'list' , listType ]
] , rep . apool ) ;
2011-03-26 13:10:41 +00:00
}
}
}
var cs = builder . toString ( ) ;
2011-07-07 17:59:34 +00:00
if ( ! Changeset . isIdentity ( cs ) )
{
2011-03-26 13:10:41 +00:00
performDocumentApplyChangeset ( cs ) ;
}
2012-01-15 17:20:20 +00: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 13:10:41 +00:00
}
2012-01-15 17:20:20 +00:00
function doInsertList ( type )
2011-07-07 17:59:34 +00:00
{
if ( ! ( rep . selStart && rep . selEnd ) )
{
2011-03-26 13:10:41 +00:00
return ;
}
var firstLine , lastLine ;
firstLine = rep . selStart [ 0 ] ;
2011-07-07 17:59:34 +00:00
lastLine = Math . max ( firstLine , rep . selEnd [ 0 ] - ( ( rep . selEnd [ 1 ] == 0 ) ? 1 : 0 ) ) ;
2011-03-26 13:10:41 +00:00
var allLinesAreList = true ;
2011-07-07 17:59:34 +00:00
for ( var n = firstLine ; n <= lastLine ; n ++ )
{
2011-11-25 10:10:57 +00:00
var listType = getLineListType ( n ) ;
2012-01-15 17:20:20 +00:00
if ( ! listType || listType . slice ( 0 , type . length ) != type )
2011-07-07 17:59:34 +00:00
{
2011-03-26 13:10:41 +00:00
allLinesAreList = false ;
break ;
}
}
var mods = [ ] ;
2011-07-07 17:59:34 +00:00
for ( var n = firstLine ; n <= lastLine ; n ++ )
{
2011-11-25 10:10:57 +00:00
var t = '' ;
var level = 0 ;
var listType = /([a-z]+)([12345678])/ . exec ( getLineListType ( n ) ) ;
if ( listType )
{
t = listType [ 1 ] ;
level = Number ( listType [ 2 ] ) ;
}
2011-03-26 13:10:41 +00:00
var t = getLineListType ( n ) ;
2012-01-15 17:20:20 +00:00
mods . push ( [ n , allLinesAreList ? 'indent' + level : ( t ? type + level : type + '1' ) ] ) ;
2011-03-26 13:10:41 +00:00
}
setLineListTypes ( mods ) ;
}
2012-01-15 17:20:20 +00:00
function doInsertUnorderedList ( ) {
doInsertList ( 'bullet' ) ;
}
function doInsertOrderedList ( ) {
doInsertList ( 'number' ) ;
}
2011-03-26 13:10:41 +00:00
editorInfo . ace _doInsertUnorderedList = doInsertUnorderedList ;
2012-01-15 17:20:20 +00:00
editorInfo . ace _doInsertOrderedList = doInsertOrderedList ;
2011-03-26 13:10:41 +00:00
2011-07-07 17:59:34 +00:00
var mozillaFakeArrows = ( browser . mozilla && ( function ( )
{
2011-03-26 13:10:41 +00:00
// In Firefox 2, arrow keys are unstable while DOM-manipulating
// operations are going on. Specifically, if an operation
// (computation that ties up the event queue) is going on (in the
// call-stack of some event, like a timeout) that at some point
// mutates nodes involved in the selection, then the arrow
// keypress may (randomly) move the caret to the beginning or end
// of the document. If the operation also mutates the selection
// range, the old selection or the new selection may be used, or
// neither.
// As long as the arrow is pressed during the busy operation, it
// doesn't seem to matter that the keydown and keypress events
// aren't generated until afterwards, or that the arrow movement
// can still be stopped (meaning it hasn't been performed yet);
// Firefox must be preserving some old information about the
// selection or the DOM from when the key was initially pressed.
// However, it also doesn't seem to matter when the key was
// actually pressed relative to the time of the mutation within
// the prolonged operation. Also, even in very controlled tests
// (like a mutation followed by a long period of busyWaiting), the
// problem shows up often but not every time, with no discernable
// pattern. Who knows, it could have something to do with the
// caret-blinking timer, or DOM changes not being applied
// immediately.
// This problem, mercifully, does not show up at all in IE or
// Safari. My solution is to have my own, full-featured arrow-key
// implementation for Firefox.
// Note that the problem addressed here is potentially very subtle,
// especially if the operation is quick and is timed to usually happen
// when the user is idle.
// features:
// - 'up' and 'down' arrows preserve column when passing through shorter lines
// - shift-arrows extend the "focus" point, which may be start or end of range
// - the focus point is kept horizontally and vertically scrolled into view
// - arrows without shift cause caret to move to beginning or end of selection (left,right)
// or move focus point up or down a line (up,down)
// - command-(left,right,up,down) on Mac acts like (line-start, line-end, doc-start, doc-end)
// - takes wrapping into account when doesWrap is true, i.e. up-arrow and down-arrow move
// between the virtual lines within a wrapped line; this was difficult, and unfortunately
// requires mutating the DOM to get the necessary information
var savedFocusColumn = 0 ; // a value of 0 has no effect
var updatingSelectionNow = false ;
2011-07-07 17:59:34 +00:00
function getVirtualLineView ( lineNum )
{
2011-03-26 13:10:41 +00:00
var lineNode = rep . lines . atIndex ( lineNum ) . lineNode ;
2011-07-07 17:59:34 +00:00
while ( lineNode . firstChild && isBlockElement ( lineNode . firstChild ) )
{
2011-03-26 13:10:41 +00:00
lineNode = lineNode . firstChild ;
}
return makeVirtualLineView ( lineNode ) ;
}
2011-07-07 17:59:34 +00:00
function markerlessLineAndChar ( line , chr )
{
2011-03-26 13:10:41 +00:00
return [ line , chr - rep . lines . atIndex ( line ) . lineMarker ] ;
}
2011-07-07 17:59:34 +00:00
function markerfulLineAndChar ( line , chr )
{
2011-03-26 13:10:41 +00:00
return [ line , chr + rep . lines . atIndex ( line ) . lineMarker ] ;
}
return {
2011-07-07 17:59:34 +00:00
notifySelectionChanged : function ( )
{
if ( ! updatingSelectionNow )
{
savedFocusColumn = 0 ;
}
2011-03-26 13:10:41 +00:00
} ,
2011-07-07 17:59:34 +00:00
handleKeyEvent : function ( evt )
{
// returns "true" if handled
if ( evt . type != "keypress" ) return false ;
var keyCode = evt . keyCode ;
if ( keyCode < 37 || keyCode > 40 ) return false ;
incorporateUserChanges ( ) ;
if ( ! ( rep . selStart && rep . selEnd ) ) return true ;
// {byWord,toEnd,normal}
var moveMode = ( evt . altKey ? "byWord" : ( evt . ctrlKey ? "byWord" : ( evt . metaKey ? "toEnd" : "normal" ) ) ) ;
var anchorCaret = markerlessLineAndChar ( rep . selStart [ 0 ] , rep . selStart [ 1 ] ) ;
var focusCaret = markerlessLineAndChar ( rep . selEnd [ 0 ] , rep . selEnd [ 1 ] ) ;
var wasCaret = isCaret ( ) ;
if ( rep . selFocusAtStart )
{
var tmp = anchorCaret ;
anchorCaret = focusCaret ;
focusCaret = tmp ;
}
var K _UP = 38 ,
K _DOWN = 40 ,
K _LEFT = 37 ,
K _RIGHT = 39 ;
var dontMove = false ;
if ( wasCaret && ! evt . shiftKey )
{
// collapse, will mutate both together
anchorCaret = focusCaret ;
}
else if ( ( ! wasCaret ) && ( ! evt . shiftKey ) )
{
if ( keyCode == K _LEFT )
{
// place caret at beginning
if ( rep . selFocusAtStart ) anchorCaret = focusCaret ;
else focusCaret = anchorCaret ;
if ( moveMode == "normal" ) dontMove = true ;
}
else if ( keyCode == K _RIGHT )
{
// place caret at end
if ( rep . selFocusAtStart ) focusCaret = anchorCaret ;
else anchorCaret = focusCaret ;
if ( moveMode == "normal" ) dontMove = true ;
}
else
{
// collapse, will mutate both together
anchorCaret = focusCaret ;
}
}
if ( ! dontMove )
{
function lineLength ( i )
{
2011-03-26 13:10:41 +00:00
var entry = rep . lines . atIndex ( i ) ;
return entry . text . length - entry . lineMarker ;
}
2011-07-07 17:59:34 +00:00
function lineText ( i )
{
2011-03-26 13:10:41 +00:00
var entry = rep . lines . atIndex ( i ) ;
return entry . text . substring ( entry . lineMarker ) ;
}
2011-07-07 17:59:34 +00:00
if ( keyCode == K _UP || keyCode == K _DOWN )
{
var up = ( keyCode == K _UP ) ;
var canChangeLines = ( ( up && focusCaret [ 0 ] ) || ( ( ! up ) && focusCaret [ 0 ] < rep . lines . length ( ) - 1 ) ) ;
var virtualLineView , virtualLineSpot , canChangeVirtualLines = false ;
if ( doesWrap )
{
virtualLineView = getVirtualLineView ( focusCaret [ 0 ] ) ;
virtualLineSpot = virtualLineView . getVLineAndOffsetForChar ( focusCaret [ 1 ] ) ;
canChangeVirtualLines = ( ( up && virtualLineSpot . vline > 0 ) || ( ( ! up ) && virtualLineSpot . vline < (
virtualLineView . getNumVirtualLines ( ) - 1 ) ) ) ;
}
var newColByVirtualLineChange ;
if ( moveMode == "toEnd" )
{
if ( up )
{
focusCaret [ 0 ] = 0 ;
focusCaret [ 1 ] = 0 ;
}
else
{
focusCaret [ 0 ] = rep . lines . length ( ) - 1 ;
focusCaret [ 1 ] = lineLength ( focusCaret [ 0 ] ) ;
}
}
else if ( moveMode == "byWord" )
{
// move by "paragraph", a feature that Firefox lacks but IE and Safari both have
if ( up )
{
if ( focusCaret [ 1 ] == 0 && canChangeLines )
{
focusCaret [ 0 ] -- ;
focusCaret [ 1 ] = 0 ;
}
else focusCaret [ 1 ] = 0 ;
}
else
{
var lineLen = lineLength ( focusCaret [ 0 ] ) ;
if ( browser . windows )
{
if ( canChangeLines )
{
focusCaret [ 0 ] ++ ;
focusCaret [ 1 ] = 0 ;
}
else
{
focusCaret [ 1 ] = lineLen ;
}
}
else
{
if ( focusCaret [ 1 ] == lineLen && canChangeLines )
{
focusCaret [ 0 ] ++ ;
focusCaret [ 1 ] = lineLength ( focusCaret [ 0 ] ) ;
}
else
{
focusCaret [ 1 ] = lineLen ;
}
}
}
savedFocusColumn = 0 ;
}
else if ( canChangeVirtualLines )
{
var vline = virtualLineSpot . vline ;
var offset = virtualLineSpot . offset ;
if ( up ) vline -- ;
else vline ++ ;
if ( savedFocusColumn > offset ) offset = savedFocusColumn ;
else
{
savedFocusColumn = offset ;
}
var newSpot = virtualLineView . getCharForVLineAndOffset ( vline , offset ) ;
focusCaret [ 1 ] = newSpot . lineChar ;
}
else if ( canChangeLines )
{
if ( up ) focusCaret [ 0 ] -- ;
else focusCaret [ 0 ] ++ ;
var offset = focusCaret [ 1 ] ;
if ( doesWrap )
{
offset = virtualLineSpot . offset ;
}
if ( savedFocusColumn > offset ) offset = savedFocusColumn ;
else
{
savedFocusColumn = offset ;
}
if ( doesWrap )
{
var newLineView = getVirtualLineView ( focusCaret [ 0 ] ) ;
var vline = ( up ? newLineView . getNumVirtualLines ( ) - 1 : 0 ) ;
var newSpot = newLineView . getCharForVLineAndOffset ( vline , offset ) ;
focusCaret [ 1 ] = newSpot . lineChar ;
}
else
{
var lineLen = lineLength ( focusCaret [ 0 ] ) ;
if ( offset > lineLen ) offset = lineLen ;
focusCaret [ 1 ] = offset ;
}
}
else
{
if ( up ) focusCaret [ 1 ] = 0 ;
else focusCaret [ 1 ] = lineLength ( focusCaret [ 0 ] ) ;
savedFocusColumn = 0 ;
}
}
else if ( keyCode == K _LEFT || keyCode == K _RIGHT )
{
var left = ( keyCode == K _LEFT ) ;
if ( left )
{
if ( moveMode == "toEnd" ) focusCaret [ 1 ] = 0 ;
else if ( focusCaret [ 1 ] > 0 )
{
if ( moveMode == "byWord" )
{
focusCaret [ 1 ] = moveByWordInLine ( lineText ( focusCaret [ 0 ] ) , focusCaret [ 1 ] , false ) ;
}
else
{
focusCaret [ 1 ] -- ;
}
}
else if ( focusCaret [ 0 ] > 0 )
{
focusCaret [ 0 ] -- ;
focusCaret [ 1 ] = lineLength ( focusCaret [ 0 ] ) ;
if ( moveMode == "byWord" )
{
focusCaret [ 1 ] = moveByWordInLine ( lineText ( focusCaret [ 0 ] ) , focusCaret [ 1 ] , false ) ;
}
}
}
else
{
var lineLen = lineLength ( focusCaret [ 0 ] ) ;
if ( moveMode == "toEnd" ) focusCaret [ 1 ] = lineLen ;
else if ( focusCaret [ 1 ] < lineLen )
{
if ( moveMode == "byWord" )
{
focusCaret [ 1 ] = moveByWordInLine ( lineText ( focusCaret [ 0 ] ) , focusCaret [ 1 ] , true ) ;
}
else
{
focusCaret [ 1 ] ++ ;
}
}
else if ( focusCaret [ 0 ] < rep . lines . length ( ) - 1 )
{
focusCaret [ 0 ] ++ ;
focusCaret [ 1 ] = 0 ;
if ( moveMode == "byWord" )
{
focusCaret [ 1 ] = moveByWordInLine ( lineText ( focusCaret [ 0 ] ) , focusCaret [ 1 ] , true ) ;
}
}
}
savedFocusColumn = 0 ;
}
}
var newSelFocusAtStart = ( ( focusCaret [ 0 ] < anchorCaret [ 0 ] ) || ( focusCaret [ 0 ] == anchorCaret [ 0 ] && focusCaret [ 1 ] < anchorCaret [ 1 ] ) ) ;
var newSelStart = ( newSelFocusAtStart ? focusCaret : anchorCaret ) ;
var newSelEnd = ( newSelFocusAtStart ? anchorCaret : focusCaret ) ;
updatingSelectionNow = true ;
performSelectionChange ( markerfulLineAndChar ( newSelStart [ 0 ] , newSelStart [ 1 ] ) , markerfulLineAndChar ( newSelEnd [ 0 ] , newSelEnd [ 1 ] ) , newSelFocusAtStart ) ;
updatingSelectionNow = false ;
currentCallStack . userChangedSelection = true ;
return true ;
2011-03-26 13:10:41 +00:00
}
} ;
} ) ( ) ) ;
// stolen from jquery-1.2.1
2011-07-07 17:59:34 +00:00
function fixEvent ( event )
{
2011-03-26 13:10:41 +00:00
// store a copy of the original event object
// and clone to set read-only properties
var originalEvent = event ;
2011-07-07 17:59:34 +00:00
event = extend (
{ } , originalEvent ) ;
2011-03-26 13:10:41 +00:00
// add preventDefault and stopPropagation since
// they will not work on the clone
2011-07-07 17:59:34 +00:00
event . preventDefault = function ( )
{
2011-03-26 13:10:41 +00:00
// if preventDefault exists run it on the original event
2011-07-07 17:59:34 +00:00
if ( originalEvent . preventDefault ) originalEvent . preventDefault ( ) ;
2011-03-26 13:10:41 +00:00
// otherwise set the returnValue property of the original event to false (IE)
originalEvent . returnValue = false ;
} ;
2011-07-07 17:59:34 +00:00
event . stopPropagation = function ( )
{
2011-03-26 13:10:41 +00:00
// if stopPropagation exists run it on the original event
2011-07-07 17:59:34 +00:00
if ( originalEvent . stopPropagation ) originalEvent . stopPropagation ( ) ;
2011-03-26 13:10:41 +00:00
// otherwise set the cancelBubble property of the original event to true (IE)
originalEvent . cancelBubble = true ;
} ;
// Fix target property, if necessary
2011-07-07 17:59:34 +00:00
if ( ! event . target && event . srcElement ) event . target = event . srcElement ;
2011-03-26 13:10:41 +00:00
// check if target is a textnode (safari)
2011-07-07 17:59:34 +00:00
if ( browser . safari && event . target . nodeType == 3 ) event . target = originalEvent . target . parentNode ;
2011-03-26 13:10:41 +00:00
// Add relatedTarget, if necessary
2011-07-07 17:59:34 +00:00
if ( ! event . relatedTarget && event . fromElement ) event . relatedTarget = event . fromElement == event . target ? event . toElement : event . fromElement ;
2011-03-26 13:10:41 +00:00
// Calculate pageX/Y if missing and clientX/Y available
2011-07-07 17:59:34 +00:00
if ( event . pageX == null && event . clientX != null )
{
var e = document . documentElement ,
b = document . body ;
2011-03-26 13:10:41 +00:00
event . pageX = event . clientX + ( e && e . scrollLeft || b . scrollLeft || 0 ) ;
event . pageY = event . clientY + ( e && e . scrollTop || b . scrollTop || 0 ) ;
}
// Add which for key events
2011-07-07 17:59:34 +00:00
if ( ! event . which && ( event . charCode || event . keyCode ) ) event . which = event . charCode || event . keyCode ;
2011-03-26 13:10:41 +00:00
// Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
2011-07-07 17:59:34 +00:00
if ( ! event . metaKey && event . ctrlKey ) event . metaKey = event . ctrlKey ;
2011-03-26 13:10:41 +00:00
// Add which for click: 1 == left; 2 == middle; 3 == right
// Note: button is not normalized, so don't use it
2011-07-07 17:59:34 +00:00
if ( ! event . which && event . button ) event . which = ( event . button & 1 ? 1 : ( event . button & 2 ? 3 : ( event . button & 4 ? 2 : 0 ) ) ) ;
2011-03-26 13:10:41 +00:00
return event ;
}
var lineNumbersShown ;
var sideDivInner ;
2011-07-07 17:59:34 +00:00
function initLineNumbers ( )
{
2011-03-26 13:10:41 +00:00
lineNumbersShown = 1 ;
2011-07-07 17:59:34 +00:00
sideDiv . innerHTML = '<table border="0" cellpadding="0" cellspacing="0" align="right">' + '<tr><td id="sidedivinner"><div>1</div></td></tr></table>' ;
2011-03-26 13:10:41 +00:00
sideDivInner = outerWin . document . getElementById ( "sidedivinner" ) ;
}
2011-07-07 17:59:34 +00:00
function updateLineNumbers ( )
{
2011-03-26 13:10:41 +00:00
var newNumLines = rep . lines . length ( ) ;
if ( newNumLines < 1 ) newNumLines = 1 ;
2011-12-07 13:23:28 +00:00
//update height of all current line numbers
2011-07-07 17:59:34 +00:00
if ( currentCallStack && currentCallStack . domClean )
{
2011-03-26 13:10:41 +00:00
var a = sideDivInner . firstChild ;
var b = doc . body . firstChild ;
2011-12-07 13:23:28 +00:00
var n = 0 ;
2011-07-07 17:59:34 +00:00
while ( a && b )
{
2011-12-07 13:23:28 +00:00
if ( n > lineNumbersShown ) //all updated, break
break ;
2011-07-07 17:59:34 +00:00
var h = ( b . clientHeight || b . offsetHeight ) ;
if ( b . nextSibling )
{
// when text is zoomed in mozilla, divs have fractional
// heights (though the properties are always integers)
// and the line-numbers don't line up unless we pay
// attention to where the divs are actually placed...
// (also: padding on TTs/SPANs in IE...)
h = b . nextSibling . offsetTop - b . offsetTop ;
}
if ( h )
{
var hpx = h + "px" ;
2011-12-07 13:23:28 +00:00
if ( a . style . height != hpx ) {
a . style . height = hpx ;
}
2011-07-07 17:59:34 +00:00
}
a = a . nextSibling ;
b = b . nextSibling ;
2011-12-07 13:23:28 +00:00
n ++ ;
}
}
if ( newNumLines != lineNumbersShown )
{
var container = sideDivInner ;
var odoc = outerWin . document ;
var fragment = odoc . createDocumentFragment ( ) ;
while ( lineNumbersShown < newNumLines )
{
lineNumbersShown ++ ;
var n = lineNumbersShown ;
var div = odoc . createElement ( "DIV" ) ;
//calculate height for new line number
var h = ( b . clientHeight || b . offsetHeight ) ;
if ( b . nextSibling )
h = b . nextSibling . offsetTop - b . offsetTop ;
if ( h ) // apply style to div
div . style . height = h + "px" ;
div . appendChild ( odoc . createTextNode ( String ( n ) ) ) ;
fragment . appendChild ( div ) ;
b = b . nextSibling ;
}
container . appendChild ( fragment ) ;
while ( lineNumbersShown > newNumLines )
{
container . removeChild ( container . lastChild ) ;
lineNumbersShown -- ;
2011-03-26 13:10:41 +00:00
}
}
}
} ;
OUTER ( this ) ;