]> git.gir.st - VimFx.git/blob - extension/lib/commands-frame.coffee
Scroll closest scrollable parent
[VimFx.git] / extension / lib / commands-frame.coffee
1 ###
2 # Copyright Simon Lydell 2015, 2016, 2017.
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 markableElements = require('./markable-elements')
26 prefs = require('./prefs')
27 SelectionManager = require('./selection')
28 translate = require('./translate')
29 utils = require('./utils')
30 viewportUtils = require('./viewport')
31
32 {FORWARD, BACKWARD} = SelectionManager
33 {isProperLink, isTextInputElement, isTypingElement, isContentEditable} = utils
34
35 XULDocument = Ci.nsIDOMXULDocument
36
37 # <http://www.w3.org/html/wg/drafts/html/master/dom.html#wai-aria>
38 CLICKABLE_ARIA_ROLES = [
39 'link', 'button', 'tab'
40 'checkbox', 'radio', 'combobox', 'option', 'slider', 'textbox'
41 'menuitem', 'menuitemcheckbox', 'menuitemradio'
42 ]
43
44 createComplementarySelectors = (selectors) ->
45 return [
46 selectors.join(',')
47 selectors.map((selector) -> ":not(#{selector})").join('')
48 ]
49
50 FOLLOW_DEFAULT_SELECTORS = createComplementarySelectors([
51 'a', 'button', 'input', 'textarea', 'select', 'label'
52 '[role]', '[contenteditable]'
53 ])
54
55 FOLLOW_CONTEXT_TAGS = [
56 'a', 'button', 'input', 'textarea', 'select'
57 'img', 'audio', 'video', 'canvas', 'embed', 'object'
58 ]
59
60 FOLLOW_CONTEXT_SELECTORS = createComplementarySelectors(FOLLOW_CONTEXT_TAGS)
61
62 FOLLOW_SELECTABLE_SELECTORS =
63 createComplementarySelectors(['div', 'span']).reverse()
64
65 commands = {}
66
67 commands.go_up_path = ({vim, count = 1}) ->
68 {pathname} = vim.content.location
69 newPathname = pathname.replace(/// (?: [^/]+(/|$) ){1,#{count}} $ ///, '')
70 if newPathname == pathname
71 vim.notify(translate('notification.go_up_path.limit'))
72 else
73 vim.content.location.pathname = newPathname
74
75 commands.go_to_root = ({vim}) ->
76 # `.origin` is `'null'` (as a string) on `about:` pages.
77 if "#{vim.content.location.origin}/" in [vim.content.location.href, 'null/']
78 vim.notify(translate('notification.go_up_path.limit'))
79 else
80 vim.content.location.href = vim.content.location.origin
81
82 commands.scroll = (args) ->
83 {vim} = args
84 return unless activeElement = utils.getActiveElement(vim.content)
85
86 # If the active element, or one of its parents, is scrollable, use that
87 # element.
88 scrollElement = activeElement
89 until vim.state.scrollableElements.has(scrollElement) or
90 not scrollElement.parentNode
91 scrollElement = scrollElement.parentNode
92
93 element =
94 # If no element is focused on the page, the active element is the topmost
95 # `<body>`, and blurring it is a no-op. If it is scrollable, it means that
96 # you can’t blur it in order to scroll `<html>`. Therefore it may only be
97 # scrolled if it has been explicitly focused.
98 if vim.state.scrollableElements.has(scrollElement) and
99 (scrollElement != vim.content.document.body or
100 vim.state.explicitBodyFocus)
101 scrollElement
102 else
103 vim.state.scrollableElements.filterSuitableDefault()
104 viewportUtils.scroll(element, args)
105
106 commands.mark_scroll_position = (args) ->
107 {vim, keyStr, notify = true, addToJumpList = false} = args
108
109 vim.state.marks[keyStr] = vim.state.scrollableElements.getPageScrollPosition()
110
111 if addToJumpList
112 vim.addToJumpList()
113
114 if notify
115 vim.notify(translate('notification.mark_scroll_position.success', keyStr))
116
117 commands.scroll_to_mark = (args) ->
118 {vim, extra: {keyStr, lastPositionMark}} = args
119
120 unless keyStr of vim.state.marks
121 vim.notify(translate('notification.scroll_to_mark.none', keyStr))
122 return
123
124 args.amounts = vim.state.marks[keyStr]
125 element = vim.state.scrollableElements.filterSuitableDefault()
126
127 commands.mark_scroll_position({
128 vim
129 keyStr: lastPositionMark
130 notify: false
131 addToJumpList: true
132 })
133 viewportUtils.scroll(element, args)
134
135 commands.scroll_to_position = (args) ->
136 {vim, extra: {count, direction, lastPositionMark}} = args
137
138 if direction == 'previous' and vim.state.jumpListIndex >= 0 and
139 vim.state.jumpListIndex == vim.state.jumpList.length - 1
140 vim.addToJumpList()
141
142 {jumpList, jumpListIndex} = vim.state
143 maxIndex = jumpList.length - 1
144
145 if (direction == 'previous' and jumpListIndex <= 0) or
146 (direction == 'next' and jumpListIndex >= maxIndex)
147 vim.notify(translate("notification.scroll_to_#{direction}_position.limit"))
148 return
149
150 index = jumpListIndex + count * (if direction == 'previous' then -1 else +1)
151 index = Math.max(index, 0)
152 index = Math.min(index, maxIndex)
153
154 args.amounts = jumpList[index]
155 element = vim.state.scrollableElements.filterSuitableDefault()
156
157 commands.mark_scroll_position({vim, keyStr: lastPositionMark, notify: false})
158 viewportUtils.scroll(element, args)
159 vim.state.jumpListIndex = index
160
161 helper_follow = (options, matcher, {vim, pass}) ->
162 {id, combine = true, selectors = FOLLOW_DEFAULT_SELECTORS} = options
163 if vim.content.document instanceof XULDocument
164 selectors = ['*']
165
166 if pass == 'auto'
167 pass = if selectors.length == 2 then 'first' else 'single'
168
169 vim.state.markerElements = [] if pass in ['single', 'first']
170 hrefs = {}
171
172 filter = (element, getElementShape) ->
173 type = matcher({vim, element, getElementShape})
174 if vim.hintMatcher
175 type = vim.hintMatcher(id, element, type)
176 if pass == 'complementary'
177 type = if type then null else 'complementary'
178 return unless type
179
180 shape = getElementShape(element)
181
182 unless shape.nonCoveredPoint then switch
183 # CodeMirror editor uses a tiny hidden textarea positioned at the caret.
184 # Targeting those are the only reliable way of focusing CodeMirror
185 # editors, and doing so without moving the caret.
186 when id == 'normal' and element.localName == 'textarea' and
187 element.ownerGlobal == vim.content
188 rect = element.getBoundingClientRect()
189 # Use `.clientWidth` instead of `rect.width` because the latter includes
190 # the width of the borders of the textarea, which are unreliable.
191 if element.clientWidth <= 1 and rect.height > 0
192 shape = {
193 nonCoveredPoint: {
194 x: rect.left
195 y: rect.top + rect.height / 2
196 offset: {left: 0, top: 0}
197 }
198 area: rect.width * rect.height
199 }
200
201 # Putting a large `<input type="file">` inside a smaller wrapper element
202 # with `overflow: hidden;` seems to be a common pattern, used both on
203 # addons.mozilla.org and <https://blueimp.github.io/jQuery-File-Upload/>.
204 when id in ['normal', 'focus'] and element.localName == 'input' and
205 element.type == 'file' and element.parentElement?
206 parentShape = getElementShape(element.parentElement)
207 shape = parentShape if parentShape.area <= shape.area
208
209 if not shape.nonCoveredPoint and pass == 'complementary'
210 shape = getElementShape(element, -1)
211
212 return unless shape.nonCoveredPoint
213
214 text = utils.getText(element)
215
216 originalRect = element.getBoundingClientRect()
217 length = vim.state.markerElements.push({element, originalRect})
218 wrapper = {
219 type, text, shape,
220 combinedArea: shape.area
221 elementIndex: length - 1
222 parentIndex: null
223 }
224
225 if wrapper.type == 'link'
226 {href} = element
227 wrapper.href = href
228
229 # Combine links with the same href.
230 if combine and wrapper.type == 'link' and
231 # If the element has an 'onclick' attribute we cannot be sure that all
232 # links with this href actually do the same thing. On some pages, such
233 # as startpage.com, actual proper links have the 'onclick' attribute,
234 # so we can’t exclude such links in `utils.isProperLink`.
235 not element.hasAttribute?('onclick') and
236 # GitHub’s diff expansion buttons are links with both `href` and
237 # `data-url`. They are JavaScript-powered using the latter attribute.
238 not element.hasAttribute?('data-url')
239 if href of hrefs
240 parent = hrefs[href]
241 wrapper.parentIndex = parent.elementIndex
242 parent.combinedArea += wrapper.shape.area
243 parent.numChildren += 1
244 else
245 hrefs[href] = wrapper
246
247 return wrapper
248
249 selector =
250 if pass == 'complementary'
251 '*'
252 else
253 selectors[if pass == 'second' then 1 else 0]
254 return {
255 wrappers: markableElements.find(vim.content, filter, selector)
256 viewport: viewportUtils.getWindowViewport(vim.content)
257 pass
258 }
259
260 commands.follow = helper_follow.bind(
261 null, {id: 'normal'},
262 ({vim, element, getElementShape}) ->
263 document = element.ownerDocument
264 isXUL = (document instanceof XULDocument)
265 type = null
266 switch
267 # Bootstrap. Match these before regular links, because especially slider
268 # “buttons” often get the same hint otherwise.
269 when element.hasAttribute?('data-toggle') or
270 element.hasAttribute?('data-dismiss') or
271 element.hasAttribute?('data-slide') or
272 element.hasAttribute?('data-slide-to')
273 type = 'clickable'
274 when isProperLink(element)
275 type = 'link'
276 when isTypingElement(element)
277 type = 'text'
278 when element.localName in ['a', 'button'] or
279 element.getAttribute?('role') in CLICKABLE_ARIA_ROLES or
280 # <http://www.w3.org/TR/wai-aria/states_and_properties>
281 element.hasAttribute?('aria-controls') or
282 element.hasAttribute?('aria-pressed') or
283 element.hasAttribute?('aria-checked') or
284 (element.hasAttribute?('aria-haspopup') and
285 element.getAttribute?('role') != 'menu')
286 type = 'clickable'
287 when utils.isFocusable(element) and
288 # Google Drive Documents. The hint for this element would cover the
289 # real hint that allows you to focus the document to start typing.
290 element.id != 'docs-editor'
291 type = 'clickable'
292 when element != vim.state.scrollableElements.largest and
293 vim.state.scrollableElements.has(element)
294 type = 'scrollable'
295 when element.hasAttribute?('onclick') or
296 element.hasAttribute?('onmousedown') or
297 element.hasAttribute?('onmouseup') or
298 element.hasAttribute?('oncommand') or
299 # Twitter.
300 element.classList?.contains('js-new-tweets-bar') or
301 element.hasAttribute?('data-permalink-path') or
302 # Feedly.
303 element.hasAttribute?('data-app-action') or
304 element.hasAttribute?('data-uri') or
305 element.hasAttribute?('data-page-action') or
306 # Google Drive Document.
307 element.classList?.contains('kix-appview-editor')
308 type = 'clickable'
309 # Facebook comment fields.
310 when element.parentElement?.classList?.contains('UFIInputContainer')
311 type = 'clickable-special'
312 # Putting markers on `<label>` elements is generally redundant, because
313 # its `<input>` gets one. However, some sites hide the actual `<input>`
314 # but keeps the `<label>` to click, either for styling purposes or to keep
315 # the `<input>` hidden until it is used. In those cases we should add a
316 # marker for the `<label>`. Trying to access `.control` on an element in
317 # `about:config` throws an error, so exclude XUL pages.
318 when not isXUL and element.localName == 'label' and element.control and
319 not getElementShape(element.control).nonCoveredPoint
320 type = 'clickable'
321 # Last resort checks for elements that might be clickable because of
322 # JavaScript.
323 when (not isXUL and
324 # It is common to listen for clicks on `<html>` or `<body>`. Don’t
325 # waste time on them.
326 element not in [document.documentElement, document.body]) and
327 (utils.includes(element.className, 'button') or
328 utils.includes(element.getAttribute?('aria-label'), 'close') or
329 # Do this last as it’s a potentially expensive check.
330 (utils.hasEventListeners(element, 'click') and
331 # Twitter. The hint for this element would cover the hint for
332 # showing more tweets.
333 not element.classList?.contains('js-new-items-bar-container')))
334 # Make a quick check for likely clickable descendants, to reduce the
335 # number of false positives. the element might be a “button-wrapper” or
336 # a large element with a click-tracking event listener.
337 unless element.querySelector?('a, button, input, [class*=button]')
338 type = 'clickable'
339 # When viewing an image it should get a marker to toggle zoom. This is the
340 # most unlikely rule to match, so keep it last.
341 when document.body?.childElementCount == 1 and
342 element.localName == 'img' and
343 (element.classList?.contains('overflowing') or
344 element.classList?.contains('shrinkToFit'))
345 type = 'clickable'
346 type = null if isXUL and element.classList?.contains('textbox-input')
347 return type
348 )
349
350 commands.follow_in_tab = helper_follow.bind(
351 null, {id: 'tab', selectors: ['a']},
352 ({element}) ->
353 type = if isProperLink(element) then 'link' else null
354 return type
355 )
356
357 commands.follow_copy = helper_follow.bind(
358 null, {id: 'copy'},
359 ({element}) ->
360 type = switch
361 when isProperLink(element)
362 'link'
363 when isContentEditable(element)
364 'contenteditable'
365 when isTypingElement(element)
366 'text'
367 else
368 null
369 return type
370 )
371
372 commands.follow_focus = helper_follow.bind(
373 null, {id: 'focus', combine: false},
374 ({vim, element}) ->
375 type = switch
376 when utils.isFocusable(element)
377 'focusable'
378 when element != vim.state.scrollableElements.largest and
379 vim.state.scrollableElements.has(element)
380 'scrollable'
381 else
382 null
383 return type
384 )
385
386 commands.follow_context = helper_follow.bind(
387 null, {id: 'context', selectors: FOLLOW_CONTEXT_SELECTORS},
388 ({element}) ->
389 type =
390 if element.localName in FOLLOW_CONTEXT_TAGS or
391 utils.hasMarkableTextNode(element)
392 'context'
393 else
394 null
395 return type
396 )
397
398 commands.follow_selectable = helper_follow.bind(
399 null, {id: 'selectable', selectors: FOLLOW_SELECTABLE_SELECTORS},
400 ({element}) ->
401 type =
402 if utils.hasMarkableTextNode(element)
403 'selectable'
404 else
405 null
406 return type
407 )
408
409 commands.focus_marker_element = ({vim, elementIndex, browserOffset, options}) ->
410 {element} = vim.state.markerElements[elementIndex]
411 # To be able to focus scrollable elements, `FLAG_BYKEY` _has_ to be used.
412 options.flag = 'FLAG_BYKEY' if vim.state.scrollableElements.has(element)
413 utils.focusElement(element, options)
414 vim.clearHover()
415 vim.setHover(element, browserOffset)
416
417 commands.click_marker_element = (
418 {vim, elementIndex, type, browserOffset, preventTargetBlank}
419 ) ->
420 {element} = vim.state.markerElements[elementIndex]
421 if element.target == '_blank' and preventTargetBlank
422 targetReset = element.target
423 element.target = ''
424 if type == 'clickable-special'
425 element.click()
426 else
427 isXUL = (element.ownerDocument instanceof XULDocument)
428 sequence = switch
429 when isXUL
430 if element.localName == 'tab' then ['mousedown'] else 'click-xul'
431 when type == 'context'
432 'context'
433 else
434 'click'
435 utils.simulateMouseEvents(element, sequence, browserOffset)
436 utils.openDropdown(element)
437 element.target = targetReset if targetReset
438
439 commands.copy_marker_element = ({vim, elementIndex, property}) ->
440 {element} = vim.state.markerElements[elementIndex]
441 utils.writeToClipboard(element[property])
442
443 commands.element_text_select = ({vim, elementIndex, full, scroll = false}) ->
444 {element} = vim.state.markerElements[elementIndex]
445 window = element.ownerGlobal
446 selection = window.getSelection()
447 range = window.document.createRange()
448
449 # Try to scroll the element into view, but keep the caret visible.
450 if scroll
451 viewport = viewportUtils.getWindowViewport(window)
452 rect = element.getBoundingClientRect()
453 block = switch
454 when rect.bottom > viewport.bottom
455 'end'
456 when rect.top < viewport.top and rect.height < viewport.height
457 'start'
458 else
459 null
460 if block
461 smooth = (
462 prefs.root.get('general.smoothScroll') and
463 prefs.root.get('general.smoothScroll.other')
464 )
465 element.scrollIntoView({
466 block
467 behavior: if smooth then 'smooth' else 'instant'
468 })
469
470 if full
471 range.selectNodeContents(element)
472 else
473 result = viewportUtils.getFirstNonWhitespace(element)
474 if result
475 [node, offset] = result
476 range.setStart(node, offset)
477 range.setEnd(node, offset)
478 else
479 range.setStartBefore(element)
480 range.setEndBefore(element)
481
482 utils.clearSelectionDeep(vim.content)
483
484 # Focus the window so that the selection does not appear greyed out. However,
485 # if a text input was previously focused in that window (frame), that will
486 # cause the text input to be re-focused, so make sure to blur the active
487 # element, so that the caret does not end up there.
488 window.focus()
489 window.document.activeElement?.blur?()
490
491 selection.addRange(range)
492
493 if full
494 # Force the selected text to appear in the “selection clipboard”. Note:
495 # `selection.modify` would not make any difference here. That makes sense,
496 # since web pages that programmatically change the selection shouldn’t
497 # affect the user’s clipboard. `SelectionManager` uses an internal Firefox
498 # API with clipboard privileges.
499 selectionManager = new SelectionManager(window)
500 error = selectionManager.moveCaret('characterMove', BACKWARD)
501 selectionManager.moveCaret('characterMove', FORWARD) unless error
502
503 commands.follow_pattern = ({vim, type, browserOffset, options}) ->
504 {document} = vim.content
505
506 # If there’s a `<link rel=prev/next>` element we use that.
507 for link in document.head?.getElementsByTagName('link')
508 # Also support `rel=previous`, just like Google.
509 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
510 vim.content.location.href = link.href
511 return
512
513 # Otherwise we look for a link or button on the page that seems to go to the
514 # previous or next page.
515 candidates = document.querySelectorAll(options.pattern_selector)
516
517 # Note: Earlier patterns should be favored.
518 {patterns} = options
519
520 # Search for the prev/next patterns in the following attributes of the
521 # element. `rel` should be kept as the first attribute, since the standard way
522 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
523 # favored. Even though some of these attributes only allow a fixed set of
524 # keywords, we pattern-match them anyways since lots of sites don’t follow the
525 # spec and use the attributes arbitrarily.
526 attrs = options.pattern_attrs
527
528 matchingLink = do ->
529 # Special-case for Google searches.
530 googleLink = document.getElementById("pn#{type}")
531 return googleLink if googleLink
532
533 # First search in attributes (favoring earlier attributes) as it's likely
534 # that they are more specific than text contexts.
535 for attr in attrs
536 for regex in patterns
537 for element in candidates
538 return element if regex.test(element.getAttribute?(attr))
539
540 # Then search in element contents.
541 for regex in patterns
542 for element in candidates
543 return element if regex.test(element.textContent)
544
545 return null
546
547 if matchingLink
548 utils.simulateMouseEvents(matchingLink, 'click', browserOffset)
549 # When you go to the next page of GitHub’s code search results, the page is
550 # loaded with AJAX. GitHub then annoyingly focuses its search input. This
551 # autofocus cannot be prevented in a reliable way, because the case is
552 # indistinguishable from a button whose job is to focus some text input.
553 # However, in this command we know for sure that we can prevent the next
554 # focus. This must be done _after_ the click has been triggered, since
555 # clicks count as page interactions.
556 vim.markPageInteraction(false)
557 else
558 vim.notify(translate("notification.follow_#{type}.none"))
559
560 commands.focus_text_input = ({vim, count = null}) ->
561 {lastFocusedTextInput} = vim.state
562
563 if lastFocusedTextInput and utils.isDetached(lastFocusedTextInput)
564 lastFocusedTextInput = null
565
566 candidates = utils.querySelectorAllDeep(
567 vim.content, 'input, textarea, textbox, [contenteditable]'
568 )
569 inputs = Array.filter(candidates, (element) ->
570 return isTextInputElement(element) and utils.area(element) > 0
571 )
572 if lastFocusedTextInput and lastFocusedTextInput not in inputs
573 inputs.push(lastFocusedTextInput)
574 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
575
576 if inputs.length == 0
577 vim.notify(translate('notification.focus_text_input.none'))
578 return
579
580 num = switch
581 when count?
582 count
583 when lastFocusedTextInput
584 inputs.indexOf(lastFocusedTextInput) + 1
585 else
586 1
587 index = Math.min(num, inputs.length) - 1
588 select = (count? or not vim.state.hasFocusedTextInput)
589 utils.focusElement(inputs[index], {select})
590 vim.state.inputs = inputs
591
592 commands.clear_inputs = ({vim}) ->
593 vim.state.inputs = null
594
595 commands.move_focus = ({vim, direction}) ->
596 return false unless vim.state.inputs
597 index = vim.state.inputs.indexOf(utils.getActiveElement(vim.content))
598 # If there’s only one input, `<tab>` would cycle to itself, making it feel
599 # like `<tab>` was not working. Then it’s better to let `<tab>` work as it
600 # usually does.
601 if index == -1 or vim.state.inputs.length <= 1
602 vim.state.inputs = null
603 return false
604 else
605 {inputs} = vim.state
606 nextInput = inputs[(index + direction) %% inputs.length]
607 utils.focusElement(nextInput, {select: true})
608 return true
609
610 # This is an attempt to enhance Firefox’s native “Find in page” functionality.
611 # Firefox starts searching after the end of the first selection range, or from
612 # the top of the page if there are no selection ranges. If there are frames, the
613 # top-most document in DOM order with selections seems to be used.
614 #
615 # Replace the current selection with one single range. (Searching clears the
616 # previous selection anyway.) That single range is either the first visible
617 # range, or a newly created (and collapsed) one at the top of the viewport. This
618 # way we can control where Firefox searches from.
619 commands.find_from_top_of_viewport = ({vim, direction}) ->
620 viewport = viewportUtils.getWindowViewport(vim.content)
621
622 range = viewportUtils.getFirstVisibleRange(vim.content, viewport)
623 if range
624 window = range.startContainer.ownerGlobal
625 selection = window.getSelection()
626 utils.clearSelectionDeep(vim.content)
627 window.focus()
628 # When the next match is in another frame than the current selection (A),
629 # Firefox won’t clear that selection before making a match selection (B) in
630 # the other frame. When searching again, selection B is cleared because
631 # selection A appears further up the viewport. This causes us to search
632 # _again_ from selection A, rather than selection B. In effect, we get stuck
633 # re-selecting selection B over and over. Therefore, collapse the range
634 # first, in case Firefox doesn’t.
635 range.collapse()
636 selection.addRange(range)
637 # Collapsing the range causes backwards search to keep re-selecting the same
638 # match. Therefore, move it one character back.
639 selection.modify('move', 'backward', 'character') if direction == BACKWARD
640 return
641
642 result = viewportUtils.getFirstVisibleText(vim.content, viewport)
643 return unless result
644 [textNode, offset] = result
645
646 utils.clearSelectionDeep(vim.content)
647 window = textNode.ownerGlobal
648 window.focus()
649 range = window.document.createRange()
650 range.setStart(textNode, offset)
651 range.setEnd(textNode, offset)
652 selection = window.getSelection()
653 selection.addRange(range)
654
655 commands.esc = (args) ->
656 {vim} = args
657 commands.blur_active_element(args)
658 vim.clearHover()
659 utils.clearSelectionDeep(vim.content)
660
661 {document} = vim.content
662 if document.exitFullscreen
663 document.exitFullscreen()
664 else
665 document.mozCancelFullScreen()
666
667 commands.blur_active_element = ({vim}) ->
668 vim.state.explicitBodyFocus = false
669 utils.blurActiveElement(vim.content)
670
671 helper_create_selection_manager = (vim) ->
672 window = utils.getActiveElement(vim.content)?.ownerGlobal ? vim.content
673 return new SelectionManager(window)
674
675 commands.enable_caret = ({vim}) ->
676 return unless selectionManager = helper_create_selection_manager(vim)
677 selectionManager.enableCaret()
678
679 commands.move_caret = ({vim, method, direction, select, count}) ->
680 return unless selectionManager = helper_create_selection_manager(vim)
681 for [0...count] by 1
682 if selectionManager[method]
683 error = selectionManager[method](direction, select)
684 else
685 error = selectionManager.moveCaret(method, direction, select)
686 break if error
687 return
688
689 commands.toggle_selection = ({vim, select}) ->
690 return unless selectionManager = helper_create_selection_manager(vim)
691 if select
692 vim.notify(translate('notification.toggle_selection.enter'))
693 else
694 selectionManager.collapse()
695
696 commands.toggle_selection_direction = ({vim}) ->
697 return unless selectionManager = helper_create_selection_manager(vim)
698 selectionManager.reverseDirection()
699
700 commands.get_selection = ({vim}) ->
701 return unless selectionManager = helper_create_selection_manager(vim)
702 return selectionManager.selection.toString()
703
704 commands.collapse_selection = ({vim}) ->
705 return unless selectionManager = helper_create_selection_manager(vim)
706 selectionManager.collapse()
707
708 commands.clear_selection = ({vim, blur}) ->
709 utils.clearSelectionDeep(vim.content, {blur})
710
711 commands.modal = ({vim, type, args}) ->
712 return vim.content[type](args...)
713
714 module.exports = commands
Imprint / Impressum