1 { getPref } = require 'prefs'
2 utils = require 'utils'
4 { interfaces: Ci } = Components
6 XPathResult = Ci.nsIDOMXPathResult
8 # All elements that have one or more of the following properties
9 # qualify for their own marker in hints mode
10 MARKABLE_ELEMENT_PROPERTIES = [
18 "contains(@class, 'button')"
19 "contains(@class, 'js-new-tweets-bar')"
20 "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true'"
23 # All the following elements qualify for their own marker in hints mode
31 "input[not(@type='hidden' or @disabled or @readonly)]"
36 # Marker class wraps the markable element and provides
37 # methods to manipulate the markers
39 # Creates the marker DOM node
40 constructor: (@element) ->
41 document = @element.ownerDocument
42 window = document.defaultView
43 @markerElement = document.createElement('div')
44 @markerElement.className = 'VimFxReset VimFxHintMarker'
47 show: -> @markerElement.className = 'VimFxReset VimFxHintMarker'
50 hide: -> @markerElement.className = 'VimFxReset VimFxHiddenHintMarker'
52 # Positions the marker on the page. The positioning is absulute
53 setPosition: (rect) ->
54 @markerElement.style.left = rect.left + 'px'
55 @markerElement.style.top = rect.top + 'px'
57 # Assigns hint string to the marker
58 setHint: (@hintChars) ->
59 # number of hint chars that have been matched so far
60 @enteredHintChars = ''
62 document = @element.ownerDocument
64 while @markerElement.hasChildNodes()
65 @markerElement.removeChild(@markerElement.firstChild)
67 fragment = document.createDocumentFragment()
68 for char in @hintChars
69 span = document.createElement('span')
70 span.className = 'VimFxReset'
71 span.textContent = char.toUpperCase()
73 fragment.appendChild(span)
75 @markerElement.appendChild(fragment)
77 # Add another char to the `enteredHintString`,
78 # see if it still matches `hintString`, apply classes to
79 # the distinct hint characters and show/hide marker when
80 # the entered string partially (not) matches the hint string
81 matchHintChar: (char) ->
82 # Handle backspace key by removing a previously entered hint char
83 # and resetting its class
84 if char == 'Backspace'
85 if @enteredHintChars.length > 0
86 @enteredHintChars = @enteredHintChars[0...-1]
87 @markerElement.children[@enteredHintChars.length]?.className = 'VimFxReset'
88 # Otherwise append hint char and change hint class
90 @markerElement.children[@enteredHintChars.length]?.className = 'VimFxReset VimFxCharMatch'
91 @enteredHintChars += char.toLowerCase()
93 # If entered hint chars no longer partially match the hint chars
94 # then hide the marker. Othersie show it back
95 if @hintChars.search(@enteredHintChars) == 0 then @show() else @hide()
97 # Checks if the marker will be matched if the next character entered is `char`
99 char == 'Backspace' or @hintChars.search(@enteredHintChars + char.toLowerCase()) == 0
101 # Checks if enterd hint chars completely match the hint chars
103 return @hintChars == @enteredHintChars
106 # Selects all markable elements on the page, creates markers
107 # for each of them The markers are then positioned on the page
109 # The array of markers is returned
110 Marker.createMarkers = (document, startIndex) ->
111 hintChars = utils.getHintChars()
113 set = getMarkableElements(document)
117 for i in [0...set.snapshotLength] by 1
118 e = set.snapshotItem(i)
119 if rect = getElementRect(e)
120 elements.push([e, rect])
122 elements.sort ([e1, r1], [e2, r2]) ->
123 # <a> links should always be on the top. E.g. not links should go down
124 e1tagName = e1.tagName.toLowerCase()
125 e2tagName = e2.tagName.toLowerCase()
126 if e1tagName == 'a' and e2tagName != 'a'
128 else if e1tagName != 'a' and e2tagName == 'a'
130 else if r1.area < r2.area
132 else if r1.area > r2.area
137 # start from the end because the list is sorted in ascending order
138 j = elements.length + startIndex - 1
139 for [element, rect] in elements
140 # Get a hint for an element
141 hint = indexToHint(--j, hintChars)
142 marker = new Marker(element)
143 marker.setPosition(rect)
149 # Function generator that creates a function that
150 # returns hint string for supplied numeric index.
152 # Helper function that returns a permutation number `i`
153 # of some of the characters in the `chars` agrument
158 l = Math.floor(i / n); k = i % n
160 return f(l - 1, chars) + chars[k]
163 # split the characters into two groups:
165 # * left chars are used for the head
166 # * right chars are used to build the tail
167 left = chars[...chars.length / 3]
168 right = chars[chars.length / 3...]
170 n = Math.floor(i / left.length)
172 return f(n - 1, right) + left[m]
175 # Returns elements that qualify for hint markers in hints mode.
176 # Generates and memoizes an XPath query internally
177 getMarkableElements = do ->
178 # Some preparations done on startup
179 elements = Array.concat \
181 ["*[#{ MARKABLE_ELEMENT_PROPERTIES.join(' or ') }]"]
183 xpath = elements.reduce((m, rule) ->
184 m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
187 namespaceResolver = (namespace) ->
188 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
190 # The actual function that will return the desired elements
191 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
192 document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null)
194 # Checks if the given TextRectangle object qualifies
195 # for its own Marker with respect to the `window` object
196 isRectOk = (rect, window) ->
197 rect.width > 2 and rect.height > 2 and \
198 rect.top > -2 and rect.left > -2 and \
199 rect.top < window.innerHeight - 2 and \
200 rect.left < window.innerWidth - 2
202 # Will scan through `element.getClientRects()` and look for
203 # the first visible rectange. If there are no visible rectangles, then
204 # will look at the children of the markable node.
206 # The logic has been copied over from Vimiun
207 getElementRect = (element) ->
208 document = element.ownerDocument
209 window = document.defaultView
210 docElem = document.documentElement
213 clientTop = docElem.clientTop || body?.clientTop || 0
214 clientLeft = docElem.clientLeft || body?.clientLeft || 0
215 scrollTop = window.pageYOffset || docElem.scrollTop
216 scrollLeft = window.pageXOffset || docElem.scrollLeft
218 clientRect = element.getBoundingClientRect()
219 rects = [rect for rect in element.getClientRects()]
220 rects.push(clientRect)
223 if isRectOk(rect, window)
225 top: rect.top + scrollTop - clientTop
226 left: rect.left + scrollLeft - clientLeft
229 area: clientRect.width * clientRect.height
232 # If the element has 0 dimentions then check what's inside.
233 # Floated or absolutely positioned elements are of particular interest
235 if rect.width == 0 or rect.height == 0
236 for childElement in element.children
237 if computedStyle = window.getComputedStyle(childElement, null)
238 if computedStyle.getPropertyValue('float') != 'none' or \
239 computedStyle.getPropertyValue('position') == 'absolute'
241 return childRect if childRect = getElementRect(childElement)
245 exports.Marker = Marker