]> git.gir.st - VimFx.git/blob - extension/packages/events.coffee
Merge pull request #322 from lydell/fix-autofocus
[VimFx.git] / extension / packages / events.coffee
1 utils = require 'utils'
2 keyUtils = require 'key-utils'
3 { Vim } = require 'vim'
4 { getPref } = require 'prefs'
5 { updateToolbarButton } = require 'button'
6 { unload } = require 'unload'
7
8 { interfaces: Ci } = Components
9
10 HTMLDocument = Ci.nsIDOMHTMLDocument
11
12 vimBucket = new utils.Bucket(utils.getWindowId, (w) -> new Vim(w))
13
14 keyStrFromEvent = (event) ->
15 { ctrlKey: ctrl, metaKey: meta, altKey: alt, shiftKey: shift } = event
16
17 if !meta and !alt
18 return unless keyChar = keyUtils.keyCharFromCode(event.keyCode, shift)
19 keyStr = keyUtils.applyModifiers(keyChar, ctrl, alt, meta)
20 return keyStr
21
22 return null
23
24 # When a menu or panel is shown VimFx should temporarily stop processing keyboard input, allowing
25 # accesskeys to be used.
26 popupPassthrough = false
27 checkPassthrough = (event) ->
28 if event.target.nodeName in ['menupopup', 'panel']
29 popupPassthrough = switch event.type
30 when 'popupshown' then true
31 when 'popuphidden' then false
32
33 suppress = false
34 suppressEvent = (event) ->
35 event.preventDefault()
36 event.stopPropagation()
37
38 # Returns the appropriate vim instance for `event`, but only if it’s okay to do
39 # so. VimFx must not be disabled or blacklisted.
40 getVimFromEvent = (event) ->
41 return if getPref('disabled')
42 return unless window = utils.getEventCurrentTabWindow(event)
43 return unless vim = vimBucket.get(window)
44 return if vim.blacklisted
45
46 return vim
47
48 removeVimFromTab = (tab, gBrowser) ->
49 return unless browser = gBrowser.getBrowserForTab(tab)
50 vimBucket.forget(browser.contentWindow)
51
52 updateButton = (vim) ->
53 updateToolbarButton(vim.rootWindow, {blacklisted: vim.blacklisted, insertMode: vim.mode == 'insert'})
54
55 # The following listeners are installed on every top level Chrome window
56 windowsListeners =
57 keydown: (event) ->
58 try
59 # No matter what, always reset the `suppress` flag, so we don't suppress more than intended.
60 suppress = false
61
62 if popupPassthrough
63 # The `popupPassthrough` flag is set a bit unreliably. Sometimes it can be stuck as `true`
64 # even though no popup is shown, effectively disabling the extension. Therefore we check
65 # if there actually _are_ any open popups before stopping processing keyboard input. This is
66 # only done when popups (might) be open (not on every keystroke) of performance reasons.
67 return unless rootWindow = utils.getEventRootWindow(event)
68 popups = rootWindow.document.querySelectorAll('menupopup, panel')
69 for popup in popups
70 return if popup.state == 'open'
71 popupPassthrough = false # No popup was actually open: Reset the flag.
72
73 return unless vim = getVimFromEvent(event)
74 return unless keyStr = keyStrFromEvent(event)
75 suppress = vim.onInput(keyStr, event)
76
77 suppressEvent(event) if suppress
78
79 catch error
80 console.error("#{ error }\n#{ error.stack?.replace(/@.+-> /g, '@') }")
81
82 # Note that the below event listeners can suppress the event even in blacklisted sites. That's
83 # intentional. For example, if you press 'x' to close the current tab, it will close before keyup
84 # fires. So keyup (and perhaps keypress) will fire in another tab. Even if that particular tab is
85 # blacklisted, we must suppress the event, so that 'x' isn't sent to the page. The rule is simple:
86 # If the `suppress` flag is `true`, the event should be suppressed, no matter what. It has the
87 # highest priority.
88 keypress: (event) -> suppressEvent(event) if suppress
89 keyup: (event) -> suppressEvent(event) if suppress
90
91 popupshown: checkPassthrough
92 popuphidden: checkPassthrough
93
94 focus: (event) ->
95 return unless getPref('prevent_autofocus')
96
97 target = event.originalTarget
98 return unless target.ownerDocument instanceof HTMLDocument
99
100 # We only prevent autofocus from editable elements, that is, elements that
101 # can “steal” the keystrokes, in order not to interfere too much.
102 return unless utils.isElementEditable(target)
103
104 return unless vim = getVimFromEvent(event)
105
106 # Focus events can occur before DOMContentLoaded, both when the `autofocus`
107 # attribute is used, and when a script contains `element.focus()`. If so,
108 # `vim.lastLoad` isn’t set yet and we should blur the target. If
109 # DOMContentLoaded already has fired we blur only if the focus happens
110 # within a second after it.
111 if !vim.lastLoad or Date.now() - vim.lastLoad < 1000
112 target.blur()
113
114 # Record when the page was loaded. This is used by the autofocus prevention feature.
115 DOMContentLoaded: (event) ->
116 target = event.originalTarget
117 return unless target instanceof HTMLDocument
118 return unless vim = getVimFromEvent(event)
119
120 # Some pages (at least amazon.com) loads weirdly, firing DOMContentLoaded
121 # many times. Therefore we only set `vim.lastLoad` if it wasn’t set
122 # already. Otherwise too many focus events might be considered to be
123 # autofocus events and thus blurred, even though the user might have
124 # focused something on his own.
125 if !vim.lastLoad
126 vim.lastLoad = Date.now()
127 return
128
129 # When the top level window closes we should release all Vims that were
130 # associated with tabs in this window
131 DOMWindowClose: (event) ->
132 { gBrowser } = event.originalTarget
133 return unless gBrowser
134 for tab in gBrowser.tabs
135 removeVimFromTab(tab, gBrowser)
136
137 TabClose: (event) ->
138 { gBrowser } = utils.getEventRootWindow(event) ? {}
139 return unless gBrowser
140 tab = event.originalTarget
141 removeVimFromTab(tab, gBrowser)
142
143 # Update the toolbar button icon to reflect the blacklisted state
144 TabSelect: (event) ->
145 return unless window = event.originalTarget?.linkedBrowser?.contentDocument?.defaultView
146 return unless vim = vimBucket.get(window)
147 updateButton(vim)
148
149
150 # This listener works on individual tabs within Chrome Window
151 tabsListener =
152 onLocationChange: (browser, webProgress, request, location) ->
153 return unless vim = vimBucket.get(browser.contentWindow)
154
155 # Mark the new location as not loaded yet.
156 vim.lastLoad = null
157
158 # If the location changes when in hints mode (for example because the reload button has been
159 # clicked), we're going to end up in hints mode without any markers. So switch back to normal
160 # mode in that case.
161 if vim.mode == 'hints'
162 vim.enterMode('normal')
163
164 # Update the blacklist state.
165 vim.blacklisted = utils.isBlacklisted(location.spec)
166 updateButton(vim)
167
168 addEventListeners = (window) ->
169 for name, listener of windowsListeners
170 window.addEventListener(name, listener, true)
171
172 window.gBrowser.addTabsProgressListener(tabsListener)
173
174 unload ->
175 for name, listener of windowsListeners
176 window.removeEventListener(name, listener, true)
177
178 window.gBrowser.removeTabsProgressListener(tabsListener)
179
180 exports.addEventListeners = addEventListeners
181 exports.vimBucket = vimBucket
Imprint / Impressum