1 utils = require 'utils'
2 { getPref } = require 'prefs'
3 { Marker } = require 'mode-hints/marker'
4 { addHuffmanCodeWordsTo } = require 'mode-hints/huffman'
6 { interfaces: Ci } = Components
8 HTMLDocument = Ci.nsIDOMHTMLDocument
9 XULDocument = Ci.nsIDOMXULDocument
11 CONTAINER_ID = 'VimFxHintMarkerContainer'
12 Z_INDEX_START = 100000000 # The highest `z-index` used in style.css plus one
14 # All the following elements qualify for their own marker in hints mode
22 "input[not(@type='hidden' or @disabled)]"
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 = [
37 "contains(@class, 'button')"
38 "contains(@class, 'js-new-tweets-bar')"
39 "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true'"
43 # Remove previously injected hints from the DOM
44 removeHints = (document) ->
45 document.getElementById(CONTAINER_ID)?.remove()
47 for frame in document.defaultView.frames
48 removeHints(frame.document)
51 injectHints = (document) ->
52 markers = createMarkers(document)
53 hintChars = utils.getHintChars()
55 # Each marker gets a unique `z-index`, so that it can be determined if a marker overlaps another.
56 # Put more important markers (higher weight) at the end, so that they get higher `z-index`, in
57 # order not to be overlapped.
58 markers.sort((a, b) -> a.weight - b.weight)
59 for marker, index in markers
60 marker.markerElement.style.setProperty('z-index', Z_INDEX_START + index, 'important')
62 addHuffmanCodeWordsTo(markers, {alphabet: hintChars}, (marker, hint) -> marker.setHint(hint))
67 # Must be done after the hints have been inserted into the DOM (see marker.coffee)
69 marker.completePosition()
73 insertHints = (markers) ->
76 getFrag = (document) ->
77 for [doc, frag] in docFrags
82 doc = marker.element.ownerDocument
84 docFrags.push([doc, doc.createDocumentFragment()])
87 frag.appendChild(marker.markerElement)
89 for [doc, frag] in docFrags
90 container = createHintsContainer(doc)
91 container.appendChild(frag)
92 doc.documentElement.appendChild(container)
95 # Creates and injects markers into the DOM
96 createMarkers = (document) ->
97 # For now we aren't able to handle hint markers in XUL Documents :(
98 if document instanceof HTMLDocument # or document instanceof XULDocument
99 if document.documentElement
100 # Select all markable elements in the document, create markers
101 # for each of them, and position them on the page.
102 # Note that the markers are not given hints.
103 set = getMarkableElements(document)
105 for i in [0...set.snapshotLength] by 1
106 element = set.snapshotItem(i)
107 if rect = getElementRect(element)
108 marker = new Marker(element)
109 marker.setPosition(rect.top, rect.left)
110 marker.weight = rect.area * marker.calcBloomRating()
114 for frame in document.defaultView.frames
115 markers = markers.concat(createMarkers(frame.document))
120 createHintsContainer = (document) ->
121 container = utils.createElement(document, 'div', {id: CONTAINER_ID})
125 # Returns elements that qualify for hint markers in hints mode.
126 getMarkableElements = do ->
129 "*[#{ MARKABLE_ELEMENT_PROPERTIES.join(' or ') }]"
132 return utils.getDomElements(elements)
135 # Uses `element.getBoundingClientRect()`. If that does not return a visible rectange, then looks at
136 # the children of the markable node.
138 # The logic has been copied over from Vimiun originally.
139 getElementRect = (element) ->
140 document = element.ownerDocument
141 window = document.defaultView
142 docElem = document.documentElement
145 # Prune elements that aren't visible on the page
146 computedStyle = window.getComputedStyle(element, null)
148 if computedStyle.getPropertyValue('visibility') != 'visible' or \
149 computedStyle.getPropertyValue('display') == 'none' or \
150 computedStyle.getPropertyValue('opacity') == '0'
153 clientTop = docElem.clientTop or body?.clientTop or 0
154 clientLeft = docElem.clientLeft or body?.clientLeft or 0
155 scrollTop = window.pageYOffset or docElem.scrollTop
156 scrollLeft = window.pageXOffset or docElem.scrollLeft
158 clientRect = element.getBoundingClientRect()
160 if isRectOk(clientRect, window)
162 top: clientRect.top + scrollTop - clientTop
163 left: clientRect.left + scrollLeft - clientLeft
164 width: clientRect.width
165 height: clientRect.height
166 area: clientRect.width * clientRect.height
169 # If the rect has 0 dimensions, then check what's inside.
170 # Floated or absolutely positioned elements are of particular interest.
171 if clientRect.width is 0 or clientRect.height is 0
172 for childElement in element.children
173 if computedStyle = window.getComputedStyle(childElement, null)
174 if computedStyle.getPropertyValue('float') != 'none' or \
175 computedStyle.getPropertyValue('position') == 'absolute'
177 return getElementRect(childElement)
182 # Checks if the given TextRectangle object qualifies
183 # for its own Marker with respect to the `window` object
184 isRectOk = (rect, window) ->
186 rect.width > minimum and rect.height > minimum and \
187 rect.top > -minimum and rect.left > -minimum and \
188 rect.top < window.innerHeight - minimum and \
189 rect.left < window.innerWidth - minimum
193 # Finds all stacks of markers that overlap each other (by using `getStackFor`) (#1), and rotates
194 # their `z-index`:es (#2), thus alternating which markers are visible.
195 rotateOverlappingMarkers = (originalMarkers, forward) ->
196 # Shallow working copy. This is necessary since `markers` will be mutated and eventually empty.
197 markers = originalMarkers[..]
200 stacks = (getStackFor(markers.pop(), markers) while markers.length > 0)
203 # Stacks of length 1 don't participate in any overlapping, and can therefore be skipped.
204 for stack in stacks when stack.length > 1
205 # This sort is not required, but makes the rotation more predictable.
206 stack.sort((a, b) -> a.markerElement.style.zIndex - b.markerElement.style.zIndex)
209 indexStack = (marker.markerElement.style.zIndex for marker in stack)
210 # Shift the array of indices one item forward or back
212 indexStack.unshift(indexStack.pop())
214 indexStack.push(indexStack.shift())
216 for marker, index in stack
217 marker.markerElement.style.setProperty('z-index', indexStack[index], 'important')
221 # Get an array containing `marker` and all markers that overlap `marker`, if any, which is called
222 # a "stack". All markers in the returned stack are spliced out from `markers`, thus mutating it.
223 getStackFor = (marker, markers) ->
226 { top, bottom, left, right } = marker.position
229 while index < markers.length
230 nextMarker = markers[index]
232 { top: nextTop, bottom: nextBottom, left: nextLeft, right: nextRight } = nextMarker.position
233 overlapsVertically = (nextBottom >= top and nextTop <= bottom)
234 overlapsHorizontally = (nextRight >= left and nextLeft <= right)
236 if overlapsVertically and overlapsHorizontally
237 # Also get all markers overlapping this one
238 markers.splice(index, 1)
239 stack = stack.concat(getStackFor(nextMarker, markers))
241 # Continue the search
247 exports.injectHints = injectHints
248 exports.removeHints = removeHints
249 exports.rotateOverlappingMarkers = rotateOverlappingMarkers