2012-10-03 21:15:56 +01:00
// Cross-broswer implementation of text ranges and selections
2021-08-20 20:13:24 -04:00
// documentation: http://bililite.com/blog/2011/01/17/cross-browser-text-ranges-and-selections/
// Version: 1.5
2012-10-03 21:15:56 +01:00
// Copyright (c) 2010 Daniel Wachsstock
// MIT license:
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
2021-08-20 19:33:56 -04:00
( function ( ) {
2012-10-03 21:15:56 +01:00
bililiteRange = function ( el , debug ) {
2021-08-20 19:33:56 -04:00
var ret ;
if ( debug ) {
ret = new NothingRange ( ) ; // Easier to force it to use the no-selection type than to try to find an old browser
} else if ( document . selection && ! document . addEventListener ) {
// Internet Explorer 8 and lower
ret = new IERange ( ) ;
} else if ( window . getSelection && el . setSelectionRange ) {
// Standards. Element is an input or textarea
ret = new InputRange ( ) ;
} else if ( window . getSelection ) {
// Standards, with any other kind of element
ret = new W3CRange ( )
} else {
// doesn't support selection
ret = new NothingRange ( ) ;
}
ret . _el = el ;
2021-08-20 20:13:24 -04:00
// determine parent document, as implemented by John McLear <john@mclear.co.uk>
2021-08-20 19:33:56 -04:00
ret . _doc = el . ownerDocument ;
ret . _win = 'defaultView' in ret . _doc ? ret . _doc . defaultView : ret . _doc . parentWindow ;
ret . _textProp = textProp ( el ) ;
ret . _bounds = [ 0 , ret . length ( ) ] ;
return ret ;
2012-10-03 21:15:56 +01:00
}
function textProp ( el ) {
2021-08-20 19:33:56 -04:00
// returns the property that contains the text of the element
if ( typeof el . value != 'undefined' ) return 'value' ;
if ( typeof el . text != 'undefined' ) return 'text' ;
if ( typeof el . textContent != 'undefined' ) return 'textContent' ;
return 'innerText' ;
2012-10-03 21:15:56 +01:00
}
// base class
function Range ( ) { }
Range . prototype = {
2021-08-20 19:33:56 -04:00
length : function ( ) {
return this . _el [ this . _textProp ] . replace ( /\r/g , '' ) . length ; // need to correct for IE's CrLf weirdness
} ,
bounds : function ( s ) {
if ( s === 'all' ) {
this . _bounds = [ 0 , this . length ( ) ] ;
} else if ( s === 'start' ) {
this . _bounds = [ 0 , 0 ] ;
} else if ( s === 'end' ) {
this . _bounds = [ this . length ( ) , this . length ( ) ] ;
} else if ( s === 'selection' ) {
this . bounds ( 'all' ) ; // first select the whole thing for constraining
this . _bounds = this . _nativeSelection ( ) ;
} else if ( s ) {
2021-08-20 20:13:24 -04:00
this . _bounds = s ; // don't do error checking now; things may change at a moment's notice
2021-08-20 19:33:56 -04:00
} else {
var b = [
Math . max ( 0 , Math . min ( this . length ( ) , this . _bounds [ 0 ] ) ) ,
Math . max ( 0 , Math . min ( this . length ( ) , this . _bounds [ 1 ] ) )
] ;
2021-08-20 20:13:24 -04:00
b [ 1 ] = Math . max ( b [ 0 ] , b [ 1 ] ) ;
2021-08-20 19:33:56 -04:00
return b ; // need to constrain it to fit
}
return this ; // allow for chaining
} ,
select : function ( ) {
this . _nativeSelect ( this . _nativeRange ( this . bounds ( ) ) ) ;
return this ; // allow for chaining
} ,
text : function ( text , select ) {
if ( arguments . length ) {
this . _nativeSetText ( text , this . _nativeRange ( this . bounds ( ) ) ) ;
2021-08-20 20:13:24 -04:00
try { // signal the text change (IE < 9 doesn't support this, so we live with it)
this . _el . dispatchEvent ( new CustomEvent ( 'input' , { detail : { text : text , bounds : this . bounds ( ) } } ) ) ;
} catch ( e ) { /* ignore */ }
2021-08-20 19:33:56 -04:00
if ( select == 'start' ) {
this . bounds ( [ this . _bounds [ 0 ] , this . _bounds [ 0 ] ] ) ;
} else if ( select == 'end' ) {
this . bounds ( [ this . _bounds [ 0 ] + text . length , this . _bounds [ 0 ] + text . length ] ) ;
} else if ( select == 'all' ) {
this . bounds ( [ this . _bounds [ 0 ] , this . _bounds [ 0 ] + text . length ] ) ;
}
return this ; // allow for chaining
} else {
return this . _nativeGetText ( this . _nativeRange ( this . bounds ( ) ) ) ;
}
} ,
insertEOL : function ( ) {
this . _nativeEOL ( ) ;
this . _bounds = [ this . _bounds [ 0 ] + 1 , this . _bounds [ 0 ] + 1 ] ; // move past the EOL marker
return this ;
2021-08-20 20:13:24 -04:00
} ,
scrollIntoView : function ( ) {
this . _nativeScrollIntoView ( this . _nativeRange ( this . bounds ( ) ) ) ;
return this ;
2021-08-20 19:33:56 -04:00
}
2012-10-03 21:15:56 +01:00
} ;
2021-08-20 20:13:24 -04:00
// allow extensions ala jQuery
bililiteRange . fn = Range . prototype ; // to allow monkey patching
bililiteRange . extend = function ( fns ) {
for ( fn in fns ) Range . prototype [ fn ] = fns [ fn ] ;
} ;
2012-10-03 21:15:56 +01:00
function IERange ( ) { }
IERange . prototype = new Range ( ) ;
IERange . prototype . _nativeRange = function ( bounds ) {
2021-08-20 19:33:56 -04:00
var rng ;
if ( this . _el . tagName == 'INPUT' ) {
// IE 8 is very inconsistent; textareas have createTextRange but it doesn't work
rng = this . _el . createTextRange ( ) ;
} else {
rng = this . _doc . body . createTextRange ( ) ;
rng . moveToElementText ( this . _el ) ;
}
if ( bounds ) {
if ( bounds [ 1 ] < 0 ) bounds [ 1 ] = 0 ; // IE tends to run elements out of bounds
if ( bounds [ 0 ] > this . length ( ) ) bounds [ 0 ] = this . length ( ) ;
if ( bounds [ 1 ] < rng . text . replace ( /\r/g , '' ) . length ) { // correct for IE's CrLf wierdness
// block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range
rng . moveEnd ( 'character' , - 1 ) ;
rng . moveEnd ( 'character' , bounds [ 1 ] - rng . text . replace ( /\r/g , '' ) . length ) ;
}
if ( bounds [ 0 ] > 0 ) rng . moveStart ( 'character' , bounds [ 0 ] ) ;
}
return rng ;
2012-10-03 21:15:56 +01:00
} ;
IERange . prototype . _nativeSelect = function ( rng ) {
2021-08-20 19:33:56 -04:00
rng . select ( ) ;
2012-10-03 21:15:56 +01:00
} ;
IERange . prototype . _nativeSelection = function ( ) {
2021-08-20 19:33:56 -04:00
// returns [start, end] for the selection constrained to be in element
var rng = this . _nativeRange ( ) ; // range of the element to constrain to
var len = this . length ( ) ;
if ( this . _doc . selection . type != 'Text' ) return [ len , len ] ; // append to the end
var sel = this . _doc . selection . createRange ( ) ;
try {
return [
iestart ( sel , rng ) ,
ieend ( sel , rng )
] ;
} catch ( e ) {
// IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess
return ( sel . parentElement ( ) . sourceIndex < this . _el . sourceIndex ) ? [ 0 , 0 ] : [ len , len ] ;
}
2012-10-03 21:15:56 +01:00
} ;
IERange . prototype . _nativeGetText = function ( rng ) {
2021-08-20 19:33:56 -04:00
return rng . text . replace ( /\r/g , '' ) ; // correct for IE's CrLf weirdness
2012-10-03 21:15:56 +01:00
} ;
IERange . prototype . _nativeSetText = function ( text , rng ) {
2021-08-20 19:33:56 -04:00
rng . text = text ;
2012-10-03 21:15:56 +01:00
} ;
IERange . prototype . _nativeEOL = function ( ) {
2021-08-20 19:33:56 -04:00
if ( typeof this . _el . value != 'undefined' ) {
this . text ( '\n' ) ; // for input and textarea, insert it straight
} else {
this . _nativeRange ( this . bounds ( ) ) . pasteHTML ( '<br/>' ) ;
}
2012-10-03 21:15:56 +01:00
} ;
2021-08-20 20:13:24 -04:00
IERange . prototype . _nativeScrollIntoView = function ( rng ) {
rng . scrollIntoView ( ) ;
}
2012-10-03 21:15:56 +01:00
// IE internals
function iestart ( rng , constraint ) {
2021-08-20 19:33:56 -04:00
// returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
var len = constraint . text . replace ( /\r/g , '' ) . length ; // correct for IE's CrLf wierdness
if ( rng . compareEndPoints ( 'StartToStart' , constraint ) <= 0 ) return 0 ; // at or before the beginning
if ( rng . compareEndPoints ( 'StartToEnd' , constraint ) >= 0 ) return len ;
for ( var i = 0 ; rng . compareEndPoints ( 'StartToStart' , constraint ) > 0 ; ++ i , rng . moveStart ( 'character' , - 1 ) ) ;
return i ;
2012-10-03 21:15:56 +01:00
}
function ieend ( rng , constraint ) {
2021-08-20 19:33:56 -04:00
// returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
var len = constraint . text . replace ( /\r/g , '' ) . length ; // correct for IE's CrLf wierdness
if ( rng . compareEndPoints ( 'EndToEnd' , constraint ) >= 0 ) return len ; // at or after the end
if ( rng . compareEndPoints ( 'EndToStart' , constraint ) <= 0 ) return 0 ;
for ( var i = 0 ; rng . compareEndPoints ( 'EndToStart' , constraint ) > 0 ; ++ i , rng . moveEnd ( 'character' , - 1 ) ) ;
return i ;
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 ( ) ;
InputRange . prototype . _nativeRange = function ( bounds ) {
2021-08-20 19:33:56 -04:00
return bounds || [ 0 , this . length ( ) ] ;
2012-10-03 21:15:56 +01:00
} ;
InputRange . prototype . _nativeSelect = function ( rng ) {
2021-08-20 19:33:56 -04:00
this . _el . setSelectionRange ( rng [ 0 ] , rng [ 1 ] ) ;
2012-10-03 21:15:56 +01:00
} ;
InputRange . prototype . _nativeSelection = function ( ) {
2021-08-20 19:33:56 -04:00
return [ this . _el . selectionStart , this . _el . selectionEnd ] ;
2012-10-03 21:15:56 +01:00
} ;
InputRange . prototype . _nativeGetText = function ( rng ) {
2021-08-20 19:33:56 -04:00
return this . _el . value . substring ( rng [ 0 ] , rng [ 1 ] ) ;
2012-10-03 21:15:56 +01:00
} ;
InputRange . prototype . _nativeSetText = function ( text , rng ) {
2021-08-20 19:33:56 -04: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 ( ) {
2021-08-20 19:33:56 -04:00
this . text ( '\n' ) ;
2012-10-03 21:15:56 +01:00
} ;
2021-08-20 20:13:24 -04:00
InputRange . prototype . _nativeScrollIntoView = function ( rng ) {
// I can't remember where I found this clever hack to find the location of text in a text area
var style = getComputedStyle ( this . _el ) ;
var oldheight = style . height ;
var oldval = this . _el . value ;
var oldselection = this . _nativeSelection ( ) ;
this . _el . style . height = '1px' ;
this . _el . value = oldval . slice ( 0 , rng [ 0 ] ) ;
var top = this . _el . scrollHeight ;
// this gives the bottom of the text, so we have to subtract the height of a single line
this . _el . value = 'X' ;
top -= 2 * this . _el . scrollHeight ; // show at least a line above
this . _el . style . height = oldheight ;
this . _el . value = oldval ;
this . _nativeSelect ( oldselection ) ;
// scroll into position if necessary
if ( this . _el . scrollTop > top || this . _el . scrollTop + this . _el . clientHeight < top ) {
this . _el . scrollTop = top ;
}
// now scroll the element into view; get its position as in jQuery.offset
var rect = this . _el . getBoundingClientRect ( ) ;
rect . top += this . _win . pageYOffset - this . _doc . documentElement . clientTop ;
rect . left += this . _win . pageXOffset - this . _doc . documentElement . clientLeft ;
// create an element to scroll to
var div = this . _doc . createElement ( 'div' ) ;
div . style . position = 'absolute' ;
div . style . top = ( rect . top + top - this . _el . scrollTop ) + 'px' ; // adjust for how far in the range is; it may not have scrolled all the way to the top
div . style . left = rect . left + 'px' ;
div . innerHTML = ' ' ;
this . _doc . body . appendChild ( div ) ;
div . scrollIntoViewIfNeeded ? div . scrollIntoViewIfNeeded ( ) : div . scrollIntoView ( ) ;
div . parentNode . removeChild ( div ) ;
}
2012-10-03 21:15:56 +01:00
function W3CRange ( ) { }
W3CRange . prototype = new Range ( ) ;
W3CRange . prototype . _nativeRange = function ( bounds ) {
2021-08-20 19:33:56 -04: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 ) ;
}
return rng ;
2012-10-03 21:15:56 +01:00
} ;
W3CRange . prototype . _nativeSelect = function ( rng ) {
2021-08-20 19:33:56 -04:00
this . _win . getSelection ( ) . removeAllRanges ( ) ;
this . _win . getSelection ( ) . addRange ( rng ) ;
2012-10-03 21:15:56 +01:00
} ;
W3CRange . prototype . _nativeSelection = function ( ) {
2021-08-20 19:33:56 -04: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 ) {
2021-08-20 19:33:56 -04:00
return rng . toString ( ) ;
2012-10-03 21:15:56 +01:00
} ;
W3CRange . prototype . _nativeSetText = function ( text , rng ) {
2021-08-20 19:33:56 -04: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 ( ) {
2021-08-20 19:33:56 -04: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 ) ;
2012-10-03 21:15:56 +01:00
} ;
2021-08-20 20:13:24 -04:00
W3CRange . prototype . _nativeScrollIntoView = function ( rng ) {
// can't scroll to a range; have to scroll to an element instead
var span = this . _doc . createElement ( 'span' ) ;
rng . insertNode ( span ) ;
span . scrollIntoViewIfNeeded ? span . scrollIntoViewIfNeeded ( ) : span . scrollIntoView ( ) ;
span . parentNode . removeChild ( span ) ;
}
2012-10-03 21:15:56 +01:00
// W3C internals
function nextnode ( node , root ) {
2021-08-20 19:33:56 -04: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 ) {
2021-08-20 19:33:56 -04: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 ) {
if ( n <= node . nodeValue . length ) {
rng [ bStart ? 'setStart' : 'setEnd' ] ( node , n ) ;
// special case: if we end next to a <br>, include that node.
if ( n == node . nodeValue . 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 -= node . nodeValue . 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)
2021-08-20 19:33:56 -04: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.
2021-08-20 19:33:56 -04: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 ) {
2021-08-20 19:33:56 -04: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 ) {
2021-08-20 19:33:56 -04: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 ( ) ;
NothingRange . prototype . _nativeRange = function ( bounds ) {
2021-08-20 19:33:56 -04: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 ( ) {
2021-08-20 19:33:56 -04:00
return [ 0 , 0 ] ;
2012-10-03 21:15:56 +01:00
} ;
NothingRange . prototype . _nativeGetText = function ( rng ) {
2021-08-20 19:33:56 -04: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 ) {
2021-08-20 19:33:56 -04: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 ( ) {
2021-08-20 19:33:56 -04:00
this . text ( '\n' ) ;
2012-10-03 21:15:56 +01:00
} ;
2021-08-20 20:13:24 -04:00
NothingRange . prototype . _nativeScrollIntoView = function ( ) {
this . _el . scrollIntoView ( ) ;
} ;
2012-10-03 21:15:56 +01:00
2021-08-20 19:33:56 -04:00
} ) ( ) ;