aboutsummaryrefslogtreecommitdiffstats
path: root/lib/dom_utils.coffee
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dom_utils.coffee')
-rw-r--r--lib/dom_utils.coffee94
1 files changed, 72 insertions, 22 deletions
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 2ae9412e..7c47179c 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -27,6 +27,12 @@ DomUtils =
removeElement: (el) -> el.parentNode.removeChild el
#
+ # Test whether the current frame is the top/main frame.
+ #
+ isTopFrame: ->
+ window.top == window.self
+
+ #
# Takes an array of XPath selectors, adds the necessary namespaces (currently only XHTML), and applies them
# to the document root. The namespaceResolver in evaluateXPath should be kept in sync with the namespaces
# here.
@@ -49,21 +55,37 @@ DomUtils =
#
# Returns the first visible clientRect of an element if it exists. Otherwise it returns null.
#
- getVisibleClientRect: (element) ->
+ # WARNING: If testChildren = true then the rects of visible (eg. floated) children may be returned instead.
+ # This is used for LinkHints and focusInput, **BUT IS UNSUITABLE FOR MOST OTHER PURPOSES**.
+ #
+ getVisibleClientRect: (element, testChildren = false) ->
# Note: this call will be expensive if we modify the DOM in between calls.
clientRects = (Rect.copy clientRect for clientRect in element.getClientRects())
+ # Inline elements with font-size: 0px; will declare a height of zero, even if a child with non-zero
+ # font-size contains text.
+ isInlineZeroHeight = ->
+ elementComputedStyle = window.getComputedStyle element, null
+ isInlineZeroFontSize = (0 == elementComputedStyle.getPropertyValue("display").indexOf "inline") and
+ (elementComputedStyle.getPropertyValue("font-size") == "0px")
+ # Override the function to return this value for the rest of this context.
+ isInlineZeroHeight = -> isInlineZeroFontSize
+ isInlineZeroFontSize
+
for clientRect in clientRects
- # If the link has zero dimensions, it may be wrapping visible
- # but floated elements. Check for this.
- if (clientRect.width == 0 || clientRect.height == 0)
+ # If the link has zero dimensions, it may be wrapping visible but floated elements. Check for this.
+ if (clientRect.width == 0 or clientRect.height == 0) and testChildren
for child in element.children
computedStyle = window.getComputedStyle(child, null)
- # Ignore child elements which are not floated and not absolutely positioned for parent elements with
- # zero width/height
- continue if (computedStyle.getPropertyValue('float') == 'none' &&
- computedStyle.getPropertyValue('position') != 'absolute')
- childClientRect = @getVisibleClientRect(child)
+ # Ignore child elements which are not floated and not absolutely positioned for parent elements
+ # with zero width/height, as long as the case described at isInlineZeroHeight does not apply.
+ # NOTE(mrmr1993): This ignores floated/absolutely positioned descendants nested within inline
+ # children.
+ continue if (computedStyle.getPropertyValue("float") == "none" and
+ computedStyle.getPropertyValue("position") != "absolute" and
+ not (clientRect.height == 0 and isInlineZeroHeight() and
+ 0 == computedStyle.getPropertyValue("display").indexOf "inline"))
+ childClientRect = @getVisibleClientRect child, true
continue if childClientRect == null or childClientRect.width < 3 or childClientRect.height < 3
return childClientRect
@@ -74,9 +96,7 @@ DomUtils =
# eliminate invisible elements (see test_harnesses/visibility_test.html)
computedStyle = window.getComputedStyle(element, null)
- if (computedStyle.getPropertyValue('visibility') != 'visible' ||
- computedStyle.getPropertyValue('display') == 'none')
- continue
+ continue if computedStyle.getPropertyValue('visibility') != 'visible'
return clientRect
@@ -167,15 +187,20 @@ DomUtils =
node = node.parentNode
false
- # True if element contains the active selection range.
+ # True if element is editable and contains the active selection range.
isSelected: (element) ->
+ selection = document.getSelection()
if element.isContentEditable
- node = document.getSelection()?.anchorNode
+ node = selection.anchorNode
node and @isDOMDescendant element, node
else
- # Note. This makes the wrong decision if the user has placed the caret at the start of element. We
- # cannot distinguish that case from the user having made no selection.
- element.selectionStart? and element.selectionEnd? and element.selectionEnd != 0
+ if selection.type == "Range" and selection.isCollapsed
+ # The selection is inside the Shadow DOM of a node. We can check the node it registers as being
+ # before, since this represents the node whose Shadow DOM it's inside.
+ containerNode = selection.anchorNode.childNodes[selection.anchorOffset]
+ element == containerNode # True if the selection is inside the Shadow DOM of our element.
+ else
+ false
simulateSelect: (element) ->
# If element is already active, then we don't move the selection. However, we also won't get a new focus
@@ -185,11 +210,17 @@ DomUtils =
handlerStack.bubbleEvent "click", target: element
else
element.focus()
- unless @isSelected element
- # When focusing a textbox (without an existing selection), put the selection caret at the end of the
- # textbox's contents. For some HTML5 input types (eg. date) we can't position the caret, so we wrap
- # this with a try.
- try element.setSelectionRange(element.value.length, element.value.length)
+ # If the cursor is at the start of the element's contents, send it to the end. Motivation:
+ # * the end is a more useful place to focus than the start,
+ # * this way preserves the last used position (except when it's at the beginning), so the user can
+ # 'resume where they left off'.
+ # NOTE(mrmr1993): Some elements throw an error when we try to access their selection properties, so
+ # wrap this with a try.
+ try
+ if element.selectionStart == 0 and element.selectionEnd == 0
+ element.setSelectionRange element.value.length, element.value.length
+
+
simulateClick: (element, modifiers) ->
modifiers ||= {}
@@ -295,5 +326,24 @@ DomUtils =
document.body.removeChild div
coordinates
+ # Get the text content of an element (and its descendents), but omit the text content of previously-visited
+ # nodes. See #1514.
+ # NOTE(smblott). This is currently O(N^2) (when called on N elements). An alternative would be to mark
+ # each node visited, and then clear the marks when we're done.
+ textContent: do ->
+ visitedNodes = null
+ reset: -> visitedNodes = []
+ get: (element) ->
+ nodes = document.createTreeWalker element, NodeFilter.SHOW_TEXT
+ texts =
+ while node = nodes.nextNode()
+ continue unless node.nodeType == 3
+ continue if node in visitedNodes
+ text = node.data.trim()
+ continue unless 0 < text.length
+ visitedNodes.push node
+ text
+ texts.join " "
+
root = exports ? window
root.DomUtils = DomUtils