1 { interfaces: Ci } = Components
2 XPathResult = Ci.nsIDOMXPathResult
4 { getPref } = require 'prefs'
6 # All elements that have one or more of the following properties
7 # qualify for their own marker in hints mode
8 MARKABLE_ELEMENT_PROPERTIES = [
16 "contains(@class, 'button')"
17 "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true'"
20 # All the following elements qualify for their own marker in hints mode
27 "input[not(@type='hidden' or @disabled or @readonly)]"
31 # Marker class wraps the markable element and provides
32 # methods to manipulate the markers
34 # Creates the marker DOM node
35 constructor: (@element) ->
36 document = @element.ownerDocument
37 window = document.defaultView
38 @markerElement = document.createElement 'div'
39 @markerElement.className = 'VimFxReset VimFxHintMarker'
42 show: -> @markerElement.className = 'VimFxReset VimFxHintMarker'
45 hide: -> @markerElement.className = 'VimFxReset VimFxHiddenHintMarker'
47 # Positions the marker on the page. The positioning is absulute
48 setPosition: (rect) ->
49 @markerElement.style.left = rect.left + 'px'
50 @markerElement.style.top = rect.top + 'px'
52 # Assigns hint string to the marker
53 setHint: (@hintChars) ->
54 # number of hint chars that have been matched so far
55 @enteredHintChars = ''
57 document = @element.ownerDocument
59 while @markerElement.hasChildNodes()
60 @markerElement.removeChild @markedElement.firstChild
62 fragment = document.createDocumentFragment()
63 for char in @hintChars
64 span = document.createElement 'span'
65 span.className = 'VimFxReset'
66 span.textContent = char.toUpperCase()
68 fragment.appendChild span
70 @markerElement.appendChild fragment
72 # Add another char to the `enteredHintString`,
73 # see if it still matches `hintString`, apply classes to
74 # the distinct hint characters and show/hide marker when
75 # the entered string partially (not) matches the hint string
76 matchHintChar: (char) ->
77 # Handle backspace key by removing a previously entered hint char
78 # and resetting its class
79 if char == 'backspace'
80 if @enteredHintChars.length > 0
81 @enteredHintChars = @enteredHintChars.slice(0, -1)
82 @markerElement.children[@enteredHintChars.length]?.className = 'VimFxReset'
83 # Otherwise append hint char and change hint class
85 @markerElement.children[@enteredHintChars.length]?.className = 'VimFxReset VimFxCharMatch'
86 @enteredHintChars += char
88 # If entered hint chars no longer partially match the hint chars
89 # then hide the marker. Othersie show it back
90 if @hintChars.search(@enteredHintChars) == 0 then @show() else @hide()
92 # Checks if enterd hint chars completely match the hint chars
94 return @hintChars == @enteredHintChars
97 # Selects all markable elements on the page, creates markers
98 # for each of them The markers are then positioned on the page
100 # The array of markers is returned
101 Marker.createMarkers = (document) ->
102 hintChars = getPref 'hint_chars'
104 elementsSet = getMarkableElements(document)
107 for i in [0...elementsSet.snapshotLength] by 1
108 element = elementsSet.snapshotItem(i)
109 if rect = getElementRect element
110 hint = indexToHint(j++, hintChars)
111 marker = new Marker(element)
112 marker.setPosition rect
118 # Function generator that creates a function that
119 # returns hint string for supplied numeric index.
121 # Helper function that returns a permutation number `i`
122 # of some of the characters in the `chars` agrument
127 l = Math.floor(i / n); k = i % n;
129 return f(l - 1, chars) + chars[k]
132 # split the characters into two groups:
134 # * left chars are used for the head
135 # * right chars are used to build the tail
136 left = chars[...chars.length / 3]
137 right = chars[chars.length / 3...]
139 n = Math.floor(i / left.length)
141 return f(n - 1, right) + left[m]
144 # Returns elements that qualify for hint markers in hints mode.
145 # Generates and memoizes an XPath query internally
146 getMarkableElements = do ->
147 # Some preparations done on startup
148 elements = Array.concat \
150 ["*[#{ MARKABLE_ELEMENT_PROPERTIES.join(" or ") }]"]
152 xpath = elements.reduce((m, rule) ->
153 m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
156 namespaceResolver = (namespace) ->
157 if (namespace == "xhtml") then "http://www.w3.org/1999/xhtml" else null
159 # The actual function that will return the desired elements
160 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
161 document.evaluate xpath, document.documentElement, namespaceResolver, resultType, null
163 # Checks if the given TextRectangle object qualifies
164 # for its own Marker with respect to the `window` object
165 isRectOk = (rect, window) ->
166 rect.width > 2 and rect.height > 2 and \
167 rect.top > -2 and rect.left > -2 and \
168 rect.top < window.innerHeight - 2 and \
169 rect.left < window.innerWidth - 2
171 # Will scan through `element.getClientRects()` and look for
172 # the first visible rectange. If there are no visible rectangles, then
173 # will look at the children of the markable node.
175 # The logic has been copied over from Vimiun
176 getElementRect = (element) ->
177 document = element.ownerDocument
178 window = document.defaultView
179 docElem = document.documentElement
182 clientTop = docElem.clientTop || body.clientTop || 0;
183 clientLeft = docElem.clientLeft || body.clientLeft || 0;
184 scrollTop = window.pageYOffset || docElem.scrollTop;
185 scrollLeft = window.pageXOffset || docElem.scrollLeft;
187 rects = [rect for rect in element.getClientRects()]
188 rects.push element.getBoundingClientRect()
191 if isRectOk rect, window
193 top: rect.top + scrollTop - clientTop
194 left: rect.left + scrollLeft - clientLeft
199 # If the element has 0 dimentions then check what's inside.
200 # Floated or absolutely positioned elements are of particular interest
202 if rect.width == 0 or rect.height == 0
203 for childElement in element.children
204 computedStyle = window.getComputedStyle childElement, null
205 if computedStyle.getPropertyValue 'float' != 'none' or \
206 computedStyle.getPropertyValue 'position' == 'absolute'
208 childRect if childRect = getElementRect childElement
212 exports.Marker = Marker