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