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