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