]> git.gir.st - VimFx.git/blob - extension/lib/events.coffee
Clear held modifiers when pressing a non-modifier
[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 button = require('./button')
22 utils = require('./utils')
23
24 { interfaces: Ci } = Components
25
26 HTMLDocument = Ci.nsIDOMHTMLDocument
27 HTMLInputElement = Ci.nsIDOMHTMLInputElement
28
29 # Will be set by `addEventListeners`. It’s a bit hacky, but will do for now.
30 vimfx = null
31 vimBucket = null
32
33 # When a menu or panel is shown VimFx should temporarily stop processing
34 # keyboard input, allowing accesskeys to be used.
35 popupPassthrough = false
36 checkPassthrough = (event) ->
37 if event.target.nodeName in ['menupopup', 'panel']
38 popupPassthrough = switch event.type
39 when 'popupshown' then true
40 when 'popuphidden' then false
41
42 suppress = false
43 suppressEvent = (event) ->
44 event.preventDefault()
45 event.stopPropagation()
46
47 # Returns the appropriate vim instance for `event`.
48 getVimFromEvent = (event) ->
49 return unless window = utils.getEventCurrentTabWindow(event)
50 return unless vim = vimBucket.get(window)
51 return vim
52
53 # Save the time of the last user interaction. This is used to determine whether
54 # a focus event was automatic or voluntarily dispatched.
55 markLastInteraction = (event, vim = null) ->
56 return unless vim ?= getVimFromEvent(event)
57 return unless event.originalTarget.ownerDocument instanceof HTMLDocument
58 vim.state.lastInteraction = Date.now()
59
60 removeVimFromTab = (tab, gBrowser) ->
61 return unless browser = gBrowser.getBrowserForTab(tab)
62 vimBucket.forget(browser.contentWindow)
63
64 attribute = 'vimfx-held-modifiers'
65 setHeldModifiers = (vim, event, modifiers = null) ->
66 mainWindow = vim.rootWindow.document.documentElement
67 modifiers ?= mainWindow.getAttribute(attribute)
68 isHeld = (modifier) -> event["#{ modifier }Key"]
69 mainWindow.setAttribute(attribute,
70 modifiers.split(' ').filter(isHeld).join(' '))
71
72 # The following listeners are installed on every top level Chrome window.
73 windowsListeners =
74 keydown: (event) ->
75 try
76 # No matter what, always reset the `suppress` flag, so we don't suppress
77 # more than intended.
78 suppress = false
79
80 if popupPassthrough
81 # The `popupPassthrough` flag is set a bit unreliably. Sometimes it can
82 # be stuck as `true` even though no popup is shown, effectively
83 # disabling the extension. Therefore we check if there actually _are_
84 # any open popups before stopping processing keyboard input. This is
85 # only done when popups (might) be open (not on every keystroke) of
86 # performance reasons.
87 #
88 # The autocomplete popup in text inputs (for example) is technically a
89 # panel, but it does not respond to key presses. Therefore
90 # `[ignorekeys="true"]` is excluded.
91 #
92 # coffeelint: disable=max_line_length
93 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
94 # coffeelint: enable=max_line_length
95 return unless rootWindow = utils.getEventRootWindow(event)
96 popups = rootWindow.document.querySelectorAll(
97 ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
98 )
99 for popup in popups
100 return if popup.state == 'open'
101 popupPassthrough = false # No popup was actually open: Reset the flag.
102
103 return unless vim = getVimFromEvent(event)
104
105 markLastInteraction(event, vim)
106
107 suppress = vim.onInput(event)
108
109 modifiers = if suppress == null then 'alt ctrl meta shift' else ''
110 setHeldModifiers(vim, event, modifiers)
111
112 suppressEvent(event) if suppress
113
114 catch error
115 console.error(utils.formatError(error))
116
117 keypress: (event) -> suppressEvent(event) if suppress
118
119 keyup: (event) ->
120 suppressEvent(event) if suppress
121 return unless vim = getVimFromEvent(event)
122 setHeldModifiers(vim, event)
123
124 popupshown: checkPassthrough
125 popuphidden: checkPassthrough
126
127 focus: (event) ->
128 target = event.originalTarget
129 return unless vim = getVimFromEvent(event)
130
131 findBar = vim.rootWindow.gBrowser.getFindBar()
132 if target == findBar._findField.mInputField
133 vim.enterMode('find')
134 return
135
136 if target.ownerDocument instanceof HTMLDocument and
137 utils.isTextInputElement(target)
138 vim.state.lastFocusedTextInput = target
139
140 # If the user has interacted with the page and the `window` of the page gets
141 # focus, it means that the user just switched back to the page from another
142 # window or tab. If a text input was focused when the user focused _away_
143 # from the page Firefox blurs it and then re-focuses it when the user
144 # switches back. Therefore we count this case as an interaction, so the
145 # re-focus event isn’t caught as autofocus.
146 if vim.state.lastInteraction != null and target == vim.window
147 vim.state.lastInteraction = Date.now()
148
149 # Autofocus prevention. Strictly speaking, autofocus may only happen during
150 # page load, which means that we should only prevent focus events during
151 # page load. However, it is very difficult to reliably determine when the
152 # page load ends. Moreover, a page may load very slowly. Then it is likely
153 # that the user tries to focus something before the page has loaded fully.
154 # Therefore focus events that aren’t reasonably close to a user interaction
155 # (click or key press) are blurred (regardless of whether the page is loaded
156 # or not -- but that isn’t so bad: if the user doesn’t like autofocus, he
157 # doesn’t like any automatic focusing, right? This is actually useful on
158 # devdocs.io). There is a slight risk that the user presses a key just
159 # before an autofocus, causing it not to be blurred, but that’s not likely.
160 # Autofocus prevention is also restricted to `<input>` elements, since only
161 # such elements are commonly autofocused. Many sites have buttons which
162 # inserts a `<textarea>` when clicked (which might take up to a second) and
163 # then focuses the `<textarea>`. Such focus events should _not_ be blurred.
164 # There are also many buttons that do the same thing but insert an `<input>`
165 # element. There is sadly always a risk that those events are blurred.
166 focusManager = Cc['@mozilla.org/focus-manager;1']
167 .getService(Ci.nsIFocusManager)
168 if vimfx.options.prevent_autofocus and
169 vim.mode in vimfx.options.prevent_autofocus_modes and
170 target.ownerDocument instanceof HTMLDocument and
171 target instanceof HTMLInputElement and
172 # Only blur programmatic events (not caused by clicks or keypresses).
173 focusManager.getLastFocusMethod(null) == 0 and
174 (vim.state.lastInteraction == null or
175 Date.now() - vim.state.lastInteraction > vimfx.options.autofocus_limit)
176 vim.state.lastAutofocusPrevention = Date.now()
177 target.blur()
178
179 blur: (event) ->
180 target = event.originalTarget
181 return unless vim = getVimFromEvent(event)
182
183 findBar = vim.rootWindow.gBrowser.getFindBar()
184 if target == findBar._findField.mInputField
185 vim.enterMode('normal')
186 return
187
188 # Some sites (such as icloud.com) re-focuses inputs if they are blurred,
189 # causing an infinite loop autofocus prevention and re-focusing. Therefore
190 # we suppress blur events that happen just after an autofocus prevention.
191 if vim.state.lastAutofocusPrevention != null and
192 Date.now() - vim.state.lastAutofocusPrevention < 1
193 vim.state.lastAutofocusPrevention = null
194 suppressEvent(event)
195 return
196
197 click: (event) ->
198 target = event.originalTarget
199 return unless vim = getVimFromEvent(event)
200
201 # If the user clicks the reload button or a link when in hints mode, we’re
202 # going to end up in hints mode without any markers. Or if the user clicks a
203 # text input, then that input will be focused, but you can’t type in it
204 # (instead markers will be matched). So if the user clicks anything in hints
205 # mode it’s better to leave it.
206 if vim.mode == 'hints' and not utils.isEventSimulated(event) and
207 # Exclude the VimFx button, though, since clicking it returns to normal
208 # mode. Otherwise we’d first returned to normal mode and then the button
209 # would have opened the help dialog.
210 target != vim.rootWindow.getElementById(button.BUTTON_ID)
211 vim.enterMode('normal')
212 return
213
214 mousedown: markLastInteraction
215 mouseup: markLastInteraction
216
217 overflow: (event) ->
218 return unless vim = getVimFromEvent(event)
219 return unless computedStyle = vim.window.getComputedStyle(event.target)
220 return if computedStyle.getPropertyValue('overflow') == 'hidden'
221 vim.state.scrollableElements.set(event.target)
222
223 underflow: (event) ->
224 return unless vim = getVimFromEvent(event)
225 vim.state.scrollableElements.delete(event.target)
226
227 # When the top level window closes we should release all Vims that were
228 # associated with tabs in this window.
229 DOMWindowClose: (event) ->
230 { gBrowser } = event.originalTarget
231 return unless gBrowser
232 for tab in gBrowser.tabs
233 removeVimFromTab(tab, gBrowser)
234
235 TabClose: (event) ->
236 { gBrowser } = utils.getEventRootWindow(event) ? {}
237 return unless gBrowser
238 tab = event.originalTarget
239 removeVimFromTab(tab, gBrowser)
240
241 TabSelect: (event) ->
242 return unless window = event.originalTarget?.linkedBrowser?.contentDocument
243 ?.defaultView
244 # Get the current vim in order to trigger an update event for the toolbar
245 # button.
246 vimBucket.get(window)
247
248
249 # This listener works on individual tabs within Chrome Window.
250 tabsListener =
251 onLocationChange: (browser, webProgress, request, location) ->
252 return unless vim = vimBucket.get(browser.contentWindow)
253 vim.resetState()
254
255 addEventListeners = (_vimfx, window) ->
256 vimfx = _vimfx
257 { vimBucket } = _vimfx
258 for name, listener of windowsListeners
259 window.addEventListener(name, listener, true)
260
261 window.gBrowser.addTabsProgressListener(tabsListener)
262
263 module.onShutdown(->
264 for name, listener of windowsListeners
265 window.removeEventListener(name, listener, true)
266
267 window.gBrowser.removeTabsProgressListener(tabsListener)
268 )
269
270 module.exports = {
271 addEventListeners
272 }
Imprint / Impressum