]> git.gir.st - VimFx.git/blob - extension/lib/events.coffee
Improve error reporting in test runner
[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 { getPref } = require('./prefs')
24
25 { interfaces: Ci } = Components
26
27 HTMLDocument = Ci.nsIDOMHTMLDocument
28 HTMLInputElement = Ci.nsIDOMHTMLInputElement
29
30 # Will be set by `addEventListeners`. It’s a bit hacky, but will do for now.
31 vimBucket = null
32
33 keyStrFromEvent = (event) ->
34 return notation.stringify(event, {
35 # Check that `event.code` really is available before using the
36 # 'ignore_keyboard_layout' option. If not `event.key` will be used anyway,
37 # and the user needs to enable `event.code` support (which can be done from
38 # VimFx’s settings page).
39 ignoreKeyboardLayout: getPref('ignore_keyboard_layout') and event.code?
40 # Advanced setting for advanced users. Currently requires you to modifiy it
41 # from about:config and check the error console for errors.
42 translations: JSON.parse(getPref('translations'))
43 })
44
45 # When a menu or panel is shown VimFx should temporarily stop processing
46 # keyboard input, allowing accesskeys to be used.
47 popupPassthrough = false
48 checkPassthrough = (event) ->
49 if event.target.nodeName in ['menupopup', 'panel']
50 popupPassthrough = switch event.type
51 when 'popupshown' then true
52 when 'popuphidden' then false
53
54 suppress = false
55 suppressEvent = (event) ->
56 event.preventDefault()
57 event.stopPropagation()
58
59 # Returns the appropriate vim instance for `event`, but only if it’s okay to do
60 # so. VimFx must not be disabled or blacklisted.
61 getVimFromEvent = (event) ->
62 return if getPref('disabled')
63 return unless window = utils.getEventCurrentTabWindow(event)
64 return unless vim = vimBucket.get(window)
65 return if vim.state.blacklisted
66
67 return vim
68
69 # Save the time of the last user interaction. This is used to determine whether
70 # a focus event was automatic or voluntarily dispatched.
71 markLastInteraction = (event, vim = null) ->
72 return unless vim ?= getVimFromEvent(event)
73 return unless event.originalTarget.ownerDocument instanceof HTMLDocument
74 vim.state.lastInteraction = Date.now()
75
76 removeVimFromTab = (tab, gBrowser) ->
77 return unless browser = gBrowser.getBrowserForTab(tab)
78 vimBucket.forget(browser.contentWindow)
79
80 # The following listeners are installed on every top level Chrome window.
81 windowsListeners =
82 keydown: (event) ->
83 try
84 # No matter what, always reset the `suppress` flag, so we don't suppress
85 # more than intended.
86 suppress = false
87
88 if popupPassthrough
89 # The `popupPassthrough` flag is set a bit unreliably. Sometimes it can
90 # be stuck as `true` even though no popup is shown, effectively
91 # disabling the extension. Therefore we check if there actually _are_
92 # any open popups before stopping processing keyboard input. This is
93 # only done when popups (might) be open (not on every keystroke) of
94 # performance reasons.
95 #
96 # The autocomplete popup in text inputs (for example) is technically a
97 # panel, but it does not respond to key presses. Therefore
98 # `[ignorekeys="true"]` is excluded.
99 #
100 # coffeelint: disable=max_line_length
101 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
102 # coffeelint: enable=max_line_length
103 return unless rootWindow = utils.getEventRootWindow(event)
104 popups = rootWindow.document.querySelectorAll(
105 ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
106 )
107 for popup in popups
108 return if popup.state == 'open'
109 popupPassthrough = false # No popup was actually open: Reset the flag.
110
111 return unless vim = getVimFromEvent(event)
112
113 markLastInteraction(event, vim)
114
115 return unless keyStr = keyStrFromEvent(event)
116 suppress = vim.onInput(keyStr, event)
117
118 suppressEvent(event) if suppress
119
120 catch error
121 console.error(utils.formatError(error))
122
123 # Note that the below event listeners can suppress the event even in
124 # blacklisted sites. That's intentional. For example, if you press 'x' to
125 # close the current tab, it will close before keyup fires. So keyup (and
126 # perhaps keypress) will fire in another tab. Even if that particular tab is
127 # blacklisted, we must suppress the event, so that 'x' isn't sent to the page.
128 # The rule is simple: If the `suppress` flag is `true`, the event should be
129 # suppressed, no matter what. It has the highest priority.
130 keypress: (event) -> suppressEvent(event) if suppress
131 keyup: (event) -> suppressEvent(event) if suppress
132
133 popupshown: checkPassthrough
134 popuphidden: checkPassthrough
135
136 focus: (event) ->
137 target = event.originalTarget
138 return unless vim = getVimFromEvent(event)
139
140 findBar = vim.rootWindow.gBrowser.getFindBar()
141 if target == findBar._findField.mInputField
142 vim.enterMode('find')
143 return
144
145 if target.ownerDocument instanceof HTMLDocument and
146 utils.isTextInputElement(target)
147 vim.state.lastFocusedTextInput = target
148
149 # If the user has interacted with the page and the `window` of the page gets
150 # focus, it means that the user just switched back to the page from another
151 # window or tab. If a text input was focused when the user focused _away_
152 # from the page Firefox blurs it and then re-focuses it when the user
153 # switches back. Therefore we count this case as an interaction, so the
154 # re-focus event isn’t caught as autofocus.
155 if vim.state.lastInteraction != null and target == vim.window
156 vim.state.lastInteraction = Date.now()
157
158 # Autofocus prevention. Strictly speaking, autofocus may only happen during
159 # page load, which means that we should only prevent focus events during
160 # page load. However, it is very difficult to reliably determine when the
161 # page load ends. Moreover, a page may load very slowly. Then it is likely
162 # that the user tries to focus something before the page has loaded fully.
163 # Therefore focus events that aren’t reasonably close to a user interaction
164 # (click or key press) are blurred (regardless of whether the page is loaded
165 # or not -- but that isn’t so bad: if the user doesn’t like autofocus, he
166 # doesn’t like any automatic focusing, right? This is actually useful on
167 # devdocs.io). There is a slight risk that the user presses a key just
168 # before an autofocus, causing it not to be blurred, but that’s not likely.
169 # Lastly, the autofocus prevention is restricted to `<input>` elements,
170 # since only such elements are commonly autofocused. Many sites have
171 # buttons which inserts a `<textarea>` when clicked (which might take up to
172 # a second) and then focuses the `<textarea>`. Such focus events should
173 # _not_ be blurred.
174 if getPref('prevent_autofocus') and
175 vim.mode != 'insert' and
176 target.ownerDocument instanceof HTMLDocument and
177 target instanceof HTMLInputElement and
178 (vim.state.lastInteraction == null or
179 Date.now() - vim.state.lastInteraction > getPref('autofocus_limit'))
180 vim.state.lastAutofocusPrevention = Date.now()
181 target.blur()
182
183 blur: (event) ->
184 target = event.originalTarget
185 return unless vim = getVimFromEvent(event)
186
187 findBar = vim.rootWindow.gBrowser.getFindBar()
188 if target == findBar._findField.mInputField
189 vim.enterMode('normal')
190 return
191
192 # Some sites (such as icloud.com) re-focuses inputs if they are blurred,
193 # causing an infinite loop autofocus prevention and re-focusing. Therefore
194 # we suppress blur events that happen just after an autofocus prevention.
195 if vim.state.lastAutofocusPrevention != null and
196 Date.now() - vim.state.lastAutofocusPrevention < 1
197 vim.state.lastAutofocusPrevention = null
198 suppressEvent(event)
199 return
200
201 click: (event) ->
202 target = event.originalTarget
203 return unless vim = getVimFromEvent(event)
204
205 # If the user clicks the reload button or a link when in hints mode, we’re
206 # going to end up in hints mode without any markers. Or if the user clicks a
207 # text input, then that input will be focused, but you can’t type in it
208 # (instead markers will be matched). So if the user clicks anything in hints
209 # mode it’s better to leave it.
210 if vim.mode == 'hints' and not utils.isEventSimulated(event)
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
254 vim.resetState()
255
256 # Update the blacklist state.
257 vim.state.blacklisted = utils.isBlacklisted(location.spec)
258 vim.state.blacklistedKeys = utils.getBlacklistedKeys(location.spec)
259 # If only specific keys are blacklisted, remove blacklist state.
260 if vim.state.blacklistedKeys.length > 0
261 vim.state.blacklisted = false
262
263 addEventListeners = (vimfx, window) ->
264 { vimBucket } = vimfx
265 for name, listener of windowsListeners
266 window.addEventListener(name, listener, true)
267
268 window.gBrowser.addTabsProgressListener(tabsListener)
269
270 module.onShutdown(->
271 for name, listener of windowsListeners
272 window.removeEventListener(name, listener, true)
273
274 window.gBrowser.removeTabsProgressListener(tabsListener)
275 )
276
277 exports.addEventListeners = addEventListeners
Imprint / Impressum