]> git.gir.st - VimFx.git/blob - extension/lib/commands-frame.coffee
Use normal cursor in help dialog
[VimFx.git] / extension / lib / commands-frame.coffee
1 ###
2 # Copyright Simon Lydell 2015.
3 #
4 # This file is part of VimFx.
5 #
6 # VimFx is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # VimFx is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
18 ###
19
20 # This file is the equivalent to commands.coffee, but for frame scripts,
21 # allowing interaction with web page content. Most “commands” here have the
22 # same name as the command in commands.coffee that calls it. There are also a
23 # few more generalized “commands” used in more than one place.
24
25 hints = require('./hints')
26 utils = require('./utils')
27
28 {isProperLink, isTextInputElement, isTypingElement, isContentEditable} = utils
29
30 XULDocument = Ci.nsIDOMXULDocument
31
32 commands = {}
33
34 commands.go_up_path = ({vim, count = 1}) ->
35 vim.content.location.pathname = vim.content.location.pathname.replace(
36 /// (?: /[^/]+ ){1,#{count}} /?$ ///, ''
37 )
38
39 commands.go_to_root = ({vim}) ->
40 vim.content.location.href = vim.content.location.origin
41
42 commands.scroll = ({vim, method, type, direction, amount, property, smooth}) ->
43 activeElement = utils.getActiveElement(vim.content)
44 document = activeElement.ownerDocument
45
46 element = switch
47 when vim.state.scrollableElements.has(activeElement)
48 activeElement
49 # If the currently focused element isn’t scrollable, scroll the largest
50 # scrollable element instead, which usually means `<html>`.
51 when vim.state.scrollableElements.hasOrUpdateLargestScrollable()
52 vim.state.scrollableElements.largest
53 else
54 # If this point is reached, it _should_ mean that the page hasn’t got any
55 # scrollable elements, and the whole page itself isn’t scrollable. Instead
56 # of simply `return`ing, scroll the entire page (the best bet at this
57 # point) instead because we cannot be 100% sure that nothing is scrollable
58 # (for example, if VimFx is updated in the middle of a session). Not being
59 # able to scroll is very annoying.
60 vim.state.scrollableElements.quirks(document.documentElement)
61
62 options = {}
63 options[direction] = switch type
64 when 'lines' then amount
65 when 'pages' then amount * element[property]
66 when 'other' then Math.min(amount, element[property])
67 options.behavior = 'smooth' if smooth
68
69 element[method](options)
70
71 helper_follow = ({id, combine = true}, matcher, {vim}) ->
72 hrefs = {}
73 vim.state.markerElements = []
74
75 filter = (element, getElementShape) ->
76 {type, semantic} = matcher({vim, element, getElementShape})
77
78 customMatcher = FRAME_SCRIPT_ENVIRONMENT.VimFxHintMatcher
79 if customMatcher
80 {type, semantic} = customMatcher(id, element, {type, semantic})
81
82 return unless type
83 return unless shape = getElementShape(element)
84
85 length = vim.state.markerElements.push(element)
86 wrapper = {type, semantic, shape, elementIndex: length - 1}
87
88 # Combine links with the same href.
89 if combine and wrapper.type == 'link'
90 {href} = element
91 wrapper.href = href
92 if href of hrefs
93 parent = hrefs[href]
94 wrapper.parentIndex = parent.elementIndex
95 parent.shape.area += wrapper.shape.area
96 parent.numChildren++
97 else
98 wrapper.numChildren = 0
99 hrefs[href] = wrapper
100
101 return wrapper
102
103 return hints.getMarkableElementsAndViewport(vim.content, filter)
104
105 commands.follow = helper_follow.bind(null, {id: 'normal'},
106 ({vim, element, getElementShape}) ->
107 document = element.ownerDocument
108 isXUL = (document instanceof XULDocument)
109 type = null
110 semantic = true
111 switch
112 when isProperLink(element)
113 type = 'link'
114 when isTypingElement(element) or isContentEditable(element)
115 type = 'text'
116 when element.tabIndex > -1 and
117 not (isXUL and element.nodeName.endsWith('box') and
118 element.nodeName != 'checkbox')
119 type = 'clickable'
120 unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON']
121 semantic = false
122 when element != vim.state.scrollableElements.largest and
123 vim.state.scrollableElements.has(element)
124 type = 'scrollable'
125 when element.hasAttribute('onclick') or
126 element.hasAttribute('onmousedown') or
127 element.hasAttribute('onmouseup') or
128 element.hasAttribute('oncommand') or
129 element.getAttribute('role') in ['link', 'button'] or
130 # Twitter special-case.
131 element.classList.contains('js-new-tweets-bar') or
132 # Feedly special-case.
133 element.hasAttribute('data-app-action') or
134 element.hasAttribute('data-uri') or
135 element.hasAttribute('data-page-action')
136 type = 'clickable'
137 semantic = false
138 # Facebook special-case (comment fields).
139 when element.parentElement?.classList.contains('UFIInputContainer')
140 type = 'clickable-special'
141 # Putting markers on `<label>` elements is generally redundant, because
142 # its `<input>` gets one. However, some sites hide the actual `<input>`
143 # but keeps the `<label>` to click, either for styling purposes or to keep
144 # the `<input>` hidden until it is used. In those cases we should add a
145 # marker for the `<label>`.
146 when element.nodeName == 'LABEL'
147 input =
148 if element.htmlFor
149 document.getElementById(element.htmlFor)
150 else
151 element.querySelector('input, textarea, select')
152 if input and not getElementShape(input)
153 type = 'clickable'
154 # Elements that have “button” somewhere in the class might be clickable,
155 # unless they contain a real link or button or yet an element with
156 # “button” somewhere in the class, in which case they likely are
157 # “button-wrapper”s. (`<SVG element>.className` is not a string!)
158 when not isXUL and typeof element.className == 'string' and
159 element.className.toLowerCase().includes('button')
160 unless element.querySelector('a, button, [class*=button]')
161 type = 'clickable'
162 semantic = false
163 # When viewing an image it should get a marker to toggle zoom.
164 when document.body?.childElementCount == 1 and
165 element.nodeName == 'IMG' and
166 (element.classList.contains('overflowing') or
167 element.classList.contains('shrinkToFit'))
168 type = 'clickable'
169 return {type, semantic}
170 )
171
172 commands.follow_in_tab = helper_follow.bind(null, {id: 'tab'},
173 ({element}) ->
174 type = if isProperLink(element) then 'link' else null
175 return {type, semantic: true}
176 )
177
178 commands.follow_copy = helper_follow.bind(null, {id: 'copy'},
179 ({element}) ->
180 type = switch
181 when isProperLink(element) then 'link'
182 when isTypingElement(element) then 'text'
183 when isContentEditable(element) then 'contenteditable'
184 else null
185 return {type, semantic: true}
186 )
187
188 commands.follow_focus = helper_follow.bind(null, {id: 'focus', combine: false},
189 ({vim, element}) ->
190 type = switch
191 when element.tabIndex > -1
192 'focusable'
193 when element != vim.state.scrollableElements.largest and
194 vim.state.scrollableElements.has(element)
195 'scrollable'
196 else
197 null
198 return {type, semantic: true}
199 )
200
201 commands.focus_marker_element = ({vim, elementIndex, options}) ->
202 element = vim.state.markerElements[elementIndex]
203 utils.focusElement(element, options)
204
205 commands.click_marker_element = (args) ->
206 {vim, elementIndex, type, preventTargetBlank} = args
207 element = vim.state.markerElements[elementIndex]
208 if element.target == '_blank' and preventTargetBlank
209 targetReset = element.target
210 element.target = ''
211 if type == 'clickable-special'
212 element.click()
213 else
214 utils.simulateClick(element)
215 element.target = targetReset if targetReset
216
217 commands.copy_marker_element = ({vim, elementIndex, property}) ->
218 element = vim.state.markerElements[elementIndex]
219 utils.writeToClipboard(element[property])
220
221 commands.follow_pattern = ({vim, type, options}) ->
222 {document} = vim.content
223
224 # If there’s a `<link rel=prev/next>` element we use that.
225 for link in document.head?.getElementsByTagName('link')
226 # Also support `rel=previous`, just like Google.
227 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
228 vim.content.location.href = link.href
229 return
230
231 # Otherwise we look for a link or button on the page that seems to go to the
232 # previous or next page.
233 candidates = document.querySelectorAll(options.pattern_selector)
234
235 # Note: Earlier patterns should be favored.
236 {patterns} = options
237
238 # Search for the prev/next patterns in the following attributes of the
239 # element. `rel` should be kept as the first attribute, since the standard way
240 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
241 # favored. Even though some of these attributes only allow a fixed set of
242 # keywords, we pattern-match them anyways since lots of sites don’t follow the
243 # spec and use the attributes arbitrarily.
244 attrs = options.pattern_attrs
245
246 matchingLink = do ->
247 # First search in attributes (favoring earlier attributes) as it's likely
248 # that they are more specific than text contexts.
249 for attr in attrs
250 for regex in patterns
251 for element in candidates
252 return element if regex.test(element.getAttribute(attr))
253
254 # Then search in element contents.
255 for regex in patterns
256 for element in candidates
257 return element if regex.test(element.textContent)
258
259 return null
260
261 utils.simulateClick(matchingLink) if matchingLink
262
263 commands.focus_text_input = ({vim, count = null}) ->
264 {lastFocusedTextInput} = vim.state
265 inputs = Array.filter(
266 utils.querySelectorAllDeep(vim.content, 'input, textarea'), (element) ->
267 return isTextInputElement(element) and utils.area(element) > 0
268 )
269 if lastFocusedTextInput and lastFocusedTextInput not in inputs
270 inputs.push(lastFocusedTextInput)
271 return unless inputs.length > 0
272 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
273 unless count?
274 count =
275 if lastFocusedTextInput
276 inputs.indexOf(lastFocusedTextInput) + 1
277 else
278 1
279 index = Math.min(count, inputs.length) - 1
280 utils.focusElement(inputs[index], {select: true})
281 vim.state.inputs = inputs
282
283 commands.clear_inputs = ({vim}) ->
284 vim.state.inputs = null
285
286 commands.move_focus = ({vim, direction}) ->
287 return false unless vim.state.inputs
288 index = vim.state.inputs.indexOf(utils.getActiveElement(vim.content))
289 # If there’s only one input, `<tab>` would cycle to itself, making it feel
290 # like `<tab>` was not working. Then it’s better to let `<tab>` work as it
291 # usually does.
292 if index == -1 or vim.state.inputs.length <= 1
293 vim.state.inputs = null
294 return false
295 else
296 {inputs} = vim.state
297 nextInput = inputs[(index + direction) %% inputs.length]
298 utils.focusElement(nextInput, {select: true})
299 return true
300
301 commands.esc = (args) ->
302 commands.blur_active_element(args)
303
304 {document} = args.vim.content
305 if document.exitFullscreen
306 document.exitFullscreen()
307 else
308 document.mozCancelFullScreen()
309
310 commands.blur_active_element = ({vim}) ->
311 utils.blurActiveElement(vim.content)
312
313 module.exports = commands
Imprint / Impressum