aboutsummaryrefslogtreecommitdiffstats
path: root/lib/dom_utils.coffee
blob: 0ce9c50c411be1350bcf1062564f6dfb8332a69f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
DomUtils =
  #
  # Runs :callback if the DOM has loaded, otherwise runs it on load
  #
  documentReady: (->
    loaded = false
    window.addEventListener("DOMContentLoaded", -> loaded = true)
    (callback) -> if loaded then callback() else window.addEventListener("DOMContentLoaded", callback)
  )()

  #
  # 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.
  #
  makeXPath: (elementArray) ->
    xpath = []
    for i of elementArray
      xpath.push("//" + elementArray[i], "//xhtml:" + elementArray[i])
    xpath.join(" | ")

  evaluateXPath: (xpath, resultType) ->
    namespaceResolver = (namespace) ->
      if (namespace == "xhtml") then "http://www.w3.org/1999/xhtml" else null
    document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null)

  #
  # Returns the first visible clientRect of an element if it exists. Otherwise it returns null.
  #
  getVisibleClientRect: (element) ->
    # Note: this call will be expensive if we modify the DOM in between calls.
    clientRects = element.getClientRects()
    clientRectsLength = clientRects.length

    for i in [0...clientRectsLength]
      if (clientRects[i].top < -2 || clientRects[i].top >= window.innerHeight - 4 ||
          clientRects[i].left < -2 || clientRects[i].left  >= window.innerWidth - 4)
        continue

      if (clientRects[i].width < 3 || clientRects[i].height < 3)
        continue

      # eliminate invisible elements (see test_harnesses/visibility_test.html)
      computedStyle = window.getComputedStyle(element, null)
      if (computedStyle.getPropertyValue('visibility') != 'visible' ||
          computedStyle.getPropertyValue('display') == 'none')
        continue

      return clientRects[i]

    for i in [0...clientRectsLength]
      # If the link has zero dimensions, it may be wrapping visible
      # but floated elements. Check for this.
      if (clientRects[i].width == 0 || clientRects[i].height == 0)
        childrenCount = element.children.length
        for j in [0...childrenCount]
          computedStyle = window.getComputedStyle(element.children[j], null)
          # Ignore child elements which are not floated and not absolutely positioned for parent elements with zero width/height
          if (computedStyle.getPropertyValue('float') == 'none' && computedStyle.getPropertyValue('position') != 'absolute')
            continue
          childClientRect = this.getVisibleClientRect(element.children[j])
          if (childClientRect == null)
            continue
          return childClientRect
    null

  #
  # Selectable means the element has a text caret; this is not the same as "focusable".
  #
  isSelectable: (element) ->
    selectableTypes = ["search", "text", "password"]
    (element.nodeName.toLowerCase() == "input" && selectableTypes.indexOf(element.type) >= 0) ||
        element.nodeName.toLowerCase() == "textarea"

  simulateSelect: (element) ->
    element.focus()
    # When focusing a textbox, put the selection caret at the end of the textbox's contents.
    element.setSelectionRange(element.value.length, element.value.length)

  simulateClick: (element, modifiers) ->
    modifiers ||= {}

    eventSequence = ["mouseover", "mousedown", "mouseup", "click"]
    for event in eventSequence
      mouseEvent = document.createEvent("MouseEvents")
      mouseEvent.initMouseEvent(event, true, true, window, 1, 0, 0, 0, 0, modifiers.ctrlKey, false, false,
          modifiers.metaKey, 0, null)
      # Debugging note: Firefox will not execute the element's default action if we dispatch this click event,
      # but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately
      element.dispatchEvent(mouseEvent)

  # momentarily flash a rectangular border to give user some visual feedback
  flashRect: (rect) ->
    flashEl = document.createElement("div")
    flashEl.id = "vimiumFlash"
    flashEl.className = "vimiumReset"
    flashEl.style.left = rect.left + window.scrollX + "px"
    flashEl.style.top = rect.top  + window.scrollY  + "px"
    flashEl.style.width = rect.width + "px"
    flashEl.style.height = rect.height + "px"
    document.body.appendChild(flashEl)
    setTimeout((-> flashEl.parentNode.removeChild(flashEl)), 400)

root = exports ? window
root.DomUtils = DomUtils