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