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 translate = require('./l10n')
27 utils = require('./utils')
29 {isProperLink, isTextInputElement, isTypingElement, isContentEditable} = utils
31 XULDocument = Ci.nsIDOMXULDocument
35 commands.go_up_path = ({vim, count = 1}) ->
36 vim.content.location.pathname = vim.content.location.pathname.replace(
37 /// (?: /[^/]+ ){1,#{count}} /?$ ///, ''
40 commands.go_to_root = ({vim}) ->
41 vim.content.location.href = vim.content.location.origin
43 helper_scroll = (element, args) ->
44 {method, type, directions, amounts, properties, smooth} = args
46 for direction, index in directions
47 amount = amounts[index]
48 options[direction] = switch type
49 when 'lines' then amount
50 when 'pages' then amount * element[properties[index]]
51 when 'other' then Math.min(amount, element[properties[index]])
52 options.behavior = 'smooth' if smooth
53 element[method](options)
55 commands.scroll = (args) ->
57 activeElement = utils.getActiveElement(vim.content)
59 if vim.state.scrollableElements.has(activeElement)
62 vim.state.scrollableElements.filterSuitableDefault()
63 helper_scroll(element, args)
65 commands.mark_scroll_position = ({vim, keyStr}) ->
66 element = vim.state.scrollableElements.filterSuitableDefault()
67 vim.state.marks[keyStr] = [element.scrollTop, element.scrollLeft]
68 vim.notify(translate('mark_scroll_position.success', keyStr))
70 commands.scroll_to_mark = (args) ->
71 {vim, amounts: keyStr} = args
72 unless keyStr of vim.state.marks
73 vim.notify(translate('scroll_to_mark.error', keyStr))
76 args.amounts = vim.state.marks[keyStr]
77 element = vim.state.scrollableElements.filterSuitableDefault()
78 helper_scroll(element, args)
80 helper_follow = ({id, combine = true}, matcher, {vim}) ->
82 vim.state.markerElements = []
84 filter = (element, getElementShape) ->
85 {type, semantic} = matcher({vim, element, getElementShape})
87 customMatcher = FRAME_SCRIPT_ENVIRONMENT.VimFxHintMatcher
89 {type, semantic} = customMatcher(id, element, {type, semantic})
92 return unless shape = getElementShape(element)
94 length = vim.state.markerElements.push(element)
95 wrapper = {type, semantic, shape, elementIndex: length - 1}
97 # Combine links with the same href.
98 if combine and wrapper.type == 'link'
103 wrapper.parentIndex = parent.elementIndex
104 parent.shape.area += wrapper.shape.area
107 wrapper.numChildren = 0
108 hrefs[href] = wrapper
112 return hints.getMarkableElementsAndViewport(vim.content, filter)
114 commands.follow = helper_follow.bind(null, {id: 'normal'},
115 ({vim, element, getElementShape}) ->
116 document = element.ownerDocument
117 isXUL = (document instanceof XULDocument)
121 when isProperLink(element)
123 when isTypingElement(element) or isContentEditable(element)
125 when element.tabIndex > -1 and
126 not (isXUL and element.nodeName.endsWith('box') and
127 element.nodeName != 'checkbox')
129 unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON']
131 when element != vim.state.scrollableElements.largest and
132 vim.state.scrollableElements.has(element)
134 when element.hasAttribute('onclick') or
135 element.hasAttribute('onmousedown') or
136 element.hasAttribute('onmouseup') or
137 element.hasAttribute('oncommand') or
138 element.getAttribute('role') in ['link', 'button'] or
139 # Twitter special-case.
140 element.classList.contains('js-new-tweets-bar') or
141 # Feedly special-case.
142 element.hasAttribute('data-app-action') or
143 element.hasAttribute('data-uri') or
144 element.hasAttribute('data-page-action')
147 # Facebook special-case (comment fields).
148 when element.parentElement?.classList.contains('UFIInputContainer')
149 type = 'clickable-special'
150 # Putting markers on `<label>` elements is generally redundant, because
151 # its `<input>` gets one. However, some sites hide the actual `<input>`
152 # but keeps the `<label>` to click, either for styling purposes or to keep
153 # the `<input>` hidden until it is used. In those cases we should add a
154 # marker for the `<label>`.
155 when element.nodeName == 'LABEL'
158 document.getElementById(element.htmlFor)
160 element.querySelector('input, textarea, select')
161 if input and not getElementShape(input)
163 # Elements that have “button” somewhere in the class might be clickable,
164 # unless they contain a real link or button or yet an element with
165 # “button” somewhere in the class, in which case they likely are
166 # “button-wrapper”s. (`<SVG element>.className` is not a string!)
167 when not isXUL and typeof element.className == 'string' and
168 element.className.toLowerCase().includes('button')
169 unless element.querySelector('a, button, [class*=button]')
172 # When viewing an image it should get a marker to toggle zoom.
173 when document.body?.childElementCount == 1 and
174 element.nodeName == 'IMG' and
175 (element.classList.contains('overflowing') or
176 element.classList.contains('shrinkToFit'))
178 return {type, semantic}
181 commands.follow_in_tab = helper_follow.bind(null, {id: 'tab'},
183 type = if isProperLink(element) then 'link' else null
184 return {type, semantic: true}
187 commands.follow_copy = helper_follow.bind(null, {id: 'copy'},
190 when isProperLink(element) then 'link'
191 when isTypingElement(element) then 'text'
192 when isContentEditable(element) then 'contenteditable'
194 return {type, semantic: true}
197 commands.follow_focus = helper_follow.bind(null, {id: 'focus', combine: false},
200 when element.tabIndex > -1
202 when element != vim.state.scrollableElements.largest and
203 vim.state.scrollableElements.has(element)
207 return {type, semantic: true}
210 commands.focus_marker_element = ({vim, elementIndex, options}) ->
211 element = vim.state.markerElements[elementIndex]
212 utils.focusElement(element, options)
214 commands.click_marker_element = (args) ->
215 {vim, elementIndex, type, preventTargetBlank} = args
216 element = vim.state.markerElements[elementIndex]
217 if element.target == '_blank' and preventTargetBlank
218 targetReset = element.target
220 if type == 'clickable-special'
223 utils.simulateClick(element)
224 element.target = targetReset if targetReset
226 commands.copy_marker_element = ({vim, elementIndex, property}) ->
227 element = vim.state.markerElements[elementIndex]
228 utils.writeToClipboard(element[property])
230 commands.follow_pattern = ({vim, type, options}) ->
231 {document} = vim.content
233 # If there’s a `<link rel=prev/next>` element we use that.
234 for link in document.head?.getElementsByTagName('link')
235 # Also support `rel=previous`, just like Google.
236 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
237 vim.content.location.href = link.href
240 # Otherwise we look for a link or button on the page that seems to go to the
241 # previous or next page.
242 candidates = document.querySelectorAll(options.pattern_selector)
244 # Note: Earlier patterns should be favored.
247 # Search for the prev/next patterns in the following attributes of the
248 # element. `rel` should be kept as the first attribute, since the standard way
249 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
250 # favored. Even though some of these attributes only allow a fixed set of
251 # keywords, we pattern-match them anyways since lots of sites don’t follow the
252 # spec and use the attributes arbitrarily.
253 attrs = options.pattern_attrs
256 # First search in attributes (favoring earlier attributes) as it's likely
257 # that they are more specific than text contexts.
259 for regex in patterns
260 for element in candidates
261 return element if regex.test(element.getAttribute(attr))
263 # Then search in element contents.
264 for regex in patterns
265 for element in candidates
266 return element if regex.test(element.textContent)
270 utils.simulateClick(matchingLink) if matchingLink
272 commands.focus_text_input = ({vim, count = null}) ->
273 {lastFocusedTextInput} = vim.state
274 inputs = Array.filter(
275 utils.querySelectorAllDeep(vim.content, 'input, textarea'), (element) ->
276 return isTextInputElement(element) and utils.area(element) > 0
278 if lastFocusedTextInput and lastFocusedTextInput not in inputs
279 inputs.push(lastFocusedTextInput)
280 return unless inputs.length > 0
281 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
284 if lastFocusedTextInput
285 inputs.indexOf(lastFocusedTextInput) + 1
288 index = Math.min(count, inputs.length) - 1
289 utils.focusElement(inputs[index], {select: true})
290 vim.state.inputs = inputs
292 commands.clear_inputs = ({vim}) ->
293 vim.state.inputs = null
295 commands.move_focus = ({vim, direction}) ->
296 return false unless vim.state.inputs
297 index = vim.state.inputs.indexOf(utils.getActiveElement(vim.content))
298 # If there’s only one input, `<tab>` would cycle to itself, making it feel
299 # like `<tab>` was not working. Then it’s better to let `<tab>` work as it
301 if index == -1 or vim.state.inputs.length <= 1
302 vim.state.inputs = null
306 nextInput = inputs[(index + direction) %% inputs.length]
307 utils.focusElement(nextInput, {select: true})
310 commands.esc = (args) ->
311 commands.blur_active_element(args)
313 {document} = args.vim.content
314 if document.exitFullscreen
315 document.exitFullscreen()
317 document.mozCancelFullScreen()
319 commands.blur_active_element = ({vim}) ->
320 utils.blurActiveElement(vim.content)
322 module.exports = commands