]> git.gir.st - VimFx.git/blob - extension/packages/hints.coffee
Closes #164. Invisible elements won't get hint markers
[VimFx.git] / extension / packages / hints.coffee
1 utils = require 'utils'
2 { getPref } = require 'prefs'
3 { Marker } = require 'marker'
4 { addHuffmanCodeWordsTo } = require 'huffman'
5
6 { interfaces: Ci } = Components
7
8 HTMLDocument = Ci.nsIDOMHTMLDocument
9 XULDocument = Ci.nsIDOMXULDocument
10 XPathResult = Ci.nsIDOMXPathResult
11
12 CONTAINER_ID = 'VimFxHintMarkerContainer'
13
14 # All the following elements qualify for their own marker in hints mode
15 MARKABLE_ELEMENTS = [
16 "a"
17 "iframe"
18 "area[@href]"
19 "textarea"
20 "button"
21 "select"
22 "input[not(@type='hidden' or @disabled or @readonly)]"
23 "embed"
24 "object"
25 ]
26
27 # All elements that have one or more of the following properties
28 # qualify for their own marker in hints mode
29 MARKABLE_ELEMENT_PROPERTIES = [
30 "@tabindex"
31 "@onclick"
32 "@onmousedown"
33 "@onmouseup"
34 "@oncommand"
35 "@role='link'"
36 "@role='button'"
37 "contains(@class, 'button')"
38 "contains(@class, 'js-new-tweets-bar')"
39 "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true'"
40 ]
41
42
43 # Remove previously injected hints from the DOM
44 removeHints = (document) ->
45 if container = document.getElementById(CONTAINER_ID)
46 document.documentElement.removeChild(container)
47
48 for frame in document.defaultView.frames
49 removeHints(frame.document)
50
51
52 # Like `injectMarkers`, but also sets hints for the markers
53 injectHints = (document) ->
54 markers = injectMarkers(document)
55 hintChars = utils.getHintChars()
56
57 addHuffmanCodeWordsTo(markers, {alphabet: hintChars}, (marker, hint) -> marker.setHint(hint))
58
59 return markers
60
61
62 # Creates and injects markers into the DOM
63 injectMarkers = (document) ->
64 # First remove previous hints container
65 removeHints(document)
66
67 # For now we aren't able to handle hint markers in XUL Documents :(
68 if document instanceof HTMLDocument# or document instanceof XULDocument
69 if document.documentElement
70 # For performance use Document Fragment
71 fragment = document.createDocumentFragment()
72
73 # Select all markable elements in the document, create markers
74 # for each of them, and position them on the page.
75 # Note that the markers are not given hints.
76 set = getMarkableElements(document)
77 markers = []
78 for i in [0...set.snapshotLength] by 1
79 element = set.snapshotItem(i)
80 if rect = getElementRect(element)
81 marker = new Marker(element)
82
83 marker.setPosition(rect)
84 fragment.appendChild(marker.markerElement)
85
86 marker.weight = rect.area * marker.calcBloomRating()
87
88 markers.push(marker)
89
90 container = createHintsContainer(document)
91 container.appendChild(fragment)
92 document.documentElement.appendChild(container)
93
94 for frame in document.defaultView.frames
95 markers = markers.concat(injectMarkers(frame.document))
96
97 return markers or []
98
99
100 createHintsContainer = (document) ->
101 container = document.createElement('div')
102 container.id = CONTAINER_ID
103 container.className = 'VimFxReset'
104 return container
105
106
107 # Returns elements that qualify for hint markers in hints mode.
108 # Generates and memoizes an XPath query internally
109 getMarkableElements = do ->
110 # Some preparations done on startup
111 elements = [
112 MARKABLE_ELEMENTS...
113 "*[#{ MARKABLE_ELEMENT_PROPERTIES.join(' or ') }]"
114 ]
115
116 reduce = (m, rule) -> m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
117 xpath = elements.reduce(reduce, []).join(' | ')
118
119 namespaceResolver = (namespace) ->
120 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
121
122 # The actual function that will return the desired elements
123 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
124 return document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null)
125
126
127 # Uses `element.getBoundingClientRect()`. If that does not return a visible rectange, then looks at
128 # the children of the markable node.
129 #
130 # The logic has been copied over from Vimiun originally.
131 getElementRect = (element) ->
132 document = element.ownerDocument
133 window = document.defaultView
134 docElem = document.documentElement
135 body = document.body
136
137 # Prune elements that aren't visible on the page
138 computedStyle = window.getComputedStyle(element, null)
139 if computedStyle
140 if computedStyle.getPropertyValue('visibility') != 'visible' or \
141 computedStyle.getPropertyValue('display') == 'none' or \
142 computedStyle.getPropertyValue('opacity') == '0'
143 return
144
145 clientTop = docElem.clientTop or body?.clientTop or 0
146 clientLeft = docElem.clientLeft or body?.clientLeft or 0
147 scrollTop = window.pageYOffset or docElem.scrollTop
148 scrollLeft = window.pageXOffset or docElem.scrollLeft
149
150 clientRect = element.getBoundingClientRect()
151
152 if isRectOk(clientRect, window)
153 return {
154 top: clientRect.top + scrollTop - clientTop
155 left: clientRect.left + scrollLeft - clientLeft
156 width: clientRect.width
157 height: clientRect.height
158 area: clientRect.width * clientRect.height
159 }
160
161 # If the rect has 0 dimensions, then check what's inside.
162 # Floated or absolutely positioned elements are of particular interest.
163 if clientRect.width is 0 or clientRect.height is 0
164 for childElement in element.children
165 if computedStyle = window.getComputedStyle(childElement, null)
166 if computedStyle.getPropertyValue('float') != 'none' or \
167 computedStyle.getPropertyValue('position') == 'absolute'
168
169 return getElementRect(childElement)
170
171 return
172
173
174 # Checks if the given TextRectangle object qualifies
175 # for its own Marker with respect to the `window` object
176 isRectOk = (rect, window) ->
177 minimum = 2
178 rect.width > minimum and rect.height > minimum and \
179 rect.top > -minimum and rect.left > -minimum and \
180 rect.top < window.innerHeight - minimum and \
181 rect.left < window.innerWidth - minimum
182
183
184 exports.injectHints = injectHints
185 exports.removeHints = removeHints
Imprint / Impressum