]> git.gir.st - VimFx.git/blob - extension/packages/events.coffee
Suppress non-hintchars key presses in hints mode
[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 HTMLInputElement = Ci.nsIDOMHTMLInputElement
12
13 vimBucket = new utils.Bucket((window) -> new Vim(window))
14
15 keyStrFromEvent = (event) ->
16 { ctrlKey: ctrl, metaKey: meta, altKey: alt, shiftKey: shift } = event
17
18 if !meta and !alt
19 return unless keyChar = keyUtils.keyCharFromCode(event.keyCode, shift)
20 keyStr = keyUtils.applyModifiers(keyChar, ctrl, alt, meta)
21 return keyStr
22
23 return null
24
25 # When a menu or panel is shown VimFx should temporarily stop processing keyboard input, allowing
26 # accesskeys to be used.
27 popupPassthrough = false
28 checkPassthrough = (event) ->
29 if event.target.nodeName in ['menupopup', 'panel']
30 popupPassthrough = switch event.type
31 when 'popupshown' then true
32 when 'popuphidden' then false
33
34 suppress = false
35 suppressEvent = (event) ->
36 event.preventDefault()
37 event.stopPropagation()
38
39 # Returns the appropriate vim instance for `event`, but only if it’s okay to do
40 # so. VimFx must not be disabled or blacklisted.
41 getVimFromEvent = (event) ->
42 return if getPref('disabled')
43 return unless window = utils.getEventCurrentTabWindow(event)
44 return unless vim = vimBucket.get(window)
45 return if vim.blacklisted
46
47 return vim
48
49 # Save the time of the last user interaction. This is used to determine whether
50 # a focus event was automatic or voluntarily dispatched.
51 markLastInteraction = (event, vim = null) ->
52 return unless vim ?= getVimFromEvent(event)
53 return unless event.originalTarget.ownerDocument instanceof HTMLDocument
54 vim.lastInteraction = Date.now()
55
56 removeVimFromTab = (tab, gBrowser) ->
57 return unless browser = gBrowser.getBrowserForTab(tab)
58 vimBucket.forget(browser.contentWindow)
59
60 updateButton = (vim) ->
61 updateToolbarButton(vim.rootWindow, {blacklisted: vim.blacklisted, insertMode: vim.mode == 'insert'})
62
63 # The following listeners are installed on every top level Chrome window
64 windowsListeners =
65 keydown: (event) ->
66 try
67 # No matter what, always reset the `suppress` flag, so we don't suppress more than intended.
68 suppress = false
69
70 if popupPassthrough
71 # The `popupPassthrough` flag is set a bit unreliably. Sometimes it can be stuck as `true`
72 # even though no popup is shown, effectively disabling the extension. Therefore we check
73 # if there actually _are_ any open popups before stopping processing keyboard input. This is
74 # only done when popups (might) be open (not on every keystroke) of performance reasons.
75 #
76 # The autocomplete popup in text inputs (for example) is technically a panel, but it does
77 # not respond to key presses. Therefore `[ignorekeys="true"]` is excluded.
78 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
79 return unless rootWindow = utils.getEventRootWindow(event)
80 popups = rootWindow.document.querySelectorAll(
81 ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
82 )
83 for popup in popups
84 return if popup.state == 'open'
85 popupPassthrough = false # No popup was actually open: Reset the flag.
86
87 return unless vim = getVimFromEvent(event)
88
89 markLastInteraction(event, vim)
90
91 return unless keyStr = keyStrFromEvent(event)
92 suppress = vim.onInput(keyStr, event)
93
94 suppressEvent(event) if suppress
95
96 catch error
97 console.error("#{ error }\n#{ error.stack?.replace(/@.+-> /g, '@') }")
98
99 # Note that the below event listeners can suppress the event even in blacklisted sites. That's
100 # intentional. For example, if you press 'x' to close the current tab, it will close before keyup
101 # fires. So keyup (and perhaps keypress) will fire in another tab. Even if that particular tab is
102 # blacklisted, we must suppress the event, so that 'x' isn't sent to the page. The rule is simple:
103 # If the `suppress` flag is `true`, the event should be suppressed, no matter what. It has the
104 # highest priority.
105 keypress: (event) -> suppressEvent(event) if suppress
106 keyup: (event) -> suppressEvent(event) if suppress
107
108 popupshown: checkPassthrough
109 popuphidden: checkPassthrough
110
111 focus: (event) ->
112 target = event.originalTarget
113 return unless vim = getVimFromEvent(event)
114
115 findBar = vim.rootWindow.gBrowser.getFindBar()
116 if target == findBar._findField.mInputField
117 vim.enterMode('find')
118 return
119
120 # If the user has interacted with the page and the `window` of the page
121 # gets focus, it means that the user just switched back to the page from
122 # another window or tab. If a text input was focused when the user focused
123 # _away_ from the page Firefox blurs it and then re-focuses it when the
124 # user switches back. Therefore we count this case as an interaction, so
125 # the re-focus event isn’t caught as autofocus.
126 if vim.lastInteraction != null and target == vim.window
127 vim.lastInteraction = Date.now()
128
129 # Autofocus prevention. Strictly speaking, autofocus may only happen during
130 # page load, which means that we should only prevent focus events during
131 # page load. However, it is very difficult to reliably determine when the
132 # page load ends. Moreover, a page may load very slowly. Then it is likely
133 # that the user tries to focus something before the page has loaded fully.
134 # Therefore focus events that aren’t reasonably close to a user interaction
135 # (click or key press) are blurred (regardless of whether the page is
136 # loaded or not -- but that isn’t so bad: if the user doesn’t like
137 # autofocus, he doesn’t like any automatic focusing, right? This is
138 # actually useful on devdocs.io). There is a slight risk that the user
139 # presses a key just before an autofocus, causing it not to be blurred, but
140 # that’s not likely. Lastly, the autofocus prevention is restricted to
141 # `<input>` elements, since only such elements are commonly autofocused.
142 # Many sites have buttons which inserts a `<textarea>` when clicked (which
143 # might take up to a second) and then focuses the `<textarea>`. Such focus
144 # events should _not_ be blurred.
145 if getPref('prevent_autofocus') and
146 target.ownerDocument instanceof HTMLDocument and
147 target instanceof HTMLInputElement and
148 (vim.lastInteraction == null or Date.now() - vim.lastInteraction > 100)
149 target.blur()
150
151 blur: (event) ->
152 target = event.originalTarget
153 return unless vim = getVimFromEvent(event)
154
155 findBar = vim.rootWindow.gBrowser.getFindBar()
156 if target == findBar._findField.mInputField
157 vim.enterMode('normal')
158 return
159
160 click: (event) ->
161 target = event.originalTarget
162 return unless vim = getVimFromEvent(event)
163
164 # If the user clicks the reload button or a link when in hints mode, we’re
165 # going to end up in hints mode without any markers. Or if the user clicks
166 # a text input, then that input will be focused, but you can’t type in it
167 # (instead markers will be matched). So if the user clicks anything in
168 # hints mode it’s better to leave it.
169 if vim.mode == 'hints' and not utils.isEventSimulated(event)
170 vim.enterMode('normal')
171 return
172
173 mousedown: markLastInteraction
174 mouseup: markLastInteraction
175
176 # When the top level window closes we should release all Vims that were
177 # associated with tabs in this window
178 DOMWindowClose: (event) ->
179 { gBrowser } = event.originalTarget
180 return unless gBrowser
181 for tab in gBrowser.tabs
182 removeVimFromTab(tab, gBrowser)
183
184 TabClose: (event) ->
185 { gBrowser } = utils.getEventRootWindow(event) ? {}
186 return unless gBrowser
187 tab = event.originalTarget
188 removeVimFromTab(tab, gBrowser)
189
190 # Update the toolbar button icon to reflect the blacklisted state
191 TabSelect: (event) ->
192 return unless window = event.originalTarget?.linkedBrowser?.contentDocument?.defaultView
193 return unless vim = vimBucket.get(window)
194 updateButton(vim)
195
196
197 # This listener works on individual tabs within Chrome Window
198 tabsListener =
199 onLocationChange: (browser, webProgress, request, location) ->
200 return unless vim = vimBucket.get(browser.contentWindow)
201
202 # There hasn’t been any interaction on the page yet, so reset it.
203 vim.lastInteraction = null
204
205 # Update the blacklist state.
206 vim.blacklisted = utils.isBlacklisted(location.spec)
207 updateButton(vim)
208
209 addEventListeners = (window) ->
210 for name, listener of windowsListeners
211 window.addEventListener(name, listener, true)
212
213 window.gBrowser.addTabsProgressListener(tabsListener)
214
215 unload ->
216 for name, listener of windowsListeners
217 window.removeEventListener(name, listener, true)
218
219 window.gBrowser.removeTabsProgressListener(tabsListener)
220
221 exports.addEventListeners = addEventListeners
222 exports.vimBucket = vimBucket
Imprint / Impressum