]> git.gir.st - VimFx.git/blob - packages/marker.coffee
Finally implemented hints and markers, as well as most of the commands. Began writing...
[VimFx.git] / packages / marker.coffee
1 { interfaces: Ci } = Components
2 XPathResult = Ci.nsIDOMXPathResult
3
4 HINTCHARS = 'asdfgercvhjkl;uinm'
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 "area[@href]"
24 "textarea"
25 "button"
26 "select"
27 "input[not(@type='hidden' or @disabled or @readonly)]"
28 ]
29
30
31 # Marker class wraps the markable element and provides
32 # methods to manipulate the markers
33 class Marker
34 # Creates the marker DOM node
35 constructor: (@element) ->
36 document = @element.ownerDocument
37 window = document.defaultView
38 @markerElement = document.createElement 'div'
39 @markerElement.className = 'vimffReset vimffHintMarker'
40
41 # Hides the marker
42 hide: -> @markerElement.style.display = 'none'
43
44 # Shows the marker
45 show: -> @markerElement.style.display = 'block'
46
47 # Positions the marker on the page. The positioning is absulute
48 setPosition: (rect) ->
49 @markerElement.style.left = rect.left + 'px'
50 @markerElement.style.top = rect.top + 'px'
51
52 # Assigns hint string to the marker
53 setHint: (@hintChars) ->
54 # number of hint chars that have been matched so far
55 @matchedHintCharCount = 0
56
57 document = @element.ownerDocument
58
59 while @markerElement.hasChildNodes()
60 @markerElement.removeChild @markedElement.firstChild
61
62 for char in @hintChars
63 span = document.createElement 'span'
64 span.className = 'vimffReset'
65 span.textContent = char.toUpperCase()
66
67 @markerElement.appendChild span
68
69 matchHintChar: (char) ->
70 if char == 'backspace'
71 if @matchedHintCharCount > 0
72 @matchedHintCharCount -= 1
73 @markerElement.children[@matchedHintCharCount].className = 'vimffReset'
74 else
75 if @hintChars[@matchedHintCharCount] == char
76 @markerElement.children[@matchedHintCharCount].className = 'vimffReset vimffCharMatch'
77 @matchedHintCharCount += 1
78
79 return @matchedHintCharCount
80
81 isComplete: ->
82 return @hintChars.length == @hintCompletion
83
84
85 # Selects all markable elements on the page, creates markers
86 # for each of them The markers are then positioned on the page
87 #
88 # The array of markers is returned
89 Marker.createMarkers = (document) ->
90 elementsSet = getMarkableElements(document)
91 markers = {};
92 j = 0
93 for i in [0...elementsSet.snapshotLength] by 1
94 element = elementsSet.snapshotItem(i)
95 if rect = getElementRect element
96 hint = indexToHint(j++)
97 marker = new Marker(element)
98 marker.setPosition rect
99 marker.setHint hint
100 markers[hint] = marker
101
102 return markers
103
104 # Function generator that creates a function that
105 # returns hint string for supplied numeric index.
106 indexToHint = do ->
107 # split the characters into two groups:
108 #
109 # * left chars are used for the head
110 # * right chars are used to build the tail
111 left = HINTCHARS[...HINTCHARS.length / 3]
112 right = HINTCHARS[HINTCHARS.length / 3...]
113
114 # Helper function that returns a permutation number `i`
115 # of some of the characters in the `chars` agrument
116 f = (i, chars) ->
117 return '' if i < 0
118
119 n = chars.length
120 l = Math.floor(i / n); k = i % n;
121
122 return f(l - 1, chars) + chars[k]
123
124 return (i) ->
125 n = Math.floor(i / left.length)
126 m = i % left.length
127 return f(n - 1, right) + left[m]
128
129
130 # Returns elements that qualify for hint markers in hints mode.
131 # Generates and memoizes an XPath query internally
132 getMarkableElements = do ->
133 # Some preparations done on startup
134 elements = Array.concat \
135 MARKABLE_ELEMENTS,
136 ["*[#{ MARKABLE_ELEMENT_PROPERTIES.join(" or ") }]"]
137
138 xpath = elements.reduce((m, rule) ->
139 m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
140 , []).join(' | ')
141
142 namespaceResolver = (namespace) ->
143 if (namespace == "xhtml") then "http://www.w3.org/1999/xhtml" else null
144
145 # The actual function that will return the desired elements
146 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
147 document.evaluate xpath, document.documentElement, namespaceResolver, resultType, null
148
149 # Checks if the given TextRectangle object qualifies
150 # for its own Marker with respect to the `window` object
151 isRectOk = (rect, window) ->
152 rect.width > 2 and rect.height > 2 and \
153 rect.top > -2 and rect.left > -2 and \
154 rect.top < window.innerHeight - 2 and \
155 rect.left < window.innerWidth - 2
156
157 # Will scan through `element.getClientRects()` and look for
158 # the first visible rectange. If there are no visible rectangles, then
159 # will look at the children of the markable node.
160 #
161 # The logic has been copied over from Vimiun
162 getElementRect = (element) ->
163 document = element.ownerDocument
164 window = document.defaultView
165 docElem = document.documentElement
166 body = document.body
167
168 clientTop = docElem.clientTop || body.clientTop || 0;
169 clientLeft = docElem.clientLeft || body.clientLeft || 0;
170 scrollTop = window.pageYOffset || docElem.scrollTop;
171 scrollLeft = window.pageXOffset || docElem.scrollLeft;
172
173 rects = [rect for rect in element.getClientRects()]
174 rects.push element.getBoundingClientRect()
175
176 for rect in rects
177 if isRectOk rect, window
178 return {
179 top: rect.top + scrollTop - clientTop
180 left: rect.left + scrollLeft - clientLeft
181 width: rect.width
182 height: rect.height
183 }
184
185 # If the element has 0 dimentions then check what's inside.
186 # Floated or absolutely positioned elements are of particular interest
187 for rect in rects
188 if rect.width == 0 or rect.height == 0
189 for childElement in element.children
190 computedStyle = window.getComputedStyle childElement, null
191 if computedStyle.getPropertyValue 'float' != 'none' or \
192 computedStyle.getPropertyValue 'position' == 'absolute'
193
194 childRect if childRect = getElementRect childElement
195
196 return undefined
197
198 exports.Marker = Marker
Imprint / Impressum