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 = ({vim, method, type, direction, amount, property, smooth}) ->
43 activeElement = utils.getActiveElement(vim.content)
44 document = activeElement.ownerDocument
47 when vim.state.scrollableElements.has(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
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)
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
69 element[method](options)
71 # Combine links with the same href.
72 combine = (hrefs, element, wrapper) ->
73 if wrapper.type == 'link'
78 wrapper.parentIndex = parent.elementIndex
79 parent.shape.area += wrapper.shape.area
82 wrapper.numChildren = 0
86 commands.follow = ({vim}) ->
88 vim.state.markerElements = []
89 filter = (element, getElementShape) ->
90 document = element.ownerDocument
91 isXUL = (document instanceof XULDocument)
94 when isProperLink(element)
96 when isTypingElement(element) or isContentEditable(element)
98 when element.tabIndex > -1 and
99 not (isXUL and element.nodeName.endsWith('box') and
100 element.nodeName != 'checkbox')
102 unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON']
104 when element != vim.state.scrollableElements.largest and
105 vim.state.scrollableElements.has(element)
107 when element.hasAttribute('onclick') or
108 element.hasAttribute('onmousedown') or
109 element.hasAttribute('onmouseup') or
110 element.hasAttribute('oncommand') or
111 element.getAttribute('role') in ['link', 'button'] or
112 # Twitter special-case.
113 element.classList.contains('js-new-tweets-bar') or
114 # Feedly special-case.
115 element.hasAttribute('data-app-action') or
116 element.hasAttribute('data-uri') or
117 element.hasAttribute('data-page-action')
120 # Facebook special-case (comment fields).
121 when element.parentElement?.classList.contains('UFIInputContainer')
122 type = 'clickable-special'
123 # Putting markers on `<label>` elements is generally redundant, because
124 # its `<input>` gets one. However, some sites hide the actual `<input>`
125 # but keeps the `<label>` to click, either for styling purposes or to keep
126 # the `<input>` hidden until it is used. In those cases we should add a
127 # marker for the `<label>`.
128 when element.nodeName == 'LABEL'
131 document.getElementById(element.htmlFor)
133 element.querySelector('input, textarea, select')
134 if input and not getElementShape(input)
136 # Elements that have “button” somewhere in the class might be clickable,
137 # unless they contain a real link or button or yet an element with
138 # “button” somewhere in the class, in which case they likely are
139 # “button-wrapper”s. (`<SVG element>.className` is not a string!)
140 when not isXUL and typeof element.className == 'string' and
141 element.className.toLowerCase().includes('button')
142 unless element.querySelector('a, button, [class*=button]')
145 # When viewing an image it should get a marker to toggle zoom.
146 when document.body?.childElementCount == 1 and
147 element.nodeName == 'IMG' and
148 (element.classList.contains('overflowing') or
149 element.classList.contains('shrinkToFit'))
152 return unless shape = getElementShape(element)
153 length = vim.state.markerElements.push(element)
155 hrefs, element, {elementIndex: length - 1, shape, semantic, type}
158 return hints.getMarkableElementsAndViewport(vim.content, filter)
160 commands.follow_in_tab = ({vim}) ->
162 vim.state.markerElements = []
163 filter = (element, getElementShape) ->
164 return unless isProperLink(element)
165 return unless shape = getElementShape(element)
166 length = vim.state.markerElements.push(element)
169 {elementIndex: length - 1, shape, semantic: true, type: 'link'}
172 return hints.getMarkableElementsAndViewport(vim.content, filter)
174 commands.follow_copy = ({vim}) ->
176 vim.state.markerElements = []
177 filter = (element, getElementShape) ->
179 when isProperLink(element) then 'link'
180 when isTypingElement(element) then 'typing'
181 when isContentEditable(element) then 'contenteditable'
183 return unless shape = getElementShape(element)
184 length = vim.state.markerElements.push(element)
186 hrefs, element, {elementIndex: length - 1, shape, semantic: true, type}
189 return hints.getMarkableElementsAndViewport(vim.content, filter)
191 commands.follow_focus = ({vim}) ->
192 vim.state.markerElements = []
193 filter = (element, getElementShape) ->
195 when element.tabIndex > -1
197 when element != vim.state.scrollableElements.largest and
198 vim.state.scrollableElements.has(element)
201 return unless shape = getElementShape(element)
202 length = vim.state.markerElements.push(element)
203 return {elementIndex: length - 1, shape, semantic: true, type}
205 return hints.getMarkableElementsAndViewport(vim.content, filter)
207 commands.focus_marker_element = ({vim, elementIndex, options}) ->
208 element = vim.state.markerElements[elementIndex]
209 utils.focusElement(element, options)
211 commands.click_marker_element = (args) ->
212 {vim, elementIndex, type, preventTargetBlank} = args
213 element = vim.state.markerElements[elementIndex]
214 if element.target == '_blank' and preventTargetBlank
215 targetReset = element.target
217 if type == 'clickable-special'
220 utils.simulateClick(element)
221 element.target = targetReset if targetReset
223 commands.copy_marker_element = ({vim, elementIndex, property}) ->
224 element = vim.state.markerElements[elementIndex]
225 utils.writeToClipboard(element[property])
227 commands.follow_pattern = ({vim, type, options}) ->
228 {document} = vim.content
230 # If there’s a `<link rel=prev/next>` element we use that.
231 for link in document.head?.getElementsByTagName('link')
232 # Also support `rel=previous`, just like Google.
233 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
234 vim.content.location.href = link.href
237 # Otherwise we look for a link or button on the page that seems to go to the
238 # previous or next page.
239 candidates = document.querySelectorAll(options.pattern_selector)
241 # Note: Earlier patterns should be favored.
244 # Search for the prev/next patterns in the following attributes of the
245 # element. `rel` should be kept as the first attribute, since the standard way
246 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
247 # favored. Even though some of these attributes only allow a fixed set of
248 # keywords, we pattern-match them anyways since lots of sites don’t follow the
249 # spec and use the attributes arbitrarily.
250 attrs = options.pattern_attrs
253 # First search in attributes (favoring earlier attributes) as it's likely
254 # that they are more specific than text contexts.
256 for regex in patterns
257 for element in candidates
258 return element if regex.test(element.getAttribute(attr))
260 # Then search in element contents.
261 for regex in patterns
262 for element in candidates
263 return element if regex.test(element.textContent)
267 utils.simulateClick(matchingLink) if matchingLink
269 commands.focus_text_input = ({vim, count = null}) ->
270 {lastFocusedTextInput} = vim.state
271 inputs = Array.filter(
272 utils.querySelectorAllDeep(vim.content, 'input, textarea'), (element) ->
273 return isTextInputElement(element) and utils.area(element) > 0
275 if lastFocusedTextInput and lastFocusedTextInput not in inputs
276 inputs.push(lastFocusedTextInput)
277 return unless inputs.length > 0
278 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
281 if lastFocusedTextInput
282 inputs.indexOf(lastFocusedTextInput) + 1
285 index = Math.min(count, inputs.length) - 1
286 utils.focusElement(inputs[index], {select: true})
287 vim.state.inputs = inputs
289 commands.clear_inputs = ({vim}) ->
290 vim.state.inputs = null
292 commands.move_focus = ({vim, direction}) ->
293 return false unless vim.state.inputs
294 index = vim.state.inputs.indexOf(utils.getActiveElement(vim.content))
295 # If there’s only one input, `<tab>` would cycle to itself, making it feel
296 # like `<tab>` was not working. Then it’s better to let `<tab>` work as it
298 if index == -1 or vim.state.inputs.length <= 1
299 vim.state.inputs = null
303 nextInput = inputs[(index + direction) %% inputs.length]
304 utils.focusElement(nextInput, {select: true})
307 commands.esc = (args) ->
308 commands.blur_active_element(args)
310 {document} = args.vim.content
311 if document.exitFullscreen
312 document.exitFullscreen()
314 document.mozCancelFullScreen()
316 commands.blur_active_element = ({vim}) ->
317 utils.blurActiveElement(vim.content)
319 module.exports = commands