]> git.gir.st - VimFx.git/blob - extension/lib/commands-frame.coffee
Implement simple marks
[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 translate = require('./l10n')
27 utils = require('./utils')
28
29 {isProperLink, isTextInputElement, isTypingElement, isContentEditable} = utils
30
31 XULDocument = Ci.nsIDOMXULDocument
32
33 commands = {}
34
35 commands.go_up_path = ({vim, count = 1}) ->
36 vim.content.location.pathname = vim.content.location.pathname.replace(
37 /// (?: /[^/]+ ){1,#{count}} /?$ ///, ''
38 )
39
40 commands.go_to_root = ({vim}) ->
41 vim.content.location.href = vim.content.location.origin
42
43 helper_scroll = (element, args) ->
44 {method, type, directions, amounts, properties, smooth} = args
45 options = {}
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)
54
55 commands.scroll = (args) ->
56 {vim} = args
57 activeElement = utils.getActiveElement(vim.content)
58 element =
59 if vim.state.scrollableElements.has(activeElement)
60 activeElement
61 else
62 vim.state.scrollableElements.filterSuitableDefault()
63 helper_scroll(element, args)
64
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))
69
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))
74 return
75
76 args.amounts = vim.state.marks[keyStr]
77 element = vim.state.scrollableElements.filterSuitableDefault()
78 helper_scroll(element, args)
79
80 helper_follow = ({id, combine = true}, matcher, {vim}) ->
81 hrefs = {}
82 vim.state.markerElements = []
83
84 filter = (element, getElementShape) ->
85 {type, semantic} = matcher({vim, element, getElementShape})
86
87 customMatcher = FRAME_SCRIPT_ENVIRONMENT.VimFxHintMatcher
88 if customMatcher
89 {type, semantic} = customMatcher(id, element, {type, semantic})
90
91 return unless type
92 return unless shape = getElementShape(element)
93
94 length = vim.state.markerElements.push(element)
95 wrapper = {type, semantic, shape, elementIndex: length - 1}
96
97 # Combine links with the same href.
98 if combine and wrapper.type == 'link'
99 {href} = element
100 wrapper.href = href
101 if href of hrefs
102 parent = hrefs[href]
103 wrapper.parentIndex = parent.elementIndex
104 parent.shape.area += wrapper.shape.area
105 parent.numChildren++
106 else
107 wrapper.numChildren = 0
108 hrefs[href] = wrapper
109
110 return wrapper
111
112 return hints.getMarkableElementsAndViewport(vim.content, filter)
113
114 commands.follow = helper_follow.bind(null, {id: 'normal'},
115 ({vim, element, getElementShape}) ->
116 document = element.ownerDocument
117 isXUL = (document instanceof XULDocument)
118 type = null
119 semantic = true
120 switch
121 when isProperLink(element)
122 type = 'link'
123 when isTypingElement(element) or isContentEditable(element)
124 type = 'text'
125 when element.tabIndex > -1 and
126 not (isXUL and element.nodeName.endsWith('box') and
127 element.nodeName != 'checkbox')
128 type = 'clickable'
129 unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON']
130 semantic = false
131 when element != vim.state.scrollableElements.largest and
132 vim.state.scrollableElements.has(element)
133 type = 'scrollable'
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')
145 type = 'clickable'
146 semantic = false
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'
156 input =
157 if element.htmlFor
158 document.getElementById(element.htmlFor)
159 else
160 element.querySelector('input, textarea, select')
161 if input and not getElementShape(input)
162 type = 'clickable'
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]')
170 type = 'clickable'
171 semantic = false
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'))
177 type = 'clickable'
178 return {type, semantic}
179 )
180
181 commands.follow_in_tab = helper_follow.bind(null, {id: 'tab'},
182 ({element}) ->
183 type = if isProperLink(element) then 'link' else null
184 return {type, semantic: true}
185 )
186
187 commands.follow_copy = helper_follow.bind(null, {id: 'copy'},
188 ({element}) ->
189 type = switch
190 when isProperLink(element) then 'link'
191 when isTypingElement(element) then 'text'
192 when isContentEditable(element) then 'contenteditable'
193 else null
194 return {type, semantic: true}
195 )
196
197 commands.follow_focus = helper_follow.bind(null, {id: 'focus', combine: false},
198 ({vim, element}) ->
199 type = switch
200 when element.tabIndex > -1
201 'focusable'
202 when element != vim.state.scrollableElements.largest and
203 vim.state.scrollableElements.has(element)
204 'scrollable'
205 else
206 null
207 return {type, semantic: true}
208 )
209
210 commands.focus_marker_element = ({vim, elementIndex, options}) ->
211 element = vim.state.markerElements[elementIndex]
212 utils.focusElement(element, options)
213
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
219 element.target = ''
220 if type == 'clickable-special'
221 element.click()
222 else
223 utils.simulateClick(element)
224 element.target = targetReset if targetReset
225
226 commands.copy_marker_element = ({vim, elementIndex, property}) ->
227 element = vim.state.markerElements[elementIndex]
228 utils.writeToClipboard(element[property])
229
230 commands.follow_pattern = ({vim, type, options}) ->
231 {document} = vim.content
232
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
238 return
239
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)
243
244 # Note: Earlier patterns should be favored.
245 {patterns} = options
246
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
254
255 matchingLink = do ->
256 # First search in attributes (favoring earlier attributes) as it's likely
257 # that they are more specific than text contexts.
258 for attr in attrs
259 for regex in patterns
260 for element in candidates
261 return element if regex.test(element.getAttribute(attr))
262
263 # Then search in element contents.
264 for regex in patterns
265 for element in candidates
266 return element if regex.test(element.textContent)
267
268 return null
269
270 utils.simulateClick(matchingLink) if matchingLink
271
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
277 )
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)
282 unless count?
283 count =
284 if lastFocusedTextInput
285 inputs.indexOf(lastFocusedTextInput) + 1
286 else
287 1
288 index = Math.min(count, inputs.length) - 1
289 utils.focusElement(inputs[index], {select: true})
290 vim.state.inputs = inputs
291
292 commands.clear_inputs = ({vim}) ->
293 vim.state.inputs = null
294
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
300 # usually does.
301 if index == -1 or vim.state.inputs.length <= 1
302 vim.state.inputs = null
303 return false
304 else
305 {inputs} = vim.state
306 nextInput = inputs[(index + direction) %% inputs.length]
307 utils.focusElement(nextInput, {select: true})
308 return true
309
310 commands.esc = (args) ->
311 commands.blur_active_element(args)
312
313 {document} = args.vim.content
314 if document.exitFullscreen
315 document.exitFullscreen()
316 else
317 document.mozCancelFullScreen()
318
319 commands.blur_active_element = ({vim}) ->
320 utils.blurActiveElement(vim.content)
321
322 module.exports = commands
Imprint / Impressum