]> git.gir.st - VimFx.git/blob - extension/lib/commands.coffee
Make `paste_and_go` work exactly like Firefox
[VimFx.git] / extension / lib / commands.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014.
4 # Copyright Wang Zhuochun 2013, 2014.
5 #
6 # This file is part of VimFx.
7 #
8 # VimFx is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # VimFx is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
20 ###
21
22 help = require('./help')
23 { Marker } = require('./marker')
24 utils = require('./utils')
25
26 { isProperLink, isTextInputElement, isContentEditable } = utils
27
28 { classes: Cc, interfaces: Ci, utils: Cu } = Components
29
30 XULDocument = Ci.nsIDOMXULDocument
31
32 commands = {}
33
34
35
36 commands.focus_location_bar = ({ vim }) ->
37 # This function works even if the Address Bar has been removed.
38 vim.rootWindow.focusAndSelectUrlBar()
39
40 commands.focus_search_bar = ({ vim }) ->
41 # The `.webSearch()` method opens a search engine in a tab if the Search Bar
42 # has been removed. Therefore we first check if it exists.
43 if vim.rootWindow.BrowserSearch.searchBar
44 vim.rootWindow.BrowserSearch.webSearch()
45
46 helper_paste_and_go = (event, { vim }) ->
47 { gURLBar, goDoCommand } = vim.rootWindow
48 gURLBar.value = vim.rootWindow.readFromClipboard()
49 gURLBar.handleCommand(event)
50
51 commands.paste_and_go = helper_paste_and_go.bind(null, null)
52
53 commands.paste_and_go_in_tab = helper_paste_and_go.bind(null, {altKey: true})
54
55 commands.copy_current_url = ({ vim }) ->
56 utils.writeToClipboard(vim.rootWindow.gBrowser.currentURI.spec)
57
58 # Go up one level in the URL hierarchy.
59 commands.go_up_path = ({ vim, count }) ->
60 { pathname } = vim.window.location
61 vim.window.location.pathname = pathname.replace(
62 /// (?: /[^/]+ ){1,#{ count ? 1 }} /?$ ///, ''
63 )
64
65 # Go up to root of the URL hierarchy.
66 commands.go_to_root = ({ vim }) ->
67 vim.window.location.href = vim.window.location.origin
68
69 commands.go_home = ({ vim }) ->
70 vim.rootWindow.BrowserHome()
71
72 helper_go_history = (num, { vim, count }) ->
73 { gBrowser } = vim.rootWindow
74 { index, count: length } = gBrowser.sessionHistory
75 newIndex = index + num * (count ? 1)
76 newIndex = Math.max(newIndex, 0)
77 newIndex = Math.min(newIndex, length - 1)
78 gBrowser.gotoIndex(newIndex) unless newIndex == index
79
80 commands.history_back = helper_go_history.bind(null, -1)
81
82 commands.history_forward = helper_go_history.bind(null, +1)
83
84 commands.reload = ({ vim }) ->
85 vim.rootWindow.BrowserReload()
86
87 commands.reload_force = ({ vim }) ->
88 vim.rootWindow.BrowserReloadSkipCache()
89
90 commands.reload_all = ({ vim }) ->
91 vim.rootWindow.gBrowser.reloadAllTabs()
92
93 commands.reload_all_force = ({ vim }) ->
94 for tab in vim.rootWindow.gBrowser.visibleTabs
95 gBrowser = tab.linkedBrowser
96 consts = gBrowser.webNavigation
97 flags = consts.LOAD_FLAGS_BYPASS_PROXY | consts.LOAD_FLAGS_BYPASS_CACHE
98 gBrowser.reload(flags)
99 return
100
101 commands.stop = ({ vim }) ->
102 vim.rootWindow.BrowserStop()
103
104 commands.stop_all = ({ vim }) ->
105 for tab in vim.rootWindow.gBrowser.visibleTabs
106 tab.linkedBrowser.stop()
107 return
108
109
110
111 axisMap =
112 x: ['left', 'scrollLeftMax', 'clientWidth', 'horizontalScrollDistance', 5]
113 y: ['top', 'scrollTopMax', 'clientHeight', 'verticalScrollDistance', 20]
114
115 helper_scroll = (method, type, axis, amount, { vim, event, count }) ->
116 frameDocument = event.target.ownerDocument
117 element =
118 if vim.state.scrollableElements.has(event.target)
119 event.target
120 else
121 frameDocument.documentElement
122
123 [ direction, max, dimension, distance, lineAmount ] = axisMap[axis]
124
125 if method == 'scrollTo'
126 amount = Math.min(amount, element[max])
127 else
128 unit = switch type
129 when 'lines'
130 prefs.root.get("toolkit.scrollbox.#{ distance }") * lineAmount
131 when 'pages'
132 element[dimension]
133 amount *= unit * (count ? 1)
134
135 options = {}
136 options[direction] = amount
137 if prefs.root.get('general.smoothScroll') and
138 prefs.root.get("general.smoothScroll.#{ type }")
139 options.behavior = 'smooth'
140
141 prefs.root.tmp(
142 'layout.css.scroll-behavior.spring-constant',
143 vim.parent.options["smoothScroll.#{ type }.spring-constant"],
144 ->
145 element[method](options)
146 # When scrolling the whole page, the body sometimes needs to be scrolled
147 # too.
148 if element == frameDocument.documentElement
149 frameDocument.body?[method](options)
150 )
151
152 scroll = Function::bind.bind(helper_scroll, null)
153
154 commands.scroll_left = scroll('scrollBy', 'lines', 'x', -1)
155 commands.scroll_right = scroll('scrollBy', 'lines', 'x', +1)
156 commands.scroll_down = scroll('scrollBy', 'lines', 'y', +1)
157 commands.scroll_up = scroll('scrollBy', 'lines', 'y', -1)
158 commands.scroll_page_down = scroll('scrollBy', 'pages', 'y', +1)
159 commands.scroll_page_up = scroll('scrollBy', 'pages', 'y', -1)
160 commands.scroll_half_page_down = scroll('scrollBy', 'pages', 'y', +0.5)
161 commands.scroll_half_page_up = scroll('scrollBy', 'pages', 'y', -0.5)
162 commands.scroll_to_top = scroll('scrollTo', 'other', 'y', 0)
163 commands.scroll_to_bottom = scroll('scrollTo', 'other', 'y', Infinity)
164 commands.scroll_to_left = scroll('scrollTo', 'other', 'x', 0)
165 commands.scroll_to_right = scroll('scrollTo', 'other', 'x', Infinity)
166
167
168
169 commands.tab_new = ({ vim }) ->
170 vim.rootWindow.BrowserOpenTab()
171
172 commands.tab_duplicate = ({ vim }) ->
173 { gBrowser } = vim.rootWindow
174 gBrowser.duplicateTab(gBrowser.selectedTab)
175
176 absoluteTabIndex = (relativeIndex, gBrowser) ->
177 tabs = gBrowser.visibleTabs
178 { selectedTab } = gBrowser
179
180 currentIndex = tabs.indexOf(selectedTab)
181 absoluteIndex = currentIndex + relativeIndex
182 numTabs = tabs.length
183
184 wrap = (Math.abs(relativeIndex) == 1)
185 if wrap
186 absoluteIndex %%= numTabs
187 else
188 absoluteIndex = Math.max(0, absoluteIndex)
189 absoluteIndex = Math.min(absoluteIndex, numTabs - 1)
190
191 return absoluteIndex
192
193 helper_switch_tab = (direction, { vim, count }) ->
194 { gBrowser } = vim.rootWindow
195 gBrowser.selectTabAtIndex(absoluteTabIndex(direction * (count ? 1), gBrowser))
196
197 commands.tab_select_previous = helper_switch_tab.bind(null, -1)
198
199 commands.tab_select_next = helper_switch_tab.bind(null, +1)
200
201 helper_move_tab = (direction, { vim, count }) ->
202 { gBrowser } = vim.rootWindow
203 { selectedTab } = gBrowser
204 { pinned } = selectedTab
205
206 index = absoluteTabIndex(direction * (count ? 1), gBrowser)
207
208 if index < gBrowser._numPinnedTabs
209 gBrowser.pinTab(selectedTab) unless pinned
210 else
211 gBrowser.unpinTab(selectedTab) if pinned
212
213 gBrowser.moveTabTo(selectedTab, index)
214
215 commands.tab_move_backward = helper_move_tab.bind(null, -1)
216
217 commands.tab_move_forward = helper_move_tab.bind(null, +1)
218
219 commands.tab_select_first = ({ vim }) ->
220 vim.rootWindow.gBrowser.selectTabAtIndex(0)
221
222 commands.tab_select_first_non_pinned = ({ vim }) ->
223 firstNonPinned = vim.rootWindow.gBrowser._numPinnedTabs
224 vim.rootWindow.gBrowser.selectTabAtIndex(firstNonPinned)
225
226 commands.tab_select_last = ({ vim }) ->
227 vim.rootWindow.gBrowser.selectTabAtIndex(-1)
228
229 commands.tab_toggle_pinned = ({ vim }) ->
230 currentTab = vim.rootWindow.gBrowser.selectedTab
231 if currentTab.pinned
232 vim.rootWindow.gBrowser.unpinTab(currentTab)
233 else
234 vim.rootWindow.gBrowser.pinTab(currentTab)
235
236 commands.tab_close = ({ vim, count }) ->
237 { gBrowser } = vim.rootWindow
238 return if gBrowser.selectedTab.pinned
239 currentIndex = gBrowser.visibleTabs.indexOf(gBrowser.selectedTab)
240 for tab in gBrowser.visibleTabs[currentIndex...(currentIndex + (count ? 1))]
241 gBrowser.removeTab(tab)
242 return
243
244 commands.tab_restore = ({ vim, count }) ->
245 vim.rootWindow.undoCloseTab() for [1..count ? 1] by 1
246
247 commands.tab_close_to_end = ({ vim }) ->
248 { gBrowser } = vim.rootWindow
249 gBrowser.removeTabsToTheEndFrom(gBrowser.selectedTab)
250
251 commands.tab_close_other = ({ vim }) ->
252 { gBrowser } = vim.rootWindow
253 gBrowser.removeAllTabsBut(gBrowser.selectedTab)
254
255
256
257 # Combine links with the same href.
258 combine = (hrefs, marker) ->
259 if marker.type == 'link'
260 { href } = marker.element
261 if href of hrefs
262 parent = hrefs[href]
263 marker.parent = parent
264 parent.weight += marker.weight
265 parent.numChildren++
266 else
267 hrefs[href] = marker
268 return marker
269
270 follow_callback = (vim, { inTab, inBackground }, marker, count, keyStr) ->
271 isLast = (count == 1)
272 isLink = (marker.type == 'link')
273
274 switch
275 when keyStr.startsWith(vim.parent.options.hints_toggle_in_tab)
276 inTab = not inTab
277 when keyStr.startsWith(vim.parent.options.hints_toggle_in_background)
278 inTab = true
279 inBackground = not inBackground
280 else
281 unless isLast
282 inTab = true
283 inBackground = true
284
285 inTab = false unless isLink
286
287 if marker.type == 'text' or (isLink and not (inTab and inBackground))
288 isLast = true
289
290 { element } = marker
291 utils.focusElement(element)
292
293 if inTab
294 utils.openTab(vim.rootWindow, element.href, {
295 inBackground
296 relatedToCurrent: true
297 })
298 else
299 if element.target == '_blank' and vim.parent.options.prevent_target_blank
300 targetReset = element.target
301 element.target = ''
302 utils.simulateClick(element)
303 element.target = targetReset if targetReset
304
305 return not isLast
306
307 # Follow links, focus text inputs and click buttons with hint markers.
308 commands.follow = ({ vim, count }) ->
309 hrefs = {}
310 filter = (element, getElementShape) ->
311 document = element.ownerDocument
312 isXUL = (document instanceof XULDocument)
313 semantic = true
314 switch
315 when isProperLink(element)
316 type = 'link'
317 when isTextInputElement(element) or isContentEditable(element)
318 type = 'text'
319 when element.tabIndex > -1 and
320 not (isXUL and element.nodeName.endsWith('box'))
321 type = 'clickable'
322 unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON']
323 semantic = false
324 when element != document.documentElement and
325 vim.state.scrollableElements.has(element)
326 type = 'scrollable'
327 when element.hasAttribute('onclick') or
328 element.hasAttribute('onmousedown') or
329 element.hasAttribute('onmouseup') or
330 element.hasAttribute('oncommand') or
331 element.getAttribute('role') in ['link', 'button'] or
332 # Twitter special-case.
333 element.classList.contains('js-new-tweets-bar') or
334 # Feedly special-case.
335 element.hasAttribute('data-app-action') or
336 element.hasAttribute('data-uri') or
337 element.hasAttribute('data-page-action')
338 type = 'clickable'
339 semantic = false
340 # Putting markers on `<label>` elements is generally redundant, because
341 # its `<input>` gets one. However, some sites hide the actual `<input>`
342 # but keeps the `<label>` to click, either for styling purposes or to keep
343 # the `<input>` hidden until it is used. In those cases we should add a
344 # marker for the `<label>`.
345 when element.nodeName == 'LABEL'
346 if element.htmlFor
347 input = document.getElementById(element.htmlFor)
348 if input and not getElementShape(input)
349 type = 'clickable'
350 # Elements that have “button” somewhere in the class might be clickable,
351 # unless they contain a real link or button or yet an element with
352 # “button” somewhere in the class, in which case they likely are
353 # “button-wrapper”s. (`<SVG element>.className` is not a string!)
354 when not isXUL and typeof element.className == 'string' and
355 element.className.toLowerCase().contains('button')
356 unless element.querySelector('a, button, [class*=button]')
357 type = 'clickable'
358 semantic = false
359 # When viewing an image it should get a marker to toggle zoom.
360 when document.body?.childElementCount == 1 and
361 element.nodeName == 'IMG' and
362 (element.classList.contains('overflowing') or
363 element.classList.contains('shrinkToFit'))
364 type = 'clickable'
365 return unless type
366 return unless shape = getElementShape(element)
367 return combine(hrefs, new Marker(element, shape, {semantic, type}))
368
369 callback = follow_callback.bind(null, vim, {inTab: false, inBackground: true})
370
371 vim.enterMode('hints', filter, callback, count)
372
373 # Follow links in a new background tab with hint markers.
374 commands.follow_in_tab = ({ vim, count }, inBackground = true) ->
375 hrefs = {}
376 filter = (element, getElementShape) ->
377 return unless isProperLink(element)
378 return unless shape = getElementShape(element)
379 return combine(hrefs, new Marker(element, shape,
380 {semantic: true, type: 'link'}))
381
382 callback = follow_callback.bind(null, vim, {inTab: true, inBackground})
383
384 vim.enterMode('hints', filter, callback, count)
385
386 # Follow links in a new foreground tab with hint markers.
387 commands.follow_in_focused_tab = (args) ->
388 commands.follow_in_tab(args, false)
389
390 # Like command_follow but multiple times.
391 commands.follow_multiple = (args) ->
392 args.count = Infinity
393 commands.follow(args)
394
395 # Copy the URL or text of a markable element to the system clipboard.
396 commands.follow_copy = ({ vim }) ->
397 hrefs = {}
398 filter = (element, getElementShape) ->
399 type = switch
400 when isProperLink(element) then 'link'
401 when isTextInputElement(element) then 'textInput'
402 when isContentEditable(element) then 'contenteditable'
403 return unless type
404 return unless shape = getElementShape(element)
405 return combine(hrefs, new Marker(element, shape, {semantic: true, type}))
406
407 callback = (marker) ->
408 { element } = marker
409 text = switch marker.type
410 when 'link' then element.href
411 when 'textInput' then element.value
412 when 'contenteditable' then element.textContent
413 utils.writeToClipboard(text)
414
415 vim.enterMode('hints', filter, callback)
416
417 # Focus element with hint markers.
418 commands.follow_focus = ({ vim }) ->
419 filter = (element, getElementShape) ->
420 type = switch
421 when element.tabIndex > -1
422 'focusable'
423 when element != element.ownerDocument.documentElement and
424 vim.state.scrollableElements.has(element)
425 'scrollable'
426 return unless type
427 return unless shape = getElementShape(element)
428 return new Marker(element, shape, {semantic: true, type})
429
430 callback = (marker) ->
431 { element } = marker
432 utils.focusElement(element, {select: true})
433
434 vim.enterMode('hints', filter, callback)
435
436 helper_follow_pattern = (type, { vim }) ->
437 { document } = vim.window
438
439 # If there’s a `<link rel=prev/next>` element we use that.
440 for link in document.head?.getElementsByTagName('link')
441 # Also support `rel=previous`, just like Google.
442 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
443 vim.rootWindow.gBrowser.loadURI(link.href)
444 return
445
446 # Otherwise we look for a link or button on the page that seems to go to the
447 # previous or next page.
448 candidates = document.querySelectorAll(vim.parent.options.pattern_selector)
449
450 # Note: Earlier patterns should be favored.
451 patterns = vim.parent.options["#{ type }_patterns"]
452
453 # Search for the prev/next patterns in the following attributes of the
454 # element. `rel` should be kept as the first attribute, since the standard way
455 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
456 # favored. Even though some of these attributes only allow a fixed set of
457 # keywords, we pattern-match them anyways since lots of sites don’t follow the
458 # spec and use the attributes arbitrarily.
459 attrs = vim.parent.options.pattern_attrs
460
461 matchingLink = do ->
462 # Helper function that matches a string against all the patterns.
463 matches = (text) -> patterns.some((regex) -> regex.test(text))
464
465 # First search in attributes (favoring earlier attributes) as it's likely
466 # that they are more specific than text contexts.
467 for attr in attrs
468 for element in candidates
469 return element if matches(element.getAttribute(attr))
470
471 # Then search in element contents.
472 for element in candidates
473 return element if matches(element.textContent)
474
475 return null
476
477 utils.simulateClick(matchingLink) if matchingLink
478
479 commands.follow_previous = helper_follow_pattern.bind(null, 'prev')
480
481 commands.follow_next = helper_follow_pattern.bind(null, 'next')
482
483 # Focus last focused or first text input.
484 commands.focus_text_input = ({ vim, storage, count }) ->
485 { lastFocusedTextInput } = vim.state
486 inputs = Array.filter(
487 vim.window.document.querySelectorAll('input, textarea'), (element) ->
488 return utils.isTextInputElement(element) and utils.area(element) > 0
489 )
490 if lastFocusedTextInput and lastFocusedTextInput not in inputs
491 inputs.push(lastFocusedTextInput)
492 return unless inputs.length > 0
493 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
494 unless count?
495 count =
496 if lastFocusedTextInput
497 inputs.indexOf(lastFocusedTextInput) + 1
498 else
499 1
500 index = Math.min(count, inputs.length) - 1
501 utils.focusElement(inputs[index], {select: true})
502 storage.inputs = inputs
503
504 # Switch between text inputs or simulate `<tab>`.
505 helper_move_focus = (direction, { vim, storage }) ->
506 if storage.inputs
507 { inputs } = storage
508 nextInput = inputs[(storage.inputIndex + direction) %% inputs.length]
509 utils.focusElement(nextInput, {select: true})
510 else
511 focusManager = Cc['@mozilla.org/focus-manager;1']
512 .getService(Ci.nsIFocusManager)
513 direction =
514 if direction == -1
515 focusManager.MOVEFOCUS_BACKWARD
516 else
517 focusManager.MOVEFOCUS_FORWARD
518 focusManager.moveFocus(
519 null, # Use current window.
520 null, # Move relative to the currently focused element.
521 direction,
522 focusManager.FLAG_BYKEY
523 )
524
525 commands.focus_next = helper_move_focus.bind(null, +1)
526 commands.focus_previous = helper_move_focus.bind(null, -1)
527
528
529
530 findStorage = {lastSearchString: ''}
531
532 helper_find = (highlight, { vim }) ->
533 findBar = vim.rootWindow.gBrowser.getFindBar()
534
535 findBar.onFindCommand()
536 utils.focusElement(findBar._findField, {select: true})
537
538 return unless highlightButton = findBar.getElement('highlight')
539 if highlightButton.checked != highlight
540 highlightButton.click()
541
542 # Open the find bar, making sure that hightlighting is off.
543 commands.find = helper_find.bind(null, false)
544
545 # Open the find bar, making sure that hightlighting is on.
546 commands.find_highlight_all = helper_find.bind(null, true)
547
548 helper_find_again = (direction, { vim }) ->
549 findBar = vim.rootWindow.gBrowser.getFindBar()
550 if findStorage.lastSearchString.length > 0
551 findBar._findField.value = findStorage.lastSearchString
552 findBar.onFindAgainCommand(direction)
553
554 commands.find_next = helper_find_again.bind(null, false)
555
556 commands.find_previous = helper_find_again.bind(null, true)
557
558
559
560 commands.enter_mode_ignore = ({ vim }) ->
561 vim.enterMode('ignore')
562
563 # Quote next keypress (pass it through to the page).
564 commands.quote = ({ vim, count }) ->
565 vim.enterMode('ignore', count ? 1)
566
567 # Display the Help Dialog.
568 commands.help = ({ vim }) ->
569 help.injectHelp(vim.rootWindow, vim.parent)
570
571 # Open and focus the Developer Toolbar.
572 commands.dev = ({ vim }) ->
573 vim.rootWindow.DeveloperToolbar.show(true) # `true` to focus.
574
575 commands.esc = ({ vim, event }) ->
576 utils.blurActiveElement(vim.window)
577 utils.blurActiveElement(vim.rootWindow)
578
579 help.removeHelp(vim.rootWindow)
580
581 vim.rootWindow.DeveloperToolbar.hide()
582
583 vim.rootWindow.gBrowser.getFindBar().close()
584
585 vim.rootWindow.TabView.hide()
586
587 { document } = vim.window
588 if document.exitFullscreen
589 document.exitFullscreen()
590 else
591 document.mozCancelFullScreen()
592
593
594
595 module.exports = {
596 commands
597 findStorage
598 }
Imprint / Impressum