]> git.gir.st - VimFx.git/blob - extension/packages/marker.coffee
Merge branch 'develop' into huffman
[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 "contains(@class, 'js-new-tweets-bar')"
20 "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true'"
21 ]
22
23 # All the following elements qualify for their own marker in hints mode
24 MARKABLE_ELEMENTS = [
25 "a"
26 "iframe"
27 "area[@href]"
28 "textarea"
29 "button"
30 "select"
31 "input[not(@type='hidden' or @disabled or @readonly)]"
32 "embed"
33 "object"
34 ]
35
36 # Marker class wraps the markable element and provides
37 # methods to manipulate the markers
38 class Marker
39 # Creates the marker DOM node
40 constructor: (@element) ->
41 document = @element.ownerDocument
42 window = document.defaultView
43 @markerElement = document.createElement('div')
44 @markerElement.className = 'VimFxReset VimFxHintMarker'
45
46 # Shows the marker
47 show: -> @markerElement.className = 'VimFxReset VimFxHintMarker'
48
49 # Hides the marker
50 hide: -> @markerElement.className = 'VimFxReset VimFxHiddenHintMarker'
51
52 # Positions the marker on the page. The positioning is absulute
53 setPosition: (rect) ->
54 @markerElement.style.left = rect.left + 'px'
55 @markerElement.style.top = rect.top + 'px'
56
57 # Assigns hint string to the marker
58 setHint: (@hintChars) ->
59 # number of hint chars that have been matched so far
60 @enteredHintChars = ''
61
62 document = @element.ownerDocument
63
64 while @markerElement.hasChildNodes()
65 @markerElement.removeChild(@markerElement.firstChild)
66
67 fragment = document.createDocumentFragment()
68 for char in @hintChars
69 span = document.createElement('span')
70 span.className = 'VimFxReset'
71 span.textContent = char.toUpperCase()
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 # Note that the markers are not given any hints at this point.
108 #
109 # The array of markers is returned
110 Marker.createMarkers = (document) ->
111 set = getMarkableElements(document)
112
113 markers = []
114 for i in [0...set.snapshotLength] by 1
115 element = set.snapshotItem(i)
116 if rect = getElementRect(element)
117 marker = new Marker(element)
118 marker.setPosition(rect)
119 marker.weight = rect.area
120 markers.push(marker)
121
122 return markers
123
124
125 # Returns elements that qualify for hint markers in hints mode.
126 # Generates and memoizes an XPath query internally
127 getMarkableElements = do ->
128 # Some preparations done on startup
129 elements = Array.concat \
130 MARKABLE_ELEMENTS,
131 ["*[#{ MARKABLE_ELEMENT_PROPERTIES.join(' or ') }]"]
132
133 xpath = elements.reduce((m, rule) ->
134 m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
135 , []).join(' | ')
136
137 namespaceResolver = (namespace) ->
138 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
139
140 # The actual function that will return the desired elements
141 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
142 document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null)
143
144 # Checks if the given TextRectangle object qualifies
145 # for its own Marker with respect to the `window` object
146 isRectOk = (rect, window) ->
147 rect.width > 2 and rect.height > 2 and \
148 rect.top > -2 and rect.left > -2 and \
149 rect.top < window.innerHeight - 2 and \
150 rect.left < window.innerWidth - 2
151
152 # Will scan through `element.getClientRects()` and look for
153 # the first visible rectange. If there are no visible rectangles, then
154 # will look at the children of the markable node.
155 #
156 # The logic has been copied over from Vimiun
157 getElementRect = (element) ->
158 document = element.ownerDocument
159 window = document.defaultView
160 docElem = document.documentElement
161 body = document.body
162
163 clientTop = docElem.clientTop || body?.clientTop || 0
164 clientLeft = docElem.clientLeft || body?.clientLeft || 0
165 scrollTop = window.pageYOffset || docElem.scrollTop
166 scrollLeft = window.pageXOffset || docElem.scrollLeft
167
168 clientRect = element.getBoundingClientRect()
169 rects = [rect for rect in element.getClientRects()]
170 rects.push(clientRect)
171
172 for rect in rects
173 if isRectOk(rect, window)
174 return {
175 top: rect.top + scrollTop - clientTop
176 left: rect.left + scrollLeft - clientLeft
177 width: rect.width
178 height: rect.height
179 area: clientRect.width * clientRect.height
180 }
181
182 # If the element has 0 dimentions then check what's inside.
183 # Floated or absolutely positioned elements are of particular interest
184 for rect in rects
185 if rect.width == 0 or rect.height == 0
186 for childElement in element.children
187 if computedStyle = window.getComputedStyle(childElement, null)
188 if computedStyle.getPropertyValue('float') != 'none' or \
189 computedStyle.getPropertyValue('position') == 'absolute'
190
191 return childRect if childRect = getElementRect(childElement)
192
193 return undefined
194
195 exports.Marker = Marker
Imprint / Impressum