2013-05-10 23:44:32 +00:00
/ * *
* Position module for Rangy .
* Extensions to Range and Selection objects to provide access to pixel positions relative to the viewport or document .
*
* Part of Rangy , a cross - browser JavaScript range and selection library
* http : //code.google.com/p/rangy/
*
* Depends on Rangy core .
*
* Copyright 2013 , Tim Down
* Licensed under the MIT license .
* Version : 1.3 alpha . 772
* Build date : 26 February 2013
* /
rangy . createModule ( "Position" , function ( api , module ) {
api . requireModules ( [ "WrappedSelection" , "WrappedRange" ] ) ;
//var log = log4javascript.getLogger("rangy.position");
var NUMBER = "number" , UNDEF = "undefined" ;
var WrappedRange = api . WrappedRange ;
2013-05-13 21:44:16 +00:00
var WrappedTextRange = api . WrappedTextRange ;
2013-05-10 23:44:32 +00:00
var dom = api . dom , util = api . util , DomPosition = dom . DomPosition ;
2013-05-13 21:44:16 +00:00
2013-05-10 23:44:32 +00:00
// Feature detection
//var caretPositionFromPointSupported = (typeof document.caretPositionFromPoint != UNDEF);
// Since Rangy can deal with multiple documents which could be in different modes, we have to do the checks every
// time, unless we cache a getScrollPosition function in each document. This would necessarily pollute the
// document's global namespace, which I'm choosing to view as a greater evil than a slight performance hit.
function getScrollPosition ( win ) {
var x = 0 , y = 0 ;
if ( typeof win . pageXOffset == NUMBER && typeof win . pageYOffset == NUMBER ) {
x = win . pageXOffset ;
y = win . pageYOffset ;
} else {
var doc = win . document ;
var docEl = doc . documentElement ;
var compatMode = doc . compatMode ;
var scrollEl = ( typeof compatMode == "string" && compatMode . indexOf ( "CSS" ) >= 0 && docEl )
? docEl : dom . getBody ( doc ) ;
if ( scrollEl && typeof scrollEl . scrollLeft == NUMBER && typeof scrollEl . scrollTop == NUMBER ) {
try {
x = scrollEl . scrollLeft ;
y = scrollEl . scrollTop ;
} catch ( ex ) { }
}
}
return { x : x , y : y } ;
}
function getAncestorElement ( node , tagName ) {
tagName = tagName . toLowerCase ( ) ;
while ( node ) {
if ( node . nodeType == 1 && node . tagName . toLowerCase ( ) == tagName ) {
return node ;
}
node = node . parentNode ;
}
return null ;
}
function Rect ( top , right , bottom , left ) {
this . top = top ;
this . right = right ;
this . bottom = bottom ;
this . left = left ;
this . width = right - left ;
this . height = bottom - top ;
}
function createRelativeRect ( rect , dx , dy ) {
return new Rect ( rect . top + dy , rect . right + dx , rect . bottom + dy , rect . left + dx ) ;
}
function adjustClientRect ( rect , doc ) {
// Older IEs have an issue with a two pixel margin on the body element
var dx = 0 , dy = 0 ;
var docEl = doc . documentElement , body = dom . getBody ( doc ) ;
var container = ( docEl . clientWidth === 0 && typeof body . clientTop == NUMBER ) ? body : docEl ;
var clientLeft = container . clientLeft , clientTop = container . clientTop ;
if ( clientLeft ) {
dx = - clientLeft ;
}
if ( clientTop ) {
dy = - clientTop ;
}
return createRelativeRect ( rect , dx , dy ) ;
}
function mergeRects ( rects ) {
var tops = [ ] , bottoms = [ ] , lefts = [ ] , rights = [ ] ;
for ( var i = 0 , len = rects . length , rect ; i < len ; ++ i ) {
rect = rects [ i ] ;
if ( rect ) {
tops . push ( rect . top ) ;
bottoms . push ( rect . bottom ) ;
lefts . push ( rect . left ) ;
rights . push ( rect . right ) ;
}
}
return new Rect (
Math . min . apply ( Math , tops ) ,
Math . max . apply ( Math , rights ) ,
Math . max . apply ( Math , bottoms ) ,
Math . min . apply ( Math , lefts )
) ;
}
2013-05-13 21:44:16 +00:00
2013-05-10 23:44:32 +00:00
function getTextRangePosition ( doc , x , y ) {
var textRange = dom . getBody ( doc ) . createTextRange ( ) ;
textRange . moveToPoint ( x , y ) ;
var range = new api . WrappedTextRange ( textRange ) ;
return new DomPosition ( range . startContainer , range . startOffset ) ;
}
function caretPositionFromPoint ( doc , x , y ) {
var pos = doc . caretPositionFromPoint ( x , y ) ;
return new DomPosition ( pos . offsetNode , pos . offset ) ;
}
function caretRangeFromPoint ( doc , x , y ) {
var range = doc . caretRangeFromPoint ( x , y ) ;
return new DomPosition ( range . startContainer , range . startOffset ) ;
}
function getLastRangeRect ( range ) {
var rects = ( range . nativeRange || range ) . getClientRects ( ) ;
return ( rects . length > 0 ) ? rects [ rects . length - 1 ] : null ;
}
function pointIsInOrAboveRect ( x , y , rect ) {
console . log ( "pointIsInOrAboveRect" , x , y , Math . floor ( rect . top ) , Math . floor ( rect . right ) , Math . floor ( rect . bottom ) , Math . floor ( rect . left ) )
return y < rect . bottom && x >= rect . left && x <= rect . right ;
}
function positionFromPoint ( doc , x , y , favourPrecedingPosition ) {
var el = doc . elementFromPoint ( x , y ) ;
2013-05-13 21:44:16 +00:00
2013-05-10 23:44:32 +00:00
console . log ( "elementFromPoint is " , el ) ;
var range = api . createRange ( doc ) ;
range . selectNodeContents ( el ) ;
range . collapse ( true ) ;
var node = el . firstChild , offset , rect , textLen ;
if ( ! node ) {
node = el . parentNode ;
offset = dom . getNodeIndex ( el ) ;
if ( ! favourPrecedingPosition ) {
++ offset ;
}
} else {
// Search through the text node children of el
main : while ( node ) {
console . log ( node ) ;
if ( node . nodeType == 3 ) {
// Go through the text node character by character
for ( offset = 0 , textLen = node . length ; offset <= textLen ; ++ offset ) {
range . setEnd ( node , offset ) ;
rect = getLastRangeRect ( range ) ;
if ( rect && pointIsInOrAboveRect ( x , y , rect ) ) {
// We've gone past the point. Now we check which side (left or right) of the character the point is nearer to
if ( rect . right - x > x - rect . left ) {
-- offset ;
}
break main ;
}
}
} else {
// Handle elements
range . setEndAfter ( node ) ;
rect = getLastRangeRect ( range ) ;
if ( rect && pointIsInOrAboveRect ( x , y , rect ) ) {
offset = dom . getNodeIndex ( node ) ;
node = el . parentNode ;
if ( ! favourPrecedingPosition ) {
++ offset ;
}
break ;
}
}
node = node . nextSibling ;
}
if ( ! node ) {
node = el ;
offset = el . childNodes . length ;
}
}
return new DomPosition ( node , offset ) ;
}
function createCaretPositionFromPointGetter ( doc ) {
if ( api . features . implementsTextRange ) {
return getTextRangePosition ;
} else if ( typeof doc . caretPositionFromPoint != UNDEF ) {
return caretPositionFromPoint ;
} else if ( typeof doc . caretRangeFromPoint != UNDEF ) {
return caretRangeFromPoint ;
} else if ( typeof doc . elementFromPoint != UNDEF && rangeSupportsGetClientRects ) {
return positionFromPoint ;
} else {
throw module . createError ( "createCaretPositionFromPointGetter(): Browser does not provide a recognised method to create a selection from pixel coordinates" ) ;
}
}
2013-05-13 21:44:16 +00:00
2013-05-10 23:44:32 +00:00
function createRangeFromPoints ( startX , startY , endX , endY , doc ) {
doc = dom . getContentDocument ( doc , module , "createRangeFromPoints" ) ;
var positionFinder = createCaretPositionFromPointGetter ( doc ) ;
var startPos = positionFinder ( doc , startX , startY , false ) ;
var endPos = positionFinder ( doc , endX , endY , true ) ;
console . log ( startPos . node , startPos . offset , endPos . node , endPos . offset ) ;
var range = api . createRange ( doc ) ;
range . setStartAndEnd ( startPos . node , startPos . offset , endPos . node , endPos . offset ) ;
return range ;
}
function moveSelectionToPoints ( anchorX , anchorY , focusX , focusY , doc ) {
var startX , startY , endX , endY ;
// Detect backward selection for coordinates and flip start and end coordinates if necessary
var backward = focusY < anchorY || ( anchorY == focusY && focusX < anchorX ) ;
if ( backward ) {
startX = focusX ;
startY = focusY ;
endX = anchorX ;
endY = anchorY ;
} else {
startX = anchorX ;
startY = anchorY ;
endX = focusX ;
endY = focusY ;
}
var sel = rangy . getSelection ( doc ) ;
var range = createRangeFromPoints ( startX , startY , endX , endY , doc ) ;
sel . setSingleRange ( range ) ;
return sel ;
}
2013-05-13 21:44:16 +00:00
2013-05-10 23:44:32 +00:00
// Test that <span> elements support getBoundingClientRect
var span = document . createElement ( "span" ) ;
var elementSupportsGetBoundingClientRect = util . isHostMethod ( span , "getBoundingClientRect" ) ;
span = null ;
// Test for getBoundingClientRect support in Range
var rangeSupportsGetClientRects = false , rangeSupportsGetBoundingClientRect = false ;
if ( api . features . implementsDomRange ) {
var testRange = api . createNativeRange ( ) ;
rangeSupportsGetClientRects = util . isHostMethod ( testRange , "getClientRects" ) ;
rangeSupportsGetBoundingClientRect = util . isHostMethod ( testRange , "getBoundingClientRect" ) ;
testRange . detach ( ) ;
}
util . extend ( api . features , {
rangeSupportsGetBoundingClientRect : rangeSupportsGetBoundingClientRect ,
rangeSupportsGetClientRects : rangeSupportsGetClientRects ,
elementSupportsGetBoundingClientRect : elementSupportsGetBoundingClientRect
} ) ;
var createClientBoundaryPosGetter = function ( isStart ) {
return function ( ) {
var boundaryRange = this . cloneRange ( ) ;
boundaryRange . collapse ( isStart ) ;
var rect = boundaryRange . getBoundingClientRect ( ) ;
return {
x : rect [ isStart ? "left" : "right" ] ,
y : rect [ isStart ? "top" : "bottom" ]
} ;
} ;
} ;
var rangeProto = api . rangePrototype ;
if ( api . features . implementsTextRange && elementSupportsGetBoundingClientRect ) {
rangeProto . getBoundingClientRect = function ( ) {
// We need a TextRange
2013-05-13 21:44:16 +00:00
var textRange = WrappedTextRange . rangeToTextRange ( this ) ;
2013-05-10 23:44:32 +00:00
// Work around table problems (table cell bounding rects seem not to count if TextRange spans cells)
var cells = this . getNodes ( [ 1 ] , function ( el ) {
return /^t[dh]$/i . test ( el . tagName ) ;
} ) ;
// Merge rects for each cell selected by the range into overall rect
var rect , rects = [ ] ;
if ( cells . length > 0 ) {
var lastTable = getAncestorElement ( this . startContainer , "table" ) ;
for ( var i = 0 , cell , tempTextRange , table , subRange , subRect ; cell = cells [ i ] ; ++ i ) {
// Handle non-table sections of the range
table = getAncestorElement ( cell , "table" ) ;
if ( ! lastTable || table != lastTable ) {
// There is a section of the range prior to the current table, or lying between tables.
// Merge in its rect
subRange = this . cloneRange ( ) ;
if ( lastTable ) {
subRange . setStartAfter ( lastTable ) ;
}
subRange . setEndBefore ( table ) ;
2013-05-13 21:44:16 +00:00
rects . push ( WrappedTextRange . rangeToTextRange ( subRange ) . getBoundingClientRect ( ) ) ;
2013-05-10 23:44:32 +00:00
}
if ( this . containsNode ( cell ) ) {
rects . push ( cell . getBoundingClientRect ( ) ) ;
} else {
tempTextRange = textRange . duplicate ( ) ;
tempTextRange . moveToElementText ( cell ) ;
if ( tempTextRange . compareEndPoints ( "StartToStart" , textRange ) == - 1 ) {
tempTextRange . setEndPoint ( "StartToStart" , textRange ) ;
} else if ( tempTextRange . compareEndPoints ( "EndToEnd" , textRange ) == 1 ) {
tempTextRange . setEndPoint ( "EndToEnd" , textRange ) ;
}
rects . push ( tempTextRange . getBoundingClientRect ( ) ) ;
}
lastTable = table ;
}
// Merge in the rect for any content lying after the final table
var endTable = getAncestorElement ( this . endContainer , "table" ) ;
if ( ! endTable && lastTable ) {
subRange = this . cloneRange ( ) ;
subRange . setStartAfter ( lastTable ) ;
2013-05-13 21:44:16 +00:00
rects . push ( WrappedTextRange . rangeToTextRange ( subRange ) . getBoundingClientRect ( ) ) ;
2013-05-10 23:44:32 +00:00
}
rect = mergeRects ( rects ) ;
} else {
rect = textRange . getBoundingClientRect ( ) ;
}
return adjustClientRect ( rect , dom . getDocument ( this . startContainer ) ) ;
} ;
} else if ( api . features . implementsDomRange ) {
var createWrappedRange = function ( range ) {
return ( range instanceof WrappedRange ) ? range : new WrappedRange ( range ) ;
} ;
if ( rangeSupportsGetBoundingClientRect ) {
rangeProto . getBoundingClientRect = function ( ) {
var nativeRange = createWrappedRange ( this ) . nativeRange ;
// Test for WebKit getBoundingClientRect bug (https://bugs.webkit.org/show_bug.cgi?id=65324)
var rect = nativeRange . getBoundingClientRect ( ) || nativeRange . getClientRects ( ) [ 0 ] ;
return adjustClientRect ( rect , dom . getDocument ( this . startContainer ) ) ;
} ;
if ( rangeSupportsGetClientRects ) {
var getElementRectsForPosition = function ( node , offset ) {
var children = node . childNodes ;
//if (offset < children.length)
} ;
createClientBoundaryPosGetter = function ( isStart ) {
return function ( ) {
var rect , nativeRange = createWrappedRange ( this ) . nativeRange ;
var rects = nativeRange . getClientRects ( ) ;
if ( rects . length == 0 && elementSupportsGetBoundingClientRect ) {
if ( isStart ) {
}
console . log ( nativeRange , nativeRange . getClientRects ( ) , nativeRange . getBoundingClientRect ( ) ) ;
if ( this . collapsed
&& this . startContainer . nodeType == 1
&& this . startOffset < this . startContainer . childNodes . length ) {
var n = this . startContainer . childNodes [ this . startOffset ] ;
if ( n . getClientRects ) {
console . log ( n , n . getClientRects ( ) , this . startContainer . getClientRects ( ) )
}
}
}
if ( rects . length > 0 ) {
if ( isStart ) {
rect = rects [ 0 ] ;
return { x : rect . left , y : rect . top } ;
} else {
rect = rects [ rects . length - 1 ] ;
return { x : rect . right , y : rect . bottom } ;
}
} else {
throw module . createError ( "Cannot get position for range " + this . inspect ( ) ) ;
}
} ;
}
}
} else {
var getElementBoundingClientRect = elementSupportsGetBoundingClientRect ?
function ( el ) {
return adjustClientRect ( el . getBoundingClientRect ( ) , dom . getDocument ( el ) ) ;
} :
// This implementation is very naive. There are many browser quirks that make it extremely
// difficult to get accurate element coordinates in all situations
function ( el ) {
var x = 0 , y = 0 , offsetEl = el , width = el . offsetWidth , height = el . offsetHeight ;
while ( offsetEl ) {
x += offsetEl . offsetLeft ;
y += offsetEl . offsetTop ;
offsetEl = offsetEl . offsetParent ;
}
return adjustClientRect ( new Rect ( y , x + width , y + height , x ) , dom . getDocument ( el ) ) ;
} ;
var getRectFromBoundaries = function ( range ) {
var rect ;
range . splitBoundaries ( ) ;
var span = document . createElement ( "span" ) ;
if ( range . collapsed ) {
range . insertNode ( span ) ;
rect = getElementBoundingClientRect ( span ) ;
span . parentNode . removeChild ( span ) ;
} else {
// TODO: This isn't right. I'm not sure it can be made right sensibly. Consider what to do.
// This doesn't consider all the line boxes it needs to consider.
var workingRange = range . cloneRange ( ) ;
// Get the start rectangle
workingRange . collapse ( true ) ;
workingRange . insertNode ( span ) ;
var startRect = getElementBoundingClientRect ( span ) ;
span . parentNode . removeChild ( span ) ;
// Get the end rectangle
workingRange . collapseToPoint ( range . endContainer , range . endOffset ) ;
workingRange . insertNode ( span ) ;
var endRect = getElementBoundingClientRect ( span ) ;
span . parentNode . removeChild ( span ) ;
// Merge the start and end rects
var rects = [ startRect , endRect ] ;
// Merge in rectangles for all elements in the range
var elements = range . getNodes ( [ 1 ] , function ( el ) {
return range . containsNode ( el ) ;
} ) ;
for ( var i = 0 , len = elements . length ; i < len ; ++ i ) {
rects . push ( getElementBoundingClientRect ( elements [ i ] ) ) ;
}
rect = mergeRects ( rects )
}
// Clean up
range . normalizeBoundaries ( ) ;
return rect ;
} ;
rangeProto . getBoundingClientRect = function ( range ) {
return getRectFromBoundaries ( createWrappedRange ( range ) ) ;
} ;
}
function createDocumentBoundaryPosGetter ( isStart ) {
return function ( ) {
var pos = this [ "get" + ( isStart ? "Start" : "End" ) + "ClientPos" ] ( ) ;
var scrollPos = getScrollPosition ( dom . getWindow ( this . startContainer ) ) ;
return { x : pos . x + scrollPos . x , y : pos . y + scrollPos . y } ;
} ;
}
}
util . extend ( rangeProto , {
getBoundingDocumentRect : function ( ) {
var scrollPos = getScrollPosition ( dom . getWindow ( this . startContainer ) ) ;
return createRelativeRect ( this . getBoundingClientRect ( ) , scrollPos . x , scrollPos . y ) ;
} ,
getStartClientPos : createClientBoundaryPosGetter ( true ) ,
getEndClientPos : createClientBoundaryPosGetter ( false ) ,
getStartDocumentPos : createDocumentBoundaryPosGetter ( true ) ,
getEndDocumentPos : createDocumentBoundaryPosGetter ( false )
} ) ;
// Add Selection methods
function compareRanges ( r1 , r2 ) {
return r1 . compareBoundaryPoints ( r2 . START _TO _START , r2 ) ;
}
function createSelectionRectGetter ( isDocument ) {
return function ( ) {
var rangeMethodName = "getBounding" + ( isDocument ? "Document" : "Client" ) + "Rect" ;
var rects = [ ] ;
for ( var i = 0 , rect = null , rangeRect ; i < this . rangeCount ; ++ i ) {
rects . push ( this . getRangeAt ( i ) [ rangeMethodName ] ( ) ) ;
}
return mergeRects ( rects ) ;
} ;
}
function createSelectionBoundaryPosGetter ( isStart , isDocument ) {
return function ( ) {
if ( this . rangeCount == 0 ) {
return null ;
}
var posType = isDocument ? "Document" : "Client" ;
var ranges = this . getAllRanges ( ) ;
if ( ranges . length > 1 ) {
// Order the ranges by position within the DOM
ranges . sort ( compareRanges ) ;
}
return isStart ?
ranges [ 0 ] [ "getStart" + posType + "Pos" ] ( ) :
ranges [ ranges . length - 1 ] [ "getEnd" + posType + "Pos" ] ( ) ;
} ;
}
util . extend ( api . selectionPrototype , {
getBoundingClientRect : createSelectionRectGetter ( false ) ,
getBoundingDocumentRect : createSelectionRectGetter ( true ) ,
getStartClientPos : createSelectionBoundaryPosGetter ( true , false ) ,
getEndClientPos : createSelectionBoundaryPosGetter ( false , false ) ,
getStartDocumentPos : createSelectionBoundaryPosGetter ( true , true ) ,
getEndDocumentPos : createSelectionBoundaryPosGetter ( false , true )
} ) ;
2013-05-13 21:44:16 +00:00
2013-05-10 23:44:32 +00:00
api . positionFromPoint = function ( x , y , doc ) {
doc = dom . getContentDocument ( doc , module , "positionFromPoint" ) ;
return createCaretPositionFromPointGetter ( doc ) ( doc , x , y ) ;
} ;
2013-05-13 21:44:16 +00:00
2013-05-10 23:44:32 +00:00
api . createRangeFromPoints = createRangeFromPoints ;
api . moveSelectionToPoints = moveSelectionToPoints ;
} ) ;