]> git.gir.st - VimFx.git/blob - extension/packages/events.coffee
Only allow focus events immediately after an interaction
[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 # Autofocus prevention. Strictly speaking, autofocus may only happen during
121 # page load, which means that we should only prevent focus events during
122 # page load. However, it is very difficult to reliably determine when the
123 # page load ends. Moreover, a page may load very slowly. Then it is likely
124 # that the user tries to focus something before the page has loaded fully.
125 # Therefore focus events that aren’t reasonably close to a user interaction
126 # (click or key press) are blurred (regardless of whether the page is
127 # loaded or not -- but that isn’t so bad: if the user doesn’t like
128 # autofocus, he doesn’t like any automatic focusing, right? This is
129 # actually useful on devdocs.io). There is a slight risk that the user
130 # presses a key just before an autofocus, causing it not to be blurred, but
131 # that’s not likely. Lastly, the autofocus prevention is restricted to
132 # `<input>` elements, since only such elements are commonly autofocused.
133 # Many sites have buttons which inserts a `<textarea>` when clicked (which
134 # might take up to a second) and then focuses the `<textarea>`. Such focus
135 # events should _not_ be blurred.
136 if getPref('prevent_autofocus') and
137 target.ownerDocument instanceof HTMLDocument and
138 target instanceof HTMLInputElement and
139 (vim.lastInteraction == null or Date.now() - vim.lastInteraction > 100)
140 target.blur()
141
142 blur: (event) ->
143 target = event.originalTarget
144 return unless vim = getVimFromEvent(event)
145
146 findBar = vim.rootWindow.gBrowser.getFindBar()
147 if target == findBar._findField.mInputField
148 vim.enterMode('normal')
149 return
150
151 click: (event) ->
152 target = event.originalTarget
153 return unless vim = getVimFromEvent(event)
154
155 # If the user clicks the reload button or a link when in hints mode, we’re
156 # going to end up in hints mode without any markers. Or if the user clicks
157 # a text input, then that input will be focused, but you can’t type in it
158 # (instead markers will be matched). So if the user clicks anything in
159 # hints mode it’s better to leave it.
160 if vim.mode == 'hints' and not utils.isEventSimulated(event)
161 vim.enterMode('normal')
162 return
163
164 mousedown: markLastInteraction
165 mouseup: markLastInteraction
166
167 # When the top level window closes we should release all Vims that were
168 # associated with tabs in this window
169 DOMWindowClose: (event) ->
170 { gBrowser } = event.originalTarget
171 return unless gBrowser
172 for tab in gBrowser.tabs
173 removeVimFromTab(tab, gBrowser)
174
175 TabClose: (event) ->
176 { gBrowser } = utils.getEventRootWindow(event) ? {}
177 return unless gBrowser
178 tab = event.originalTarget
179 removeVimFromTab(tab, gBrowser)
180
181 # Update the toolbar button icon to reflect the blacklisted state
182 TabSelect: (event) ->
183 return unless window = event.originalTarget?.linkedBrowser?.contentDocument?.defaultView
184 return unless vim = vimBucket.get(window)
185 updateButton(vim)
186
187
188 # This listener works on individual tabs within Chrome Window
189 tabsListener =
190 onLocationChange: (browser, webProgress, request, location) ->
191 return unless vim = vimBucket.get(browser.contentWindow)
192
193 # There hasn’t been any interaction on the page yet, so reset it.
194 vim.lastInteraction = null
195
196 # Update the blacklist state.
197 vim.blacklisted = utils.isBlacklisted(location.spec)
198 updateButton(vim)
199
200 addEventListeners = (window) ->
201 for name, listener of windowsListeners
202 window.addEventListener(name, listener, true)
203
204 window.gBrowser.addTabsProgressListener(tabsListener)
205
206 unload ->
207 for name, listener of windowsListeners
208 window.removeEventListener(name, listener, true)
209
210 window.gBrowser.removeTabsProgressListener(tabsListener)
211
212 exports.addEventListeners = addEventListeners
213 exports.vimBucket = vimBucket
Imprint / Impressum