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