]> git.gir.st - VimFx.git/blob - extension/packages/mode-hints/hints.coffee
Raise the z-indexes used to trump youtube
[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 = 2147480001 # The highest `z-index` used in style.css plus one.
13 # In theory, `z-index` can be infinitely large. In practice, Firefox uses a
14 # 32-bit signed integer to store it, so the maximum value is 2147483647
15 # (http://www.puidokas.com/max-z-index/). Youtube (insanely) uses 1999999999
16 # for its top bar. So by using 2147480001 as a base, we trump that value with
17 # lots of margin, still leaving a few thousand values for markers, which should
18 # be more than enough. Hopefully no sites are crazy enough to use even higher
19 # values.
20
21 # Remove previously injected hints from the DOM
22 removeHints = (document) ->
23 document.getElementById(CONTAINER_ID)?.remove()
24
25 for frame in document.defaultView.frames
26 removeHints(frame.document)
27
28
29 injectHints = (document) ->
30 markers = createMarkers(document)
31 hintChars = utils.getHintChars()
32
33 # Each marker gets a unique `z-index`, so that it can be determined if a marker overlaps another.
34 # Put more important markers (higher weight) at the end, so that they get higher `z-index`, in
35 # order not to be overlapped.
36 markers.sort((a, b) -> a.weight - b.weight)
37 for marker, index in markers
38 marker.markerElement.style.setProperty('z-index', Z_INDEX_START + index, 'important')
39
40 addHuffmanCodeWordsTo(markers, {alphabet: hintChars}, (marker, hint) -> marker.setHint(hint))
41
42 removeHints(document)
43 insertHints(markers)
44
45 # Must be done after the hints have been inserted into the DOM (see marker.coffee)
46 for marker in markers
47 marker.completePosition()
48
49 return markers
50
51
52 insertHints = (markers) ->
53 docFrags = []
54
55 getFrag = (document) ->
56 for [doc, frag] in docFrags
57 if document == doc
58 return frag
59
60 for marker in markers
61 doc = marker.element.ownerDocument
62 if not getFrag(doc)
63 docFrags.push([doc, doc.createDocumentFragment()])
64
65 frag = getFrag(doc)
66 frag.appendChild(marker.markerElement)
67
68 for [doc, frag] in docFrags
69 container = createHintsContainer(doc)
70 container.appendChild(frag)
71 doc.documentElement.appendChild(container)
72
73
74 createMarkers = (document) ->
75 # For now we aren't able to handle hint markers in XUL Documents :(
76 if document instanceof HTMLDocument # or document instanceof XULDocument
77 if document.documentElement
78 # Select all markable elements in the document, create markers
79 # for each of them, and position them on the page.
80 # Note that the markers are not given hints.
81 set = utils.getMarkableElements(document, {type: 'all'})
82 markers = []
83 for element in set
84 if rect = getElementRect(element)
85 marker = new Marker(element)
86 marker.setPosition(rect.top, rect.left)
87 marker.weight = rect.area * marker.calcBloomRating()
88
89 markers.push(marker)
90
91 for frame in document.defaultView.frames
92 markers = markers.concat(createMarkers(frame.document))
93
94 return markers or []
95
96
97 createHintsContainer = (document) ->
98 container = utils.createElement(document, 'div', {id: CONTAINER_ID})
99 return container
100
101
102 # Uses `element.getBoundingClientRect()`. If that does not return a visible rectange, then looks at
103 # the children of the markable node.
104 #
105 # The logic has been copied over from Vimiun originally.
106 getElementRect = (element) ->
107 document = element.ownerDocument
108 window = document.defaultView
109 docElem = document.documentElement
110 body = document.body
111
112 return unless utils.isElementVisible(element)
113
114 clientTop = docElem.clientTop or body?.clientTop or 0
115 clientLeft = docElem.clientLeft or body?.clientLeft or 0
116 scrollTop = window.pageYOffset or docElem.scrollTop
117 scrollLeft = window.pageXOffset or docElem.scrollLeft
118
119 clientRect = element.getBoundingClientRect()
120
121 if isRectOk(clientRect, window)
122 return {
123 top: clientRect.top + scrollTop - clientTop
124 left: clientRect.left + scrollLeft - clientLeft
125 width: clientRect.width
126 height: clientRect.height
127 area: clientRect.width * clientRect.height
128 }
129
130 # If the rect has 0 dimensions, then check what's inside.
131 # Floated or absolutely positioned elements are of particular interest.
132 if clientRect.width is 0 or clientRect.height is 0
133 for childElement in element.children
134 if computedStyle = window.getComputedStyle(childElement, null)
135 if computedStyle.getPropertyValue('float') != 'none' or \
136 computedStyle.getPropertyValue('position') == 'absolute'
137
138 return getElementRect(childElement)
139
140 return
141
142
143 # Checks if the given TextRectangle object qualifies
144 # for its own Marker with respect to the `window` object
145 isRectOk = (rect, window) ->
146 minimum = 2
147 rect.width > minimum and rect.height > minimum and \
148 rect.top > -minimum and rect.left > -minimum and \
149 rect.top < window.innerHeight - minimum and \
150 rect.left < window.innerWidth - minimum
151
152
153
154 # Finds all stacks of markers that overlap each other (by using `getStackFor`) (#1), and rotates
155 # their `z-index`:es (#2), thus alternating which markers are visible.
156 rotateOverlappingMarkers = (originalMarkers, forward) ->
157 # Shallow working copy. This is necessary since `markers` will be mutated and eventually empty.
158 markers = originalMarkers[..]
159
160 # (#1)
161 stacks = (getStackFor(markers.pop(), markers) while markers.length > 0)
162
163 # (#2)
164 # Stacks of length 1 don't participate in any overlapping, and can therefore be skipped.
165 for stack in stacks when stack.length > 1
166 # This sort is not required, but makes the rotation more predictable.
167 stack.sort((a, b) -> a.markerElement.style.zIndex - b.markerElement.style.zIndex)
168
169 # Array of z-indices
170 indexStack = (marker.markerElement.style.zIndex for marker in stack)
171 # Shift the array of indices one item forward or back
172 if forward
173 indexStack.unshift(indexStack.pop())
174 else
175 indexStack.push(indexStack.shift())
176
177 for marker, index in stack
178 marker.markerElement.style.setProperty('z-index', indexStack[index], 'important')
179
180 return
181
182 # Get an array containing `marker` and all markers that overlap `marker`, if any, which is called
183 # a "stack". All markers in the returned stack are spliced out from `markers`, thus mutating it.
184 getStackFor = (marker, markers) ->
185 stack = [marker]
186
187 { top, bottom, left, right } = marker.position
188
189 index = 0
190 while index < markers.length
191 nextMarker = markers[index]
192
193 { top: nextTop, bottom: nextBottom, left: nextLeft, right: nextRight } = nextMarker.position
194 overlapsVertically = (nextBottom >= top and nextTop <= bottom)
195 overlapsHorizontally = (nextRight >= left and nextLeft <= right)
196
197 if overlapsVertically and overlapsHorizontally
198 # Also get all markers overlapping this one
199 markers.splice(index, 1)
200 stack = stack.concat(getStackFor(nextMarker, markers))
201 else
202 # Continue the search
203 index++
204
205 return stack
206
207
208 exports.injectHints = injectHints
209 exports.removeHints = removeHints
210 exports.rotateOverlappingMarkers = rotateOverlappingMarkers
Imprint / Impressum