]> git.gir.st - VimFx.git/blob - extension/packages/mode-hints/hints.coffee
Simplify `utils.createElement`
[VimFx.git] / extension / packages / mode-hints / hints.coffee
1 utils = require 'utils'
2 { getPref } = require 'prefs'
3 { Marker } = require 'mode-hints/marker'
4 { addHuffmanCodeWordsTo } = require 'mode-hints/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)]"
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 document.getElementById(CONTAINER_ID)?.remove()
46
47 for frame in document.defaultView.frames
48 removeHints(frame.document)
49
50
51 injectHints = (document) ->
52 markers = createMarkers(document)
53 hintChars = utils.getHintChars()
54
55 addHuffmanCodeWordsTo(markers, {alphabet: hintChars}, (marker, hint) -> marker.setHint(hint))
56
57 removeHints(document)
58 insertHints(markers)
59
60 # Must be done after the hints have been inserted into the DOM (see marker.coffee)
61 for marker in markers
62 marker.completePosition()
63
64 return markers
65
66 insertHints = (markers) ->
67 docFrags = []
68
69 getFrag = (document) ->
70 for [doc, frag] in docFrags
71 if document == doc
72 return frag
73
74 for marker in markers
75 doc = marker.element.ownerDocument
76 if not getFrag(doc)
77 docFrags.push([doc, doc.createDocumentFragment()])
78
79 frag = getFrag(doc)
80 frag.appendChild(marker.markerElement)
81
82 for [doc, frag] in docFrags
83 container = createHintsContainer(doc)
84 container.appendChild(frag)
85 doc.documentElement.appendChild(container)
86
87
88 # Creates and injects markers into the DOM
89 createMarkers = (document) ->
90 # For now we aren't able to handle hint markers in XUL Documents :(
91 if document instanceof HTMLDocument# or document instanceof XULDocument
92 if document.documentElement
93 # Select all markable elements in the document, create markers
94 # for each of them, and position them on the page.
95 # Note that the markers are not given hints.
96 set = getMarkableElements(document)
97 markers = []
98 for i in [0...set.snapshotLength] by 1
99 element = set.snapshotItem(i)
100 if rect = getElementRect(element)
101 marker = new Marker(element)
102 marker.setPosition(rect.top, rect.left)
103 marker.weight = rect.area * marker.calcBloomRating()
104
105 markers.push(marker)
106
107 for frame in document.defaultView.frames
108 markers = markers.concat(createMarkers(frame.document))
109
110 return markers or []
111
112
113 createHintsContainer = (document) ->
114 container = utils.createElement(document, 'div', id: CONTAINER_ID)
115 return container
116
117
118 # Returns elements that qualify for hint markers in hints mode.
119 # Generates and memoizes an XPath query internally
120 getMarkableElements = do ->
121 # Some preparations done on startup
122 elements = [
123 MARKABLE_ELEMENTS...
124 "*[#{ MARKABLE_ELEMENT_PROPERTIES.join(' or ') }]"
125 ]
126
127 reduce = (m, rule) -> m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
128 xpath = elements.reduce(reduce, []).join(' | ')
129
130 namespaceResolver = (namespace) ->
131 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
132
133 # The actual function that will return the desired elements
134 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
135 return document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null)
136
137
138 # Uses `element.getBoundingClientRect()`. If that does not return a visible rectange, then looks at
139 # the children of the markable node.
140 #
141 # The logic has been copied over from Vimiun originally.
142 getElementRect = (element) ->
143 document = element.ownerDocument
144 window = document.defaultView
145 docElem = document.documentElement
146 body = document.body
147
148 # Prune elements that aren't visible on the page
149 computedStyle = window.getComputedStyle(element, null)
150 if computedStyle
151 if computedStyle.getPropertyValue('visibility') != 'visible' or \
152 computedStyle.getPropertyValue('display') == 'none' or \
153 computedStyle.getPropertyValue('opacity') == '0'
154 return
155
156 clientTop = docElem.clientTop or body?.clientTop or 0
157 clientLeft = docElem.clientLeft or body?.clientLeft or 0
158 scrollTop = window.pageYOffset or docElem.scrollTop
159 scrollLeft = window.pageXOffset or docElem.scrollLeft
160
161 clientRect = element.getBoundingClientRect()
162
163 if isRectOk(clientRect, window)
164 return {
165 top: clientRect.top + scrollTop - clientTop
166 left: clientRect.left + scrollLeft - clientLeft
167 width: clientRect.width
168 height: clientRect.height
169 area: clientRect.width * clientRect.height
170 }
171
172 # If the rect has 0 dimensions, then check what's inside.
173 # Floated or absolutely positioned elements are of particular interest.
174 if clientRect.width is 0 or clientRect.height is 0
175 for childElement in element.children
176 if computedStyle = window.getComputedStyle(childElement, null)
177 if computedStyle.getPropertyValue('float') != 'none' or \
178 computedStyle.getPropertyValue('position') == 'absolute'
179
180 return getElementRect(childElement)
181
182 return
183
184
185 # Checks if the given TextRectangle object qualifies
186 # for its own Marker with respect to the `window` object
187 isRectOk = (rect, window) ->
188 minimum = 2
189 rect.width > minimum and rect.height > minimum and \
190 rect.top > -minimum and rect.left > -minimum and \
191 rect.top < window.innerHeight - minimum and \
192 rect.left < window.innerWidth - minimum
193
194
195
196 # Finds all stacks of markers that overlap each other (by using `getStackFor`) (#1), and rotates
197 # their `z-index`:es (#2), thus alternating which markers are visible.
198 rotateOverlappingMarkers = (originalMarkers, forward) ->
199 # Shallow working copy. This is necessary since `markers` will be mutated and eventually empty.
200 markers = originalMarkers[..]
201
202 # (#1)
203 stacks = (getStackFor(markers.pop(), markers) while markers.length > 0)
204
205 # (#2)
206 # Stacks of length 1 don't participate in any overlapping, and can therefore be skipped.
207 for stack in stacks when stack.length > 1
208 # This sort is not required, but makes the rotation more predictable.
209 stack.sort((a, b) -> a.markerElement.style.zIndex - b.markerElement.style.zIndex)
210
211 # Array of z-indices
212 indexStack = (marker.markerElement.style.zIndex for marker in stack)
213 # Shift the array of indices one item forward or back
214 if forward
215 indexStack.unshift(indexStack.pop())
216 else
217 indexStack.push(indexStack.shift())
218
219 for marker, index in stack
220 marker.markerElement.style.setProperty('z-index', indexStack[index], 'important')
221
222 return
223
224 # Get an array containing `marker` and all markers that overlap `marker`, if any, which is called
225 # a "stack". All markers in the returned stack are spliced out from `markers`, thus mutating it.
226 getStackFor = (marker, markers) ->
227 stack = [marker]
228
229 { top, bottom, left, right } = marker.position
230
231 index = 0
232 while index < markers.length
233 nextMarker = markers[index]
234
235 { top: nextTop, bottom: nextBottom, left: nextLeft, right: nextRight } = nextMarker.position
236 overlapsVertically = (nextBottom >= top and nextTop <= bottom)
237 overlapsHorizontally = (nextRight >= left and nextLeft <= right)
238
239 if overlapsVertically and overlapsHorizontally
240 # Also get all markers overlapping this one
241 markers.splice(index, 1)
242 stack = stack.concat(getStackFor(nextMarker, markers))
243 else
244 # Continue the search
245 index++
246
247 return stack
248
249
250 exports.injectHints = injectHints
251 exports.removeHints = removeHints
252 exports.rotateOverlappingMarkers = rotateOverlappingMarkers
Imprint / Impressum