]> git.gir.st - VimFx.git/blob - extension/lib/commands-frame.coffee
Make `gi` find text inputs in all frames
[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 = (args) ->
43 { vim, method, type, direction, amount, property, smooth } = args
44 activeElement = utils.getActiveElement(vim.content)
45 document = activeElement.ownerDocument
46
47 element = switch
48 when vim.state.scrollableElements.has(activeElement)
49 activeElement
50 # If the currently focused element isn’t scrollable, scroll the largest
51 # scrollable element instead, which usually means `<html>`.
52 when vim.state.scrollableElements.largest
53 { scrollableElements } = vim.state
54 # In quirks mode (when the page lacks a doctype) `<body>` is considered
55 # the root element rather than `<html>`. The 'overflow' event is triggered
56 # for `<html>` though (_not_ `<body>`!).
57 if scrollableElements.largest == document.documentElement and
58 document.compatMode == 'BackCompat' and document.body?
59 document.body
60 else
61 scrollableElements.largest
62 else
63 # This point should never be reached, but it’s better to be safe than
64 # sorry. Not being able to scroll is very annoying. Use the best bet.
65 document.documentElement
66
67 options = {}
68 options[direction] = switch type
69 when 'lines' then amount
70 when 'pages' then amount * element[property]
71 when 'other' then Math.min(amount, element[property])
72 options.behavior = 'smooth' if smooth
73
74 element[method](options)
75
76 # Combine links with the same href.
77 combine = (hrefs, element, wrapper) ->
78 if wrapper.type == 'link'
79 { href } = element
80 wrapper.href = href
81 if href of hrefs
82 parent = hrefs[href]
83 wrapper.parentIndex = parent.elementIndex
84 parent.shape.area += wrapper.shape.area
85 parent.numChildren++
86 else
87 wrapper.numChildren = 0
88 hrefs[href] = wrapper
89 return wrapper
90
91 commands.follow = ({ vim, storage }) ->
92 hrefs = {}
93 storage.markerElements = []
94 filter = (element, getElementShape) ->
95 document = element.ownerDocument
96 isXUL = (document instanceof XULDocument)
97 semantic = true
98 switch
99 when isProperLink(element)
100 type = 'link'
101 when isTypingElement(element) or isContentEditable(element)
102 type = 'text'
103 when element.tabIndex > -1 and
104 not (isXUL and element.nodeName.endsWith('box') and
105 element.nodeName != 'checkbox')
106 type = 'clickable'
107 unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON']
108 semantic = false
109 when element != document.documentElement and
110 vim.state.scrollableElements.has(element)
111 type = 'scrollable'
112 when element.hasAttribute('onclick') or
113 element.hasAttribute('onmousedown') or
114 element.hasAttribute('onmouseup') or
115 element.hasAttribute('oncommand') or
116 element.getAttribute('role') in ['link', 'button'] or
117 # Twitter special-case.
118 element.classList.contains('js-new-tweets-bar') or
119 # Feedly special-case.
120 element.hasAttribute('data-app-action') or
121 element.hasAttribute('data-uri') or
122 element.hasAttribute('data-page-action')
123 type = 'clickable'
124 semantic = false
125 # Facebook special-case (comment fields).
126 when element.parentElement?.classList.contains('UFIInputContainer')
127 type = 'clickable-special'
128 # Putting markers on `<label>` elements is generally redundant, because
129 # its `<input>` gets one. However, some sites hide the actual `<input>`
130 # but keeps the `<label>` to click, either for styling purposes or to keep
131 # the `<input>` hidden until it is used. In those cases we should add a
132 # marker for the `<label>`.
133 when element.nodeName == 'LABEL'
134 input =
135 if element.htmlFor
136 document.getElementById(element.htmlFor)
137 else
138 element.querySelector('input, textarea, select')
139 if input and not getElementShape(input)
140 type = 'clickable'
141 # Elements that have “button” somewhere in the class might be clickable,
142 # unless they contain a real link or button or yet an element with
143 # “button” somewhere in the class, in which case they likely are
144 # “button-wrapper”s. (`<SVG element>.className` is not a string!)
145 when not isXUL and typeof element.className == 'string' and
146 element.className.toLowerCase().includes('button')
147 unless element.querySelector('a, button, [class*=button]')
148 type = 'clickable'
149 semantic = false
150 # When viewing an image it should get a marker to toggle zoom.
151 when document.body?.childElementCount == 1 and
152 element.nodeName == 'IMG' and
153 (element.classList.contains('overflowing') or
154 element.classList.contains('shrinkToFit'))
155 type = 'clickable'
156 return unless type
157 return unless shape = getElementShape(element)
158 length = storage.markerElements.push(element)
159 return combine(
160 hrefs, element, {elementIndex: length - 1, shape, semantic, type}
161 )
162
163 return hints.getMarkableElementsAndViewport(vim.content, filter)
164
165 commands.follow_in_tab = ({ vim, storage }) ->
166 hrefs = {}
167 storage.markerElements = []
168 filter = (element, getElementShape) ->
169 return unless isProperLink(element)
170 return unless shape = getElementShape(element)
171 length = storage.markerElements.push(element)
172 return combine(
173 hrefs, element,
174 {elementIndex: length - 1, shape, semantic: true, type: 'link'}
175 )
176
177 return hints.getMarkableElementsAndViewport(vim.content, filter)
178
179 commands.follow_copy = ({ vim, storage }) ->
180 hrefs = {}
181 storage.markerElements = []
182 filter = (element, getElementShape) ->
183 type = switch
184 when isProperLink(element) then 'link'
185 when isTypingElement(element) then 'typing'
186 when isContentEditable(element) then 'contenteditable'
187 return unless type
188 return unless shape = getElementShape(element)
189 length = storage.markerElements.push(element)
190 return combine(
191 hrefs, element, {elementIndex: length - 1, shape, semantic: true, type}
192 )
193
194 return hints.getMarkableElementsAndViewport(vim.content, filter)
195
196 commands.follow_focus = ({ vim, storage }) ->
197 storage.markerElements = []
198 filter = (element, getElementShape) ->
199 type = switch
200 when element.tabIndex > -1
201 'focusable'
202 when element != element.ownerDocument.documentElement and
203 vim.state.scrollableElements.has(element)
204 'scrollable'
205 return unless type
206 return unless shape = getElementShape(element)
207 length = storage.markerElements.push(element)
208 return {elementIndex: length - 1, shape, semantic: true, type}
209
210 return hints.getMarkableElementsAndViewport(vim.content, filter)
211
212 commands.focus_marker_element = ({ storage, elementIndex, options }) ->
213 element = storage.markerElements[elementIndex]
214 utils.focusElement(element, options)
215
216 commands.click_marker_element = (args) ->
217 { vim, storage, elementIndex, preventTargetBlank, type } = args
218 element = storage.markerElements[elementIndex]
219 if element.target == '_blank' and preventTargetBlank
220 targetReset = element.target
221 element.target = ''
222 if type == 'clickable-special'
223 element.click()
224 else
225 utils.simulateClick(element)
226 element.target = targetReset if targetReset
227
228 commands.copy_marker_element = ({ storage, elementIndex, property }) ->
229 element = storage.markerElements[elementIndex]
230 utils.writeToClipboard(element[property])
231
232 commands.follow_pattern = ({ vim, type, options }) ->
233 { document } = vim.content
234
235 # If there’s a `<link rel=prev/next>` element we use that.
236 for link in document.head?.getElementsByTagName('link')
237 # Also support `rel=previous`, just like Google.
238 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
239 vim.content.location.href = link.href
240 return
241
242 # Otherwise we look for a link or button on the page that seems to go to the
243 # previous or next page.
244 candidates = document.querySelectorAll(options.pattern_selector)
245
246 # Note: Earlier patterns should be favored.
247 { patterns } = options
248
249 # Search for the prev/next patterns in the following attributes of the
250 # element. `rel` should be kept as the first attribute, since the standard way
251 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
252 # favored. Even though some of these attributes only allow a fixed set of
253 # keywords, we pattern-match them anyways since lots of sites don’t follow the
254 # spec and use the attributes arbitrarily.
255 attrs = options.pattern_attrs
256
257 matchingLink = do ->
258 # First search in attributes (favoring earlier attributes) as it's likely
259 # that they are more specific than text contexts.
260 for attr in attrs
261 for regex in patterns
262 for element in candidates
263 return element if regex.test(element.getAttribute(attr))
264
265 # Then search in element contents.
266 for regex in patterns
267 for element in candidates
268 return element if regex.test(element.textContent)
269
270 return null
271
272 utils.simulateClick(matchingLink) if matchingLink
273
274 commands.focus_text_input = ({ vim, storage, count = null }) ->
275 { lastFocusedTextInput } = vim.state
276 inputs = Array.filter(
277 utils.querySelectorAllDeep(vim.content, 'input, textarea'), (element) ->
278 return isTextInputElement(element) and utils.area(element) > 0
279 )
280 if lastFocusedTextInput and lastFocusedTextInput not in inputs
281 inputs.push(lastFocusedTextInput)
282 return unless inputs.length > 0
283 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
284 unless count?
285 count =
286 if lastFocusedTextInput
287 inputs.indexOf(lastFocusedTextInput) + 1
288 else
289 1
290 index = Math.min(count, inputs.length) - 1
291 utils.focusElement(inputs[index], {select: true})
292 storage.inputs = inputs
293
294 commands.clear_inputs = ({ storage }) ->
295 storage.inputs = null
296
297 commands.move_focus = ({ vim, storage, direction }) ->
298 if storage.inputs
299 index = storage.inputs.indexOf(utils.getActiveElement(vim.content))
300 # If there’s only one input, `<tab>` would cycle to itself, making it feel
301 # like `<tab>` was not working. Then it’s better to let `<tab>` work as it
302 # usually does.
303 if index == -1 or storage.inputs.length <= 1
304 storage.inputs = null
305 else
306 { inputs } = storage
307 nextInput = inputs[(index + direction) %% inputs.length]
308 utils.focusElement(nextInput, {select: true})
309 return
310
311 utils.moveFocus(direction)
312
313 commands.esc = (args) ->
314 commands.blur_active_element(args)
315
316 { document } = args.vim.content
317 if document.exitFullscreen
318 document.exitFullscreen()
319 else
320 document.mozCancelFullScreen()
321
322 commands.blur_active_element = ({ vim }) ->
323 utils.blurActiveElement(vim.content)
324
325 module.exports = commands
Imprint / Impressum