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