]> git.gir.st - VimFx.git/blob - extension/lib/events.coffee
Fix #47: 'gi' to focus last focused or first text input
[VimFx.git] / extension / lib / events.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014.
4 #
5 # This file is part of VimFx.
6 #
7 # VimFx is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # VimFx is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
19 ###
20
21 notation = require('vim-like-key-notation')
22 utils = require('./utils')
23 Vim = require('./vim')
24 { getPref } = require('./prefs')
25 { updateToolbarButton } = require('./button')
26
27 { interfaces: Ci } = Components
28
29 HTMLDocument = Ci.nsIDOMHTMLDocument
30 HTMLInputElement = Ci.nsIDOMHTMLInputElement
31
32 vimBucket = new utils.Bucket((window) -> new Vim(window))
33
34 keyStrFromEvent = (event) ->
35 return notation.stringify(event, {
36 # Check that `event.code` really is available before using the
37 # 'ignore_keyboard_layout' option. If not `event.key` will be used anyway,
38 # and the user needs to enable `event.code` support (which can be done from
39 # VimFx’s settings page).
40 ignoreKeyboardLayout: getPref('ignore_keyboard_layout') and event.code?
41 # Advanced setting for advanced users. Currently requires you to modifiy it
42 # from about:config and check the error console for errors.
43 translations: JSON.parse(getPref('translations'))
44 })
45
46 # When a menu or panel is shown VimFx should temporarily stop processing
47 # keyboard input, allowing accesskeys to be used.
48 popupPassthrough = false
49 checkPassthrough = (event) ->
50 if event.target.nodeName in ['menupopup', 'panel']
51 popupPassthrough = switch event.type
52 when 'popupshown' then true
53 when 'popuphidden' then false
54
55 suppress = false
56 suppressEvent = (event) ->
57 event.preventDefault()
58 event.stopPropagation()
59
60 # Returns the appropriate vim instance for `event`, but only if it’s okay to do
61 # so. VimFx must not be disabled or blacklisted.
62 getVimFromEvent = (event) ->
63 return if getPref('disabled')
64 return unless window = utils.getEventCurrentTabWindow(event)
65 return unless vim = vimBucket.get(window)
66 return if vim.state.blacklisted
67
68 return vim
69
70 # Save the time of the last user interaction. This is used to determine whether
71 # a focus event was automatic or voluntarily dispatched.
72 markLastInteraction = (event, vim = null) ->
73 return unless vim ?= getVimFromEvent(event)
74 return unless event.originalTarget.ownerDocument instanceof HTMLDocument
75 vim.state.lastInteraction = Date.now()
76
77 removeVimFromTab = (tab, gBrowser) ->
78 return unless browser = gBrowser.getBrowserForTab(tab)
79 vimBucket.forget(browser.contentWindow)
80
81 updateButton = (vim) ->
82 updateToolbarButton(vim.rootWindow, {
83 blacklisted: vim.state.blacklisted
84 insertMode: vim.mode == 'insert'
85 })
86
87 # The following listeners are installed on every top level Chrome window.
88 windowsListeners =
89 keydown: (event) ->
90 try
91 # No matter what, always reset the `suppress` flag, so we don't suppress
92 # more than intended.
93 suppress = false
94
95 if popupPassthrough
96 # The `popupPassthrough` flag is set a bit unreliably. Sometimes it can
97 # be stuck as `true` even though no popup is shown, effectively
98 # disabling the extension. Therefore we check if there actually _are_
99 # any open popups before stopping processing keyboard input. This is
100 # only done when popups (might) be open (not on every keystroke) of
101 # performance reasons.
102 #
103 # The autocomplete popup in text inputs (for example) is technically a
104 # panel, but it does not respond to key presses. Therefore
105 # `[ignorekeys="true"]` is excluded.
106 #
107 # coffeelint: disable=max_line_length
108 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
109 # coffeelint: enable=max_line_length
110 return unless rootWindow = utils.getEventRootWindow(event)
111 popups = rootWindow.document.querySelectorAll(
112 ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
113 )
114 for popup in popups
115 return if popup.state == 'open'
116 popupPassthrough = false # No popup was actually open: Reset the flag.
117
118 return unless vim = getVimFromEvent(event)
119
120 markLastInteraction(event, vim)
121
122 return unless keyStr = keyStrFromEvent(event)
123 suppress = vim.onInput(keyStr, event)
124
125 suppressEvent(event) if suppress
126
127 catch error
128 console.error("#{ error }\n#{ error.stack?.replace(/@.+-> /g, '@') }")
129
130 # Note that the below event listeners can suppress the event even in
131 # blacklisted sites. That's intentional. For example, if you press 'x' to
132 # close the current tab, it will close before keyup fires. So keyup (and
133 # perhaps keypress) will fire in another tab. Even if that particular tab is
134 # blacklisted, we must suppress the event, so that 'x' isn't sent to the page.
135 # The rule is simple: If the `suppress` flag is `true`, the event should be
136 # suppressed, no matter what. It has the highest priority.
137 keypress: (event) -> suppressEvent(event) if suppress
138 keyup: (event) -> suppressEvent(event) if suppress
139
140 popupshown: checkPassthrough
141 popuphidden: checkPassthrough
142
143 focus: (event) ->
144 target = event.originalTarget
145 return unless vim = getVimFromEvent(event)
146
147 findBar = vim.rootWindow.gBrowser.getFindBar()
148 if target == findBar._findField.mInputField
149 vim.enterMode('find')
150 return
151
152 if target.ownerDocument instanceof HTMLDocument and
153 utils.isTextInputElement(target)
154 vim.state.lastFocusedTextInput = target
155
156 # If the user has interacted with the page and the `window` of the page gets
157 # focus, it means that the user just switched back to the page from another
158 # window or tab. If a text input was focused when the user focused _away_
159 # from the page Firefox blurs it and then re-focuses it when the user
160 # switches back. Therefore we count this case as an interaction, so the
161 # re-focus event isn’t caught as autofocus.
162 if vim.state.lastInteraction != null and target == vim.window
163 vim.state.lastInteraction = Date.now()
164
165 # Autofocus prevention. Strictly speaking, autofocus may only happen during
166 # page load, which means that we should only prevent focus events during
167 # page load. However, it is very difficult to reliably determine when the
168 # page load ends. Moreover, a page may load very slowly. Then it is likely
169 # that the user tries to focus something before the page has loaded fully.
170 # Therefore focus events that aren’t reasonably close to a user interaction
171 # (click or key press) are blurred (regardless of whether the page is loaded
172 # or not -- but that isn’t so bad: if the user doesn’t like autofocus, he
173 # doesn’t like any automatic focusing, right? This is actually useful on
174 # devdocs.io). There is a slight risk that the user presses a key just
175 # before an autofocus, causing it not to be blurred, but that’s not likely.
176 # Lastly, the autofocus prevention is restricted to `<input>` elements,
177 # since only such elements are commonly autofocused. Many sites have
178 # buttons which inserts a `<textarea>` when clicked (which might take up to
179 # a second) and then focuses the `<textarea>`. Such focus events should
180 # _not_ be blurred.
181 if getPref('prevent_autofocus') and
182 vim.mode != 'insert' and
183 target.ownerDocument instanceof HTMLDocument and
184 target instanceof HTMLInputElement and
185 (vim.state.lastInteraction == null or
186 Date.now() - vim.state.lastInteraction > getPref('autofocus_limit'))
187 vim.state.lastAutofocusPrevention = Date.now()
188 target.blur()
189
190 blur: (event) ->
191 target = event.originalTarget
192 return unless vim = getVimFromEvent(event)
193
194 findBar = vim.rootWindow.gBrowser.getFindBar()
195 if target == findBar._findField.mInputField
196 vim.enterMode('normal')
197 return
198
199 # Some sites (such as icloud.com) re-focuses inputs if they are blurred,
200 # causing an infinite loop autofocus prevention and re-focusing. Therefore
201 # we suppress blur events that happen just after an autofocus prevention.
202 if vim.state.lastAutofocusPrevention != null and
203 Date.now() - vim.state.lastAutofocusPrevention < 1
204 vim.state.lastAutofocusPrevention = null
205 suppressEvent(event)
206 return
207
208 click: (event) ->
209 target = event.originalTarget
210 return unless vim = getVimFromEvent(event)
211
212 # If the user clicks the reload button or a link when in hints mode, we’re
213 # going to end up in hints mode without any markers. Or if the user clicks a
214 # text input, then that input will be focused, but you can’t type in it
215 # (instead markers will be matched). So if the user clicks anything in hints
216 # mode it’s better to leave it.
217 if vim.mode == 'hints' and not utils.isEventSimulated(event)
218 vim.enterMode('normal')
219 return
220
221 mousedown: markLastInteraction
222 mouseup: markLastInteraction
223
224 overflow: (event) ->
225 return unless vim = getVimFromEvent(event)
226 return unless computedStyle = vim.window.getComputedStyle(event.target)
227 return if computedStyle.getPropertyValue('overflow') == 'hidden'
228 vim.state.scrollableElements.set(event.target)
229 { largestScrollableElement } = vim.state
230 if largestScrollableElement == null or
231 utils.area(event.target) > utils.area(largestScrollableElement)
232 vim.state.largestScrollableElement = event.target
233
234 underflow: (event) ->
235 return unless vim = getVimFromEvent(event)
236 vim.state.scrollableElements.delete(event.target)
237 # It is not likely that the largest scrollable element underflows, so don’t
238 # bother looking for a new largest one now. Just pretend that there is none.
239 if event.target == vim.state.largestScrollableElement
240 vim.state.largestScrollableElement = null
241
242 # When the top level window closes we should release all Vims that were
243 # associated with tabs in this window.
244 DOMWindowClose: (event) ->
245 { gBrowser } = event.originalTarget
246 return unless gBrowser
247 for tab in gBrowser.tabs
248 removeVimFromTab(tab, gBrowser)
249
250 TabClose: (event) ->
251 { gBrowser } = utils.getEventRootWindow(event) ? {}
252 return unless gBrowser
253 tab = event.originalTarget
254 removeVimFromTab(tab, gBrowser)
255
256 # Update the toolbar button icon to reflect the blacklisted state.
257 TabSelect: (event) ->
258 return unless window = event.originalTarget?.linkedBrowser?.contentDocument
259 ?.defaultView
260 return unless vim = vimBucket.get(window)
261 updateButton(vim)
262
263
264 # This listener works on individual tabs within Chrome Window.
265 tabsListener =
266 onLocationChange: (browser, webProgress, request, location) ->
267 return unless vim = vimBucket.get(browser.contentWindow)
268
269 vim.resetState()
270
271 # Update the blacklist state.
272 vim.state.blacklisted = utils.isBlacklisted(location.spec)
273 vim.state.blacklistedKeys = utils.getBlacklistedKeys(location.spec)
274 # If only specific keys are blacklisted, remove blacklist state.
275 if vim.state.blacklistedKeys.length > 0
276 vim.state.blacklisted = false
277 updateButton(vim)
278
279 addEventListeners = (window) ->
280 for name, listener of windowsListeners
281 window.addEventListener(name, listener, true)
282
283 window.gBrowser.addTabsProgressListener(tabsListener)
284
285 module.onShutdown(->
286 for name, listener of windowsListeners
287 window.removeEventListener(name, listener, true)
288
289 window.gBrowser.removeTabsProgressListener(tabsListener)
290 )
291
292 exports.addEventListeners = addEventListeners
293 exports.vimBucket = vimBucket
Imprint / Impressum