]> git.gir.st - VimFx.git/blob - extension/lib/commands-frame.coffee
Fix being unable to scroll in quirks mode
[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.hasOrUpdateLargestScrollable()
52 vim.state.scrollableElements.largest
53 else
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.root()
61
62 options = {}
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
68
69 element[method](options)
70
71 # Combine links with the same href.
72 combine = (hrefs, element, wrapper) ->
73 if wrapper.type == 'link'
74 {href} = element
75 wrapper.href = href
76 if href of hrefs
77 parent = hrefs[href]
78 wrapper.parentIndex = parent.elementIndex
79 parent.shape.area += wrapper.shape.area
80 parent.numChildren++
81 else
82 wrapper.numChildren = 0
83 hrefs[href] = wrapper
84 return wrapper
85
86 commands.follow = ({vim}) ->
87 hrefs = {}
88 vim.state.markerElements = []
89 filter = (element, getElementShape) ->
90 document = element.ownerDocument
91 isXUL = (document instanceof XULDocument)
92 semantic = true
93 switch
94 when isProperLink(element)
95 type = 'link'
96 when isTypingElement(element) or isContentEditable(element)
97 type = 'text'
98 when element.tabIndex > -1 and
99 not (isXUL and element.nodeName.endsWith('box') and
100 element.nodeName != 'checkbox')
101 type = 'clickable'
102 unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON']
103 semantic = false
104 when element != vim.state.scrollableElements.root(element) and
105 vim.state.scrollableElements.has(element)
106 type = 'scrollable'
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')
118 type = 'clickable'
119 semantic = false
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'
129 input =
130 if element.htmlFor
131 document.getElementById(element.htmlFor)
132 else
133 element.querySelector('input, textarea, select')
134 if input and not getElementShape(input)
135 type = 'clickable'
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]')
143 type = 'clickable'
144 semantic = false
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'))
150 type = 'clickable'
151 return unless type
152 return unless shape = getElementShape(element)
153 length = vim.state.markerElements.push(element)
154 return combine(
155 hrefs, element, {elementIndex: length - 1, shape, semantic, type}
156 )
157
158 return hints.getMarkableElementsAndViewport(vim.content, filter)
159
160 commands.follow_in_tab = ({vim}) ->
161 hrefs = {}
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)
167 return combine(
168 hrefs, element,
169 {elementIndex: length - 1, shape, semantic: true, type: 'link'}
170 )
171
172 return hints.getMarkableElementsAndViewport(vim.content, filter)
173
174 commands.follow_copy = ({vim}) ->
175 hrefs = {}
176 vim.state.markerElements = []
177 filter = (element, getElementShape) ->
178 type = switch
179 when isProperLink(element) then 'link'
180 when isTypingElement(element) then 'typing'
181 when isContentEditable(element) then 'contenteditable'
182 return unless type
183 return unless shape = getElementShape(element)
184 length = vim.state.markerElements.push(element)
185 return combine(
186 hrefs, element, {elementIndex: length - 1, shape, semantic: true, type}
187 )
188
189 return hints.getMarkableElementsAndViewport(vim.content, filter)
190
191 commands.follow_focus = ({vim}) ->
192 vim.state.markerElements = []
193 filter = (element, getElementShape) ->
194 type = switch
195 when element.tabIndex > -1
196 'focusable'
197 when element != vim.state.scrollableElements.root(element) and
198 vim.state.scrollableElements.has(element)
199 'scrollable'
200 return unless type
201 return unless shape = getElementShape(element)
202 length = vim.state.markerElements.push(element)
203 return {elementIndex: length - 1, shape, semantic: true, type}
204
205 return hints.getMarkableElementsAndViewport(vim.content, filter)
206
207 commands.focus_marker_element = ({vim, elementIndex, options}) ->
208 element = vim.state.markerElements[elementIndex]
209 utils.focusElement(element, options)
210
211 commands.click_marker_element = (args) ->
212 {vim, elementIndex, preventTargetBlank, type} = args
213 element = vim.state.markerElements[elementIndex]
214 if element.target == '_blank' and preventTargetBlank
215 targetReset = element.target
216 element.target = ''
217 if type == 'clickable-special'
218 element.click()
219 else
220 utils.simulateClick(element)
221 element.target = targetReset if targetReset
222
223 commands.copy_marker_element = ({vim, elementIndex, property}) ->
224 element = vim.state.markerElements[elementIndex]
225 utils.writeToClipboard(element[property])
226
227 commands.follow_pattern = ({vim, type, options}) ->
228 {document} = vim.content
229
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
235 return
236
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)
240
241 # Note: Earlier patterns should be favored.
242 {patterns} = options
243
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
251
252 matchingLink = do ->
253 # First search in attributes (favoring earlier attributes) as it's likely
254 # that they are more specific than text contexts.
255 for attr in attrs
256 for regex in patterns
257 for element in candidates
258 return element if regex.test(element.getAttribute(attr))
259
260 # Then search in element contents.
261 for regex in patterns
262 for element in candidates
263 return element if regex.test(element.textContent)
264
265 return null
266
267 utils.simulateClick(matchingLink) if matchingLink
268
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
274 )
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)
279 unless count?
280 count =
281 if lastFocusedTextInput
282 inputs.indexOf(lastFocusedTextInput) + 1
283 else
284 1
285 index = Math.min(count, inputs.length) - 1
286 utils.focusElement(inputs[index], {select: true})
287 vim.state.inputs = inputs
288
289 commands.clear_inputs = ({vim}) ->
290 vim.state.inputs = null
291
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
297 # usually does.
298 if index == -1 or vim.state.inputs.length <= 1
299 vim.state.inputs = null
300 return false
301 else
302 {inputs} = vim.state
303 nextInput = inputs[(index + direction) %% inputs.length]
304 utils.focusElement(nextInput, {select: true})
305 return true
306
307 commands.esc = (args) ->
308 commands.blur_active_element(args)
309
310 {document} = args.vim.content
311 if document.exitFullscreen
312 document.exitFullscreen()
313 else
314 document.mozCancelFullScreen()
315
316 commands.blur_active_element = ({vim}) ->
317 utils.blurActiveElement(vim.content)
318
319 module.exports = commands
Imprint / Impressum