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