2 # Copyright Simon Lydell 2015.
4 # This file is part of VimFx.
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.
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.
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/>.
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.
25 hints = require('./hints')
26 utils = require('./utils')
28 { isProperLink, isTextInputElement, isTypingElement, isContentEditable } = utils
30 XULDocument = Ci.nsIDOMXULDocument
34 commands.go_up_path = ({ vim, count = 1 }) ->
35 vim.content.location.pathname = vim.content.location.pathname.replace(
36 /// (?: /[^/]+ ){1,#{ count }} /?$ ///, ''
39 commands.go_to_root = ({ vim }) ->
40 vim.content.location.href = vim.content.location.origin
42 commands.scroll = (args) ->
43 { vim, method, type, direction, amount, property, smooth } = args
44 activeElement = utils.getActiveElement(vim.content)
45 document = activeElement.ownerDocument
48 when vim.state.scrollableElements.has(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?
61 scrollableElements.largest
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
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
74 element[method](options)
76 # Combine links with the same href.
77 combine = (hrefs, element, wrapper) ->
78 if wrapper.type == 'link'
83 wrapper.parentIndex = parent.elementIndex
84 parent.shape.area += wrapper.shape.area
87 wrapper.numChildren = 0
91 commands.follow = ({ vim, storage }) ->
93 storage.markerElements = []
94 filter = (element, getElementShape) ->
95 document = element.ownerDocument
96 isXUL = (document instanceof XULDocument)
99 when isProperLink(element)
101 when isTypingElement(element) or isContentEditable(element)
103 when element.tabIndex > -1 and
104 not (isXUL and element.nodeName.endsWith('box') and
105 element.nodeName != 'checkbox')
107 unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON']
109 when element != document.documentElement and
110 vim.state.scrollableElements.has(element)
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')
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'
136 document.getElementById(element.htmlFor)
138 element.querySelector('input, textarea, select')
139 if input and not getElementShape(input)
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]')
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'))
157 return unless shape = getElementShape(element)
158 length = storage.markerElements.push(element)
160 hrefs, element, {elementIndex: length - 1, shape, semantic, type}
163 return hints.getMarkableElementsAndViewport(vim.content, filter)
165 commands.follow_in_tab = ({ vim, storage }) ->
167 storage.markerElements = []
168 filter = (element, getElementShape) ->
169 return unless isProperLink(element)
170 return unless shape = getElementShape(element)
171 length = storage.markerElements.push(element)
174 {elementIndex: length - 1, shape, semantic: true, type: 'link'}
177 return hints.getMarkableElementsAndViewport(vim.content, filter)
179 commands.follow_copy = ({ vim, storage }) ->
181 storage.markerElements = []
182 filter = (element, getElementShape) ->
184 when isProperLink(element) then 'link'
185 when isTypingElement(element) then 'typing'
186 when isContentEditable(element) then 'contenteditable'
188 return unless shape = getElementShape(element)
189 length = storage.markerElements.push(element)
191 hrefs, element, {elementIndex: length - 1, shape, semantic: true, type}
194 return hints.getMarkableElementsAndViewport(vim.content, filter)
196 commands.follow_focus = ({ vim, storage }) ->
197 storage.markerElements = []
198 filter = (element, getElementShape) ->
200 when element.tabIndex > -1
202 when element != element.ownerDocument.documentElement and
203 vim.state.scrollableElements.has(element)
206 return unless shape = getElementShape(element)
207 length = storage.markerElements.push(element)
208 return {elementIndex: length - 1, shape, semantic: true, type}
210 return hints.getMarkableElementsAndViewport(vim.content, filter)
212 commands.focus_marker_element = ({ storage, elementIndex, options }) ->
213 element = storage.markerElements[elementIndex]
214 utils.focusElement(element, options)
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
222 if type == 'clickable-special'
225 utils.simulateClick(element)
226 element.target = targetReset if targetReset
228 commands.copy_marker_element = ({ storage, elementIndex, property }) ->
229 element = storage.markerElements[elementIndex]
230 utils.writeToClipboard(element[property])
232 commands.follow_pattern = ({ vim, type, options }) ->
233 { document } = vim.content
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
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)
246 # Note: Earlier patterns should be favored.
247 { patterns } = options
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
258 # First search in attributes (favoring earlier attributes) as it's likely
259 # that they are more specific than text contexts.
261 for regex in patterns
262 for element in candidates
263 return element if regex.test(element.getAttribute(attr))
265 # Then search in element contents.
266 for regex in patterns
267 for element in candidates
268 return element if regex.test(element.textContent)
272 utils.simulateClick(matchingLink) if matchingLink
274 commands.focus_text_input = ({ vim, storage, count = null }) ->
275 { lastFocusedTextInput } = vim.state
276 inputs = Array.filter(
277 vim.content.document.querySelectorAll('input, textarea'), (element) ->
278 return isTextInputElement(element) and utils.area(element) > 0
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)
286 if lastFocusedTextInput
287 inputs.indexOf(lastFocusedTextInput) + 1
290 index = Math.min(count, inputs.length) - 1
291 utils.focusElement(inputs[index], {select: true})
292 storage.inputs = inputs
294 commands.clear_inputs = ({ storage }) ->
295 storage.inputs = null
297 commands.move_focus = ({ vim, storage, direction }) ->
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
303 if index == -1 or storage.inputs.length <= 1
304 storage.inputs = null
307 nextInput = inputs[(index + direction) %% inputs.length]
308 utils.focusElement(nextInput, {select: true})
311 utils.moveFocus(direction)
313 commands.esc = (args) ->
314 commands.blur_active_element(args)
316 { document } = args.vim.content
317 if document.exitFullscreen
318 document.exitFullscreen()
320 document.mozCancelFullScreen()
322 commands.blur_active_element = ({ vim }) ->
323 utils.blurActiveElement(vim.content)
325 module.exports = commands