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