2020-12-28 11:46:33 +00:00
( function ( ) {
2020-12-28 12:33:44 +00:00
2020-12-28 11:46:33 +00:00
const datakey = Symbol ( ) ; // use as the key to modify elements.
2012-10-03 21:15:56 +01:00
2020-12-28 11:46:33 +00:00
bililiteRange = function ( el ) {
var ret ;
if ( el . setSelectionRange ) {
2020-12-28 12:33:44 +00:00
// Element is an input or textarea
2020-12-28 11:46:33 +00:00
// note that some input elements do not allow selections
try {
el . selectionStart = el . selectionStart ;
ret = new InputRange ( ) ;
} catch ( e ) {
ret = new NothingRange ( ) ;
}
} else {
// Standards, with any other kind of element
ret = new W3CRange ( ) ;
}
ret . _el = el ;
// determine parent document, as implemented by John McLear <john@mclear.co.uk>
ret . _doc = el . ownerDocument ;
ret . _win = ret . _doc . defaultView ;
ret . _bounds = [ 0 , ret . length ] ;
2020-12-28 12:33:44 +00:00
2020-12-28 11:46:33 +00:00
// selection tracking. We want clicks to set the selection to the clicked location but tabbing in or element.focus() should restore
// the selection to what it was.
// There's no good way to do this. I just assume that a mousedown (or a drag and drop
// into the element) within 100 ms of the focus event must have caused the focus, and
// therefore we should not restore the selection.
2020-12-28 12:33:44 +00:00
if ( ! ( el [ datakey ] ) ) { // we haven't processed this element yet
2020-12-28 11:46:33 +00:00
const data = createDataObject ( el ) ;
startupHooks . forEach ( hook => hook ( el , ret , data ) ) ;
2020-12-28 12:33:44 +00:00
}
2020-12-28 11:46:33 +00:00
return ret ;
}
bililiteRange . version = 3.2 ;
const startupHooks = new Set ( ) ;
bililiteRange . addStartupHook = fn => startupHooks . add ( fn ) ;
startupHooks . add ( trackSelection ) ;
startupHooks . add ( fixInputEvents ) ;
startupHooks . add ( correctNewlines ) ;
function trackSelection ( element , range , data ) {
data . selection = [ 0 , 0 ] ;
2020-12-28 12:33:44 +00:00
range . listen ( 'focusout' , evt => data . selection = range . _nativeSelection ( ) ) ;
2020-12-28 11:46:33 +00:00
range . listen ( 'mousedown' , evt => data . mousetime = evt . timeStamp ) ;
range . listen ( 'drop' , evt => data . mousetime = evt . timeStamp ) ;
range . listen ( 'focus' , evt => {
if ( 'mousetime' in data && evt . timeStamp - data . mousetime < 100 ) return ;
range . _nativeSelect ( range . _nativeRange ( data . selection ) )
} )
}
function fixInputEvents ( element , range , data ) {
// DOM 3 input events, https://www.w3.org/TR/input-events-1/
// have a data field with the text inserted, but thatisn't enough to fully describe the change;
// we need to know the old text (or at least its length)
// and *where* the new text was inserted.
2020-12-28 12:33:44 +00:00
// So we enhance input events with that information.
2020-12-28 11:46:33 +00:00
// the "newText" should always be the same as the 'data' field, if it is defined
data . oldText = range . all ( ) ;
data . liveRanges = new Set ( ) ;
range . listen ( 'input' , evt => {
const newText = range . all ( ) ;
if ( ! evt . bililiteRange ) {
evt . bililiteRange = diff ( data . oldText , newText ) ;
if ( evt . bililiteRange . unchanged ) {
// no change. Assume that whatever happened, happened at the selection point (and use whatever data the browser gives us).
evt . bililiteRange . start = range . clone ( ) . bounds ( 'selection' ) [ 1 ] - ( evt . data || '' ) . length ;
}
}
data . oldText = newText ;
2020-12-28 12:33:44 +00:00
2020-12-28 11:46:33 +00:00
// Also update live ranges on this element
data . liveRanges . forEach ( rng => {
const start = evt . bililiteRange . start ;
const oldend = start + evt . bililiteRange . oldText . length ;
const newend = start + evt . bililiteRange . newText . length ;
// adjust bounds; this tries to emulate the algorithm that Microsoft Word uses for bookmarks
let [ b0 , b1 ] = rng . bounds ( ) ;
if ( b0 <= start ) {
// no change
} else if ( b0 > oldend ) {
b0 += newend - oldend ;
} else {
b0 = newend ;
}
if ( b1 < start ) {
// no change
} else if ( b1 >= oldend ) {
b1 += newend - oldend ;
} else {
b1 = start ;
}
2020-12-28 12:33:44 +00:00
rng . bounds ( [ b0 , b1 ] ) ;
2020-12-28 11:46:33 +00:00
} )
} ) ;
}
function diff ( oldText , newText ) {
// Try to find the changed text, assuming it was a continuous change
if ( oldText == newText ) {
return {
unchanged : true ,
start : 0 ,
oldText ,
newText
}
}
const oldlen = oldText . length ;
const newlen = newText . length ;
for ( var i = 0 ; i < newlen && i < oldlen ; ++ i ) {
if ( newText . charAt ( i ) != oldText . charAt ( i ) ) break ;
}
const start = i ;
for ( i = 0 ; i < newlen && i < oldlen ; ++ i ) {
let newpos = newlen - i - 1 , oldpos = oldlen - i - 1 ;
if ( newpos < start || oldpos < start ) break ;
if ( newText . charAt ( newpos ) != oldText . charAt ( oldpos ) ) break ;
}
const oldend = oldlen - i ;
const newend = newlen - i ;
return {
start ,
oldText : oldText . slice ( start , oldend ) ,
newText : newText . slice ( start , newend )
}
} ;
bililiteRange . diff = diff ; // expose
function correctNewlines ( element , range , data ) {
// we need to insert newlines rather than create new elements, so character-based calculations work
range . listen ( 'paste' , evt => {
if ( evt . defaultPrevented ) return ;
// windows adds \r's to clipboard!
range . clone ( ) . bounds ( 'selection' ) .
text ( evt . clipboardData . getData ( "text/plain" ) . replace ( /\r/g , '' ) , { inputType : 'insertFromPaste' } ) .
bounds ( 'endbounds' ) .
select ( ) .
scrollIntoView ( ) ;
evt . preventDefault ( ) ;
} ) ;
range . listen ( 'keydown' , function ( evt ) {
if ( evt . ctrlKey || evt . altKey || evt . shiftKey || evt . metaKey ) return ;
if ( evt . defaultPrevented ) return ;
if ( evt . key == 'Enter' ) {
range . clone ( ) . bounds ( 'selection' ) .
text ( '\n' , { inputType : 'insertLineBreak' } ) .
bounds ( 'endbounds' ) .
select ( ) .
scrollIntoView ( ) ;
evt . preventDefault ( ) ;
}
} ) ;
2012-10-03 21:15:56 +01:00
}
2020-12-28 11:46:33 +00:00
// convenience function for defining input events
function inputEventInit ( type , oldText , newText , start , inputType ) {
return {
type ,
inputType ,
data : newText ,
bubbles : true ,
bililiteRange : {
unchanged : ( oldText == newText ) ,
start ,
oldText ,
newText
}
} ;
2012-10-03 21:15:56 +01:00
}
// base class
function Range ( ) { }
Range . prototype = {
2020-12-28 12:33:44 +00:00
// allow use of range[0] and range[1] for start and end of bounds
2020-12-28 11:46:33 +00:00
get 0 ( ) {
return this . bounds ( ) [ 0 ] ;
} ,
set 0 ( x ) {
this . bounds ( [ x , this [ 1 ] ] ) ;
return x ;
} ,
get 1 ( ) {
return this . bounds ( ) [ 1 ] ;
} ,
set 1 ( x ) {
this . bounds ( [ this [ 0 ] , x ] ) ;
return x ;
} ,
all : function ( text ) {
if ( arguments . length ) {
return this . bounds ( 'all' ) . text ( text , { inputType : 'insertReplacementText' } ) ;
} else {
return this . _el [ this . _textProp ] ;
}
} ,
bounds : function ( s ) {
if ( typeof s === 'number' ) {
this . _bounds = [ s , s ] ;
} else if ( bililiteRange . bounds [ s ] ) {
this . bounds ( bililiteRange . bounds [ s ] . apply ( this , arguments ) ) ;
} else if ( s && s . bounds ) {
this . _bounds = s . bounds ( ) ; // copy bounds from an existing range
} else if ( s ) {
this . _bounds = s ; // don't do error checking now; things may change at a moment's notice
} else {
// constrain bounds now
var b = [
Math . max ( 0 , Math . min ( this . length , this . _bounds [ 0 ] ) ) ,
Math . max ( 0 , Math . min ( this . length , this . _bounds [ 1 ] ) )
] ;
b [ 1 ] = Math . max ( b [ 0 ] , b [ 1 ] ) ;
return b ;
}
return this ; // allow for chaining
} ,
clone : function ( ) {
return bililiteRange ( this . _el ) . bounds ( this . bounds ( ) ) ;
} ,
get data ( ) {
return this . _el [ datakey ] ;
} ,
dispatch : function ( opts = { } ) {
var event = new Event ( opts . type , opts ) ;
event . view = this . _win ;
for ( let prop in opts ) try { event [ prop ] = opts [ prop ] } catch ( e ) { } ; // ignore read-only errors for properties that were copied in the previous line
this . _el . dispatchEvent ( event ) ; // note that the event handlers will be called synchronously, before the "return this;"
return this ;
} ,
get document ( ) {
return this . _doc ;
} ,
dontlisten : function ( type , func = console . log ) {
this . _el . removeEventListener ( type , func ) ;
return this ;
} ,
get element ( ) {
return this . _el
} ,
get length ( ) {
return this . _el [ this . _textProp ] . length ;
} ,
live ( on = true ) {
this . data . liveRanges [ on ? 'add' : 'delete' ] ( this ) ;
return this ;
} ,
listen : function ( type , func = console . log ) {
this . _el . addEventListener ( type , func ) ;
return this ;
} ,
scrollIntoView : function ( scroller = ( top => this . _el . scrollTop = top ) ) {
var top = this . top ( ) ;
// scroll into position if necessary
if ( this . _el . scrollTop > top || this . _el . scrollTop + this . _el . clientHeight < top ) {
scroller . call ( this . _el , top ) ;
}
return this ;
} ,
select : function ( ) {
var b = this . data . selection = this . bounds ( ) ;
if ( this . _el === this . _doc . activeElement ) {
// only actually select if this element is active!
this . _nativeSelect ( this . _nativeRange ( b ) ) ;
}
this . dispatch ( { type : 'select' , bubbles : true } ) ;
return this ; // allow for chaining
} ,
selection : function ( text ) {
if ( arguments . length ) {
return this . bounds ( 'selection' ) . text ( text ) . bounds ( 'endbounds' ) . select ( ) ;
} else {
return this . bounds ( 'selection' ) . text ( ) ;
}
} ,
sendkeys : function ( text ) {
this . data . sendkeysOriginalText = this . text ( ) ;
this . data . sendkeysBounds = undefined ;
function simplechar ( rng , c ) {
if ( /^{[^}]*}$/ . test ( c ) ) c = c . slice ( 1 , - 1 ) ; // deal with unknown {key}s
rng . text ( c ) . bounds ( 'endbounds' ) ;
}
text . replace ( /{[^}]*}|[^{]+|{/g , part => ( bililiteRange . sendkeys [ part ] || simplechar ) ( this , part , simplechar ) ) ;
this . bounds ( this . data . sendkeysBounds ) ;
this . dispatch ( { type : 'sendkeys' , detail : text } ) ;
return this ;
} ,
text : function ( text , { inputType = 'insertText' } = { } ) {
if ( text !== undefined ) {
let eventparams = [ this . text ( ) , text , this [ 0 ] , inputType ] ;
this . dispatch ( inputEventInit ( 'beforeinput' , ... eventparams ) ) ;
this . _nativeSetText ( text , this . _nativeRange ( this . bounds ( ) ) ) ;
this [ 1 ] = this [ 0 ] + text . length ;
this . dispatch ( inputEventInit ( 'input' , ... eventparams ) ) ;
return this ; // allow for chaining
} else {
return this . _nativeGetText ( this . _nativeRange ( this . bounds ( ) ) ) ;
}
} ,
top : function ( ) {
return this . _nativeTop ( this . _nativeRange ( this . bounds ( ) ) ) ;
} ,
get window ( ) {
return this . _win ;
} ,
wrap : function ( n ) {
this . _nativeWrap ( n , this . _nativeRange ( this . bounds ( ) ) ) ;
return this ;
} ,
2012-10-03 21:15:56 +01:00
} ;
2020-12-28 11:46:33 +00:00
// allow extensions ala jQuery
bililiteRange . prototype = Range . prototype ;
bililiteRange . extend = function ( fns ) {
Object . assign ( bililiteRange . prototype , fns ) ;
2012-10-03 21:15:56 +01:00
} ;
2020-12-28 11:46:33 +00:00
bililiteRange . override = ( name , fn ) => {
const oldfn = bililiteRange . prototype [ name ] ;
bililiteRange . prototype [ name ] = function ( ) {
const oldsuper = this . super ;
this . super = oldfn ;
const ret = fn . apply ( this , arguments ) ;
this . super = oldsuper ;
return ret ;
} ;
}
//bounds functions
bililiteRange . bounds = {
all : function ( ) { return [ 0 , this . length ] } ,
start : function ( ) { return 0 } ,
end : function ( ) { return this . length } ,
selection : function ( ) {
if ( this . _el === this . _doc . activeElement ) {
this . bounds ( 'all' ) ; // first select the whole thing for constraining
return this . _nativeSelection ( ) ;
} else {
return this . data . selection ;
}
} ,
startbounds : function ( ) { return this [ 0 ] } ,
endbounds : function ( ) { return this [ 1 ] } ,
union : function ( name , ... rest ) {
const b = this . clone ( ) . bounds ( ... rest ) ;
return [ Math . min ( this [ 0 ] , b [ 0 ] ) , Math . max ( this [ 1 ] , b [ 1 ] ) ] ;
} ,
intersection : function ( name , ... rest ) {
const b = this . clone ( ) . bounds ( ... rest ) ;
return [ Math . max ( this [ 0 ] , b [ 0 ] ) , Math . min ( this [ 1 ] , b [ 1 ] ) ] ;
}
2012-10-03 21:15:56 +01:00
} ;
2020-12-28 11:46:33 +00:00
// sendkeys functions
bililiteRange . sendkeys = {
'{tab}' : function ( rng , c , simplechar ) {
simplechar ( rng , '\t' ) ; // useful for inserting what would be whitespace
} ,
'{newline}' : function ( rng ) {
rng . text ( '\n' , { inputType : 'insertLineBreak' } ) . bounds ( 'endbounds' ) ;
} ,
'{backspace}' : function ( rng ) {
var b = rng . bounds ( ) ;
if ( b [ 0 ] == b [ 1 ] ) rng . bounds ( [ b [ 0 ] - 1 , b [ 0 ] ] ) ; // no characters selected; it's just an insertion point. Remove the previous character
rng . text ( '' , { inputType : 'deleteContentBackward' } ) ; // delete the characters and update the selection
} ,
'{del}' : function ( rng ) {
var b = rng . bounds ( ) ;
if ( b [ 0 ] == b [ 1 ] ) rng . bounds ( [ b [ 0 ] , b [ 0 ] + 1 ] ) ; // no characters selected; it's just an insertion point. Remove the next character
rng . text ( '' , { inputType : 'deleteContentForward' } ) . bounds ( 'endbounds' ) ; // delete the characters and update the selection
} ,
'{rightarrow}' : function ( rng ) {
var b = rng . bounds ( ) ;
if ( b [ 0 ] == b [ 1 ] ) ++ b [ 1 ] ; // no characters selected; it's just an insertion point. Move to the right
rng . bounds ( [ b [ 1 ] , b [ 1 ] ] ) ;
} ,
'{leftarrow}' : function ( rng ) {
var b = rng . bounds ( ) ;
if ( b [ 0 ] == b [ 1 ] ) -- b [ 0 ] ; // no characters selected; it's just an insertion point. Move to the left
rng . bounds ( [ b [ 0 ] , b [ 0 ] ] ) ;
} ,
'{selectall}' : function ( rng ) {
rng . bounds ( 'all' ) ;
} ,
'{selection}' : function ( rng ) {
// insert the characters without the sendkeys processing
rng . text ( rng . data . sendkeysOriginalText ) . bounds ( 'endbounds' ) ;
} ,
'{mark}' : function ( rng ) {
rng . data . sendkeysBounds = rng . bounds ( ) ;
}
2012-10-03 21:15:56 +01:00
} ;
2020-12-28 11:46:33 +00:00
// Synonyms from the DOM standard (http://www.w3.org/TR/DOM-Level-3-Events-key/)
bililiteRange . sendkeys [ '{Enter}' ] = bililiteRange . sendkeys [ '{enter}' ] = bililiteRange . sendkeys [ '{newline}' ] ;
bililiteRange . sendkeys [ '{Backspace}' ] = bililiteRange . sendkeys [ '{backspace}' ] ;
bililiteRange . sendkeys [ '{Delete}' ] = bililiteRange . sendkeys [ '{del}' ] ;
bililiteRange . sendkeys [ '{ArrowRight}' ] = bililiteRange . sendkeys [ '{rightarrow}' ] ;
bililiteRange . sendkeys [ '{ArrowLeft}' ] = bililiteRange . sendkeys [ '{leftarrow}' ] ;
2012-10-03 21:15:56 +01:00
// an input element in a standards document. "Native Range" is just the bounds array
function InputRange ( ) { }
InputRange . prototype = new Range ( ) ;
2020-12-28 11:46:33 +00:00
InputRange . prototype . _textProp = 'value' ;
2012-10-03 21:15:56 +01:00
InputRange . prototype . _nativeRange = function ( bounds ) {
2020-12-28 11:46:33 +00:00
return bounds || [ 0 , this . length ] ;
2012-10-03 21:15:56 +01:00
} ;
InputRange . prototype . _nativeSelect = function ( rng ) {
2020-12-28 11:46:33 +00:00
this . _el . setSelectionRange ( rng [ 0 ] , rng [ 1 ] ) ;
2012-10-03 21:15:56 +01:00
} ;
InputRange . prototype . _nativeSelection = function ( ) {
2020-12-28 11:46:33 +00:00
return [ this . _el . selectionStart , this . _el . selectionEnd ] ;
2012-10-03 21:15:56 +01:00
} ;
InputRange . prototype . _nativeGetText = function ( rng ) {
2020-12-28 11:46:33 +00:00
return this . _el . value . substring ( rng [ 0 ] , rng [ 1 ] ) ;
2012-10-03 21:15:56 +01:00
} ;
InputRange . prototype . _nativeSetText = function ( text , rng ) {
2020-12-28 11:46:33 +00:00
var val = this . _el . value ;
this . _el . value = val . substring ( 0 , rng [ 0 ] ) + text + val . substring ( rng [ 1 ] ) ;
2012-10-03 21:15:56 +01:00
} ;
InputRange . prototype . _nativeEOL = function ( ) {
2020-12-28 11:46:33 +00:00
this . text ( '\n' ) ;
2012-10-03 21:15:56 +01:00
} ;
2020-12-28 11:46:33 +00:00
InputRange . prototype . _nativeTop = function ( rng ) {
if ( rng [ 0 ] == 0 ) return 0 ; // the range starts at the top
// I can't remember where I found this clever hack to find the location of text in a text area
var clone = this . _el . cloneNode ( true ) ;
clone . style . visibility = 'hidden' ;
clone . style . position = 'absolute' ;
this . _el . parentNode . insertBefore ( clone , this . _el ) ;
clone . style . height = '1px' ;
clone . value = this . _el . value . slice ( 0 , rng [ 0 ] ) ;
var top = clone . scrollHeight ;
// this gives the bottom of the text, so we have to subtract the height of a single line
clone . value = 'X' ;
top -= clone . scrollHeight ;
clone . parentNode . removeChild ( clone ) ;
return top ;
}
InputRange . prototype . _nativeWrap = function ( ) { throw new Error ( "Cannot wrap in a text element" ) } ;
2012-10-03 21:15:56 +01:00
function W3CRange ( ) { }
W3CRange . prototype = new Range ( ) ;
2020-12-28 11:46:33 +00:00
W3CRange . prototype . _textProp = 'textContent' ;
2012-10-03 21:15:56 +01:00
W3CRange . prototype . _nativeRange = function ( bounds ) {
2020-12-28 11:46:33 +00:00
var rng = this . _doc . createRange ( ) ;
rng . selectNodeContents ( this . _el ) ;
if ( bounds ) {
w3cmoveBoundary ( rng , bounds [ 0 ] , true , this . _el ) ;
rng . collapse ( true ) ;
w3cmoveBoundary ( rng , bounds [ 1 ] - bounds [ 0 ] , false , this . _el ) ;
}
2020-12-28 12:33:44 +00:00
return rng ;
2012-10-03 21:15:56 +01:00
} ;
W3CRange . prototype . _nativeSelect = function ( rng ) {
2020-12-28 11:46:33 +00:00
this . _win . getSelection ( ) . removeAllRanges ( ) ;
this . _win . getSelection ( ) . addRange ( rng ) ;
2012-10-03 21:15:56 +01:00
} ;
W3CRange . prototype . _nativeSelection = function ( ) {
2020-12-28 11:46:33 +00:00
// returns [start, end] for the selection constrained to be in element
var rng = this . _nativeRange ( ) ; // range of the element to constrain to
if ( this . _win . getSelection ( ) . rangeCount == 0 ) return [ this . length , this . length ] ; // append to the end
var sel = this . _win . getSelection ( ) . getRangeAt ( 0 ) ;
return [
w3cstart ( sel , rng ) ,
w3cend ( sel , rng )
] ;
} ;
2012-10-03 21:15:56 +01:00
W3CRange . prototype . _nativeGetText = function ( rng ) {
2020-12-28 11:46:33 +00:00
return rng . toString ( ) ;
2012-10-03 21:15:56 +01:00
} ;
W3CRange . prototype . _nativeSetText = function ( text , rng ) {
2020-12-28 11:46:33 +00:00
rng . deleteContents ( ) ;
rng . insertNode ( this . _doc . createTextNode ( text ) ) ;
this . _el . normalize ( ) ; // merge the text with the surrounding text
2012-10-03 21:15:56 +01:00
} ;
W3CRange . prototype . _nativeEOL = function ( ) {
2020-12-28 11:46:33 +00:00
var rng = this . _nativeRange ( this . bounds ( ) ) ;
rng . deleteContents ( ) ;
var br = this . _doc . createElement ( 'br' ) ;
br . setAttribute ( '_moz_dirty' , '' ) ; // for Firefox
rng . insertNode ( br ) ;
rng . insertNode ( this . _doc . createTextNode ( '\n' ) ) ;
rng . collapse ( false ) ;
} ;
W3CRange . prototype . _nativeTop = function ( rng ) {
if ( this . length == 0 ) return 0 ; // no text, no scrolling
if ( rng . toString ( ) == '' ) {
var textnode = this . _doc . createTextNode ( 'X' ) ;
rng . insertNode ( textnode ) ;
}
var startrng = this . _nativeRange ( [ 0 , 1 ] ) ;
var top = rng . getBoundingClientRect ( ) . top - startrng . getBoundingClientRect ( ) . top ;
if ( textnode ) textnode . parentNode . removeChild ( textnode ) ;
return top ;
}
W3CRange . prototype . _nativeWrap = function ( n , rng ) {
rng . surroundContents ( n ) ;
2012-10-03 21:15:56 +01:00
} ;
2020-12-28 11:46:33 +00:00
2012-10-03 21:15:56 +01:00
// W3C internals
function nextnode ( node , root ) {
2020-12-28 11:46:33 +00:00
// in-order traversal
// we've already visited node, so get kids then siblings
if ( node . firstChild ) return node . firstChild ;
if ( node . nextSibling ) return node . nextSibling ;
if ( node === root ) return null ;
while ( node . parentNode ) {
// get uncles
node = node . parentNode ;
if ( node == root ) return null ;
if ( node . nextSibling ) return node . nextSibling ;
}
return null ;
2012-10-03 21:15:56 +01:00
}
function w3cmoveBoundary ( rng , n , bStart , el ) {
2020-12-28 11:46:33 +00:00
// move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only!
// if the start is moved after the end, then an exception is raised
if ( n <= 0 ) return ;
var node = rng [ bStart ? 'startContainer' : 'endContainer' ] ;
if ( node . nodeType == 3 ) {
// we may be starting somewhere into the text
n += rng [ bStart ? 'startOffset' : 'endOffset' ] ;
}
while ( node ) {
if ( node . nodeType == 3 ) {
var length = node . nodeValue . length ;
if ( n <= length ) {
rng [ bStart ? 'setStart' : 'setEnd' ] ( node , n ) ;
// special case: if we end next to a <br>, include that node.
if ( n == length ) {
// skip past zero-length text nodes
for ( var next = nextnode ( node , el ) ; next && next . nodeType == 3 && next . nodeValue . length == 0 ; next = nextnode ( next , el ) ) {
rng [ bStart ? 'setStartAfter' : 'setEndAfter' ] ( next ) ;
}
if ( next && next . nodeType == 1 && next . nodeName == "BR" ) rng [ bStart ? 'setStartAfter' : 'setEndAfter' ] ( next ) ;
}
return ;
} else {
rng [ bStart ? 'setStartAfter' : 'setEndAfter' ] ( node ) ; // skip past this one
n -= length ; // and eat these characters
}
}
node = nextnode ( node , el ) ;
}
2012-10-03 21:15:56 +01:00
}
var START _TO _START = 0 ; // from the w3c definitions
var START _TO _END = 1 ;
var END _TO _END = 2 ;
var END _TO _START = 3 ;
// from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange)
2020-12-28 12:33:44 +00:00
// -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange.
2012-10-03 21:15:56 +01:00
// * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
// * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
// * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
2020-12-28 12:33:44 +00:00
// * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range.
2012-10-03 21:15:56 +01:00
function w3cstart ( rng , constraint ) {
2020-12-28 11:46:33 +00:00
if ( rng . compareBoundaryPoints ( START _TO _START , constraint ) <= 0 ) return 0 ; // at or before the beginning
if ( rng . compareBoundaryPoints ( END _TO _START , constraint ) >= 0 ) return constraint . toString ( ) . length ;
rng = rng . cloneRange ( ) ; // don't change the original
rng . setEnd ( constraint . endContainer , constraint . endOffset ) ; // they now end at the same place
return constraint . toString ( ) . length - rng . toString ( ) . length ;
2012-10-03 21:15:56 +01:00
}
function w3cend ( rng , constraint ) {
2020-12-28 11:46:33 +00:00
if ( rng . compareBoundaryPoints ( END _TO _END , constraint ) >= 0 ) return constraint . toString ( ) . length ; // at or after the end
if ( rng . compareBoundaryPoints ( START _TO _END , constraint ) <= 0 ) return 0 ;
rng = rng . cloneRange ( ) ; // don't change the original
rng . setStart ( constraint . startContainer , constraint . startOffset ) ; // they now start at the same place
return rng . toString ( ) . length ;
2012-10-03 21:15:56 +01:00
}
function NothingRange ( ) { }
NothingRange . prototype = new Range ( ) ;
2020-12-28 11:46:33 +00:00
NothingRange . prototype . _textProp = 'value' ;
2012-10-03 21:15:56 +01:00
NothingRange . prototype . _nativeRange = function ( bounds ) {
2020-12-28 11:46:33 +00:00
return bounds || [ 0 , this . length ] ;
2012-10-03 21:15:56 +01:00
} ;
NothingRange . prototype . _nativeSelect = function ( rng ) { // do nothing
} ;
NothingRange . prototype . _nativeSelection = function ( ) {
2020-12-28 11:46:33 +00:00
return [ 0 , 0 ] ;
2012-10-03 21:15:56 +01:00
} ;
NothingRange . prototype . _nativeGetText = function ( rng ) {
2020-12-28 11:46:33 +00:00
return this . _el [ this . _textProp ] . substring ( rng [ 0 ] , rng [ 1 ] ) ;
2012-10-03 21:15:56 +01:00
} ;
NothingRange . prototype . _nativeSetText = function ( text , rng ) {
2020-12-28 11:46:33 +00:00
var val = this . _el [ this . _textProp ] ;
this . _el [ this . _textProp ] = val . substring ( 0 , rng [ 0 ] ) + text + val . substring ( rng [ 1 ] ) ;
2012-10-03 21:15:56 +01:00
} ;
NothingRange . prototype . _nativeEOL = function ( ) {
2020-12-28 11:46:33 +00:00
this . text ( '\n' ) ;
2012-10-03 21:15:56 +01:00
} ;
2020-12-28 11:46:33 +00:00
NothingRange . prototype . _nativeTop = function ( ) {
return 0 ;
} ;
NothingRange . prototype . _nativeWrap = function ( ) { throw new Error ( "Wrapping not implemented" ) } ;
2012-10-03 21:15:56 +01:00
2020-12-28 11:46:33 +00:00
// data for elements, similar to jQuery data, but allows for monitoring with custom events
const monitored = new Set ( ) ;
2012-10-03 21:15:56 +01:00
2020-12-28 11:46:33 +00:00
function signalMonitor ( prop , value , element ) {
const attr = ` data- ${ prop } ` ;
element . dispatchEvent ( new CustomEvent ( attr , { bubbles : true , detail : value } ) ) ;
try {
2020-12-28 12:33:44 +00:00
element . setAttribute ( attr , value ) ; // illegal attribute names will throw. Ignore it
2020-12-28 11:46:33 +00:00
} finally { /* ignore */ }
}
2012-10-03 21:15:56 +01:00
2020-12-28 11:46:33 +00:00
function createDataObject ( el ) {
return el [ datakey ] = new Proxy ( new Data ( el ) , {
set ( obj , prop , value ) {
obj [ prop ] = value ;
if ( monitored . has ( prop ) ) signalMonitor ( prop , value , obj . sourceElement ) ;
2020-12-28 12:33:44 +00:00
return true ; // in strict mode, 'set' returns a success flag
2020-12-28 11:46:33 +00:00
}
} ) ;
}
2012-10-03 21:15:56 +01:00
2020-12-28 11:46:33 +00:00
var Data = function ( el ) {
Object . defineProperty ( this , 'sourceElement' , {
value : el
} ) ;
}
2012-10-03 21:15:56 +01:00
2020-12-28 11:46:33 +00:00
Data . prototype = { } ;
// for use with ex options. JSON.stringify(range.data) should return only the options that were
// both defined with bililiteRange.option() *and* actually had a value set on this particular data object.
// JSON.stringify (range.data.all) should return all the options that were defined.
Object . defineProperty ( Data . prototype , 'toJSON' , {
value : function ( ) {
let ret = { } ;
for ( let key in Data . prototype ) if ( this . hasOwnProperty ( key ) ) ret [ key ] = this [ key ] ;
return ret ;
}
} ) ;
Object . defineProperty ( Data . prototype , 'all' , {
get : function ( ) {
let ret = { } ;
for ( let key in Data . prototype ) ret [ key ] = this [ key ] ;
return ret ;
}
} ) ;
Object . defineProperty ( Data . prototype , 'trigger' , {
value : function ( ) {
monitored . forEach ( prop => signalMonitor ( prop , this [ prop ] , this . sourceElement ) ) ;
}
} ) ;
2012-10-03 21:15:56 +01:00
2020-12-28 11:46:33 +00:00
bililiteRange . createOption = function ( name , desc = { } ) {
desc = Object . assign ( {
enumerable : true , // use these as the defaults
writable : true ,
configurable : true
} , Object . getOwnPropertyDescriptor ( Data . prototype , name ) , desc ) ;
if ( 'monitored' in desc ) monitored [ desc . monitored ? 'add' : 'delete' ] ( name ) ;
Object . defineProperty ( Data . prototype , name , desc ) ;
return Data . prototype [ name ] ; // return the default value
}
2012-10-03 21:15:56 +01:00
2020-12-28 11:46:33 +00:00
} ) ( ) ;