]> git.gir.st - VimFx.git/blob - extension/packages/marker.coffee
Merge pull request #137 from ctype/delocale
[VimFx.git] / extension / packages / marker.coffee
1 { getPref } = require 'prefs'
2 utils = require 'utils'
3
4 { interfaces: Ci } = Components
5
6 XPathResult = Ci.nsIDOMXPathResult
7
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 = [
11 "@tabindex"
12 "@onclick"
13 "@onmousedown"
14 "@onmouseup"
15 "@oncommand"
16 "@role='link'"
17 "@role='button'"
18 "contains(@class, 'button')"
19 "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true'"
20 ]
21
22 # All the following elements qualify for their own marker in hints mode
23 MARKABLE_ELEMENTS = [
24 "a"
25 "iframe"
26 "area[@href]"
27 "textarea"
28 "button"
29 "select"
30 "input[not(@type='hidden' or @disabled or @readonly)]"
31 "embed"
32 "object"
33 ]
34
35 # Marker class wraps the markable element and provides
36 # methods to manipulate the markers
37 class Marker
38 # Creates the marker DOM node
39 constructor: (@element) ->
40 document = @element.ownerDocument
41 window = document.defaultView
42 @markerElement = document.createElement('div')
43 @markerElement.className = 'VimFxReset VimFxHintMarker'
44
45 # Shows the marker
46 show: -> @markerElement.className = 'VimFxReset VimFxHintMarker'
47
48 # Hides the marker
49 hide: -> @markerElement.className = 'VimFxReset VimFxHiddenHintMarker'
50
51 # Positions the marker on the page. The positioning is absulute
52 setPosition: (rect) ->
53 @markerElement.style.left = rect.left + 'px'
54 @markerElement.style.top = rect.top + 'px'
55
56 # Assigns hint string to the marker
57 setHint: (@hintChars) ->
58 # number of hint chars that have been matched so far
59 @enteredHintChars = ''
60
61 document = @element.ownerDocument
62
63 while @markerElement.hasChildNodes()
64 @markerElement.removeChild(@markedElement.firstChild)
65
66 fragment = document.createDocumentFragment()
67 for char in @hintChars
68 span = document.createElement('span')
69 span.className = 'VimFxReset'
70 span.textContent = char.toUpperCase()
71
72 fragment.appendChild(span)
73
74 @markerElement.appendChild(fragment)
75
76 # Add another char to the `enteredHintString`,
77 # see if it still matches `hintString`, apply classes to
78 # the distinct hint characters and show/hide marker when
79 # the entered string partially (not) matches the hint string
80 matchHintChar: (char) ->
81 # Handle backspace key by removing a previously entered hint char
82 # and resetting its class
83 if char == 'Backspace'
84 if @enteredHintChars.length > 0
85 @enteredHintChars = @enteredHintChars[0...-1]
86 @markerElement.children[@enteredHintChars.length]?.className = 'VimFxReset'
87 # Otherwise append hint char and change hint class
88 else
89 @markerElement.children[@enteredHintChars.length]?.className = 'VimFxReset VimFxCharMatch'
90 @enteredHintChars += char.toLowerCase()
91
92 # If entered hint chars no longer partially match the hint chars
93 # then hide the marker. Othersie show it back
94 if @hintChars.search(@enteredHintChars) == 0 then @show() else @hide()
95
96 # Checks if the marker will be matched if the next character entered is `char`
97 willMatch: (char) ->
98 char == 'Backspace' or @hintChars.search(@enteredHintChars + char.toLowerCase()) == 0
99
100 # Checks if enterd hint chars completely match the hint chars
101 isMatched: ->
102 return @hintChars == @enteredHintChars
103
104
105 # Selects all markable elements on the page, creates markers
106 # for each of them The markers are then positioned on the page
107 #
108 # The array of markers is returned
109 Marker.createMarkers = (document, startIndex) ->
110 hintChars = utils.getHintChars()
111
112 set = getMarkableElements(document)
113 markers = []
114
115 elements = []
116 for i in [0...set.snapshotLength] by 1
117 e = set.snapshotItem(i)
118 if rect = getElementRect(e)
119 elements.push([e, rect])
120
121 elements.sort ([e1, r1], [e2, r2]) ->
122 # <a> links should always be on the top. E.g. not links should go down
123 e1tagName = e1.tagName.toLowerCase()
124 e2tagName = e2.tagName.toLowerCase()
125 if e1tagName == 'a' and e2tagName != 'a'
126 return 1
127 else if e1tagName != 'a' and e2tagName == 'a'
128 return -1
129 else if r1.area < r2.area
130 return -1
131 else if r1.area > r2.area
132 return 1
133 else
134 return 0
135
136 # start from the end because the list is sorted in ascending order
137 j = elements.length + startIndex - 1
138 for [element, rect] in elements
139 # Get a hint for an element
140 hint = indexToHint(--j, hintChars)
141 marker = new Marker(element)
142 marker.setPosition(rect)
143 marker.setHint(hint)
144 markers.push(marker)
145
146 return markers
147
148 # Function generator that creates a function that
149 # returns hint string for supplied numeric index.
150 indexToHint = do ->
151 # Helper function that returns a permutation number `i`
152 # of some of the characters in the `chars` agrument
153 f = (i, chars) ->
154 return '' if i < 0
155
156 n = chars.length
157 l = Math.floor(i / n); k = i % n
158
159 return f(l - 1, chars) + chars[k]
160
161 return (i, chars) ->
162 # split the characters into two groups:
163 #
164 # * left chars are used for the head
165 # * right chars are used to build the tail
166 left = chars[...chars.length / 3]
167 right = chars[chars.length / 3...]
168
169 n = Math.floor(i / left.length)
170 m = i % left.length
171 return f(n - 1, right) + left[m]
172
173
174 # Returns elements that qualify for hint markers in hints mode.
175 # Generates and memoizes an XPath query internally
176 getMarkableElements = do ->
177 # Some preparations done on startup
178 elements = Array.concat \
179 MARKABLE_ELEMENTS,
180 ["*[#{ MARKABLE_ELEMENT_PROPERTIES.join(' or ') }]"]
181
182 xpath = elements.reduce((m, rule) ->
183 m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
184 , []).join(' | ')
185
186 namespaceResolver = (namespace) ->
187 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
188
189 # The actual function that will return the desired elements
190 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
191 document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null)
192
193 # Checks if the given TextRectangle object qualifies
194 # for its own Marker with respect to the `window` object
195 isRectOk = (rect, window) ->
196 rect.width > 2 and rect.height > 2 and \
197 rect.top > -2 and rect.left > -2 and \
198 rect.top < window.innerHeight - 2 and \
199 rect.left < window.innerWidth - 2
200
201 # Will scan through `element.getClientRects()` and look for
202 # the first visible rectange. If there are no visible rectangles, then
203 # will look at the children of the markable node.
204 #
205 # The logic has been copied over from Vimiun
206 getElementRect = (element) ->
207 document = element.ownerDocument
208 window = document.defaultView
209 docElem = document.documentElement
210 body = document.body
211
212 clientTop = docElem.clientTop || body?.clientTop || 0
213 clientLeft = docElem.clientLeft || body?.clientLeft || 0
214 scrollTop = window.pageYOffset || docElem.scrollTop
215 scrollLeft = window.pageXOffset || docElem.scrollLeft
216
217 clientRect = element.getBoundingClientRect()
218 rects = [rect for rect in element.getClientRects()]
219 rects.push(clientRect)
220
221 for rect in rects
222 if isRectOk(rect, window)
223 return {
224 top: rect.top + scrollTop - clientTop
225 left: rect.left + scrollLeft - clientLeft
226 width: rect.width
227 height: rect.height
228 area: clientRect.width * clientRect.height
229 }
230
231 # If the element has 0 dimentions then check what's inside.
232 # Floated or absolutely positioned elements are of particular interest
233 for rect in rects
234 if rect.width == 0 or rect.height == 0
235 for childElement in element.children
236 if computedStyle = window.getComputedStyle(childElement, null)
237 if computedStyle.getPropertyValue('float') != 'none' or \
238 computedStyle.getPropertyValue('position') == 'absolute'
239
240 return childRect if childRect = getElementRect(childElement)
241
242 return undefined
243
244 exports.Marker = Marker
Imprint / Impressum