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