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