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