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