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