]> git.gir.st - VimFx.git/blob - extension/lib/events.coffee
fix 'modifiers is null' TypeError
[VimFx.git] / extension / lib / events.coffee
1 # This file sets up all event listeners needed to power VimFx: To know when to
2 # launch commands and to provide state to them. Events in web page content are
3 # listened for in events-frame.coffee.
4
5 button = require('./button')
6 messageManager = require('./message-manager')
7 prefs = require('./prefs')
8 utils = require('./utils')
9
10 HELD_MODIFIERS_ATTRIBUTE = 'vimfx-held-modifiers'
11
12 class UIEventManager
13 constructor: (@vimfx, @window) ->
14 @listen = utils.listen.bind(null, @window)
15 @listenOnce = utils.listenOnce.bind(null, @window)
16
17 # This flag controls whether to suppress the various key events or not.
18 @suppress = false
19
20 # If a matched shortcut has the `<late>` special key, this flag is set to
21 # `true`.
22 @late = false
23
24 # When a menu or panel is shown VimFx should temporarily stop processing
25 # keyboard input, allowing accesskeys to be used.
26 @popupPassthrough = false
27
28 addListeners: ->
29 checkPassthrough = (value, event) =>
30 target = event.originalTarget
31 if target.localName in ['menupopup', 'panel'] and
32 # Don’t set `@popupPassthrough` to `false` if there actually are popups
33 # open. This is the case when a sub-menu closes.
34 (value or not @anyPopupsOpen())
35 @popupPassthrough = value
36
37 @listen('popupshown', checkPassthrough.bind(null, true))
38 @listen('popuphidden', checkPassthrough.bind(null, false))
39
40 @listen('keydown', (event) =>
41 # No matter what, always reset the `@suppress` flag, so we don't
42 # suppress more than intended.
43 @suppress = false
44
45 # Reset the `@late` flag, telling any late listeners for the previous
46 # event not to run. Also reset the `late` pref, telling frame scripts not
47 # to do synchronous message passing on every keydown.
48 @late = false
49 prefs.set('late', false)
50
51 if @popupPassthrough
52 # The `@popupPassthrough` flag is set a bit unreliably. Sometimes it
53 # can be stuck as `true` even though no popup is shown, effectively
54 # disabling the extension. Therefore we check if there actually _are_
55 # any open popups before stopping processing keyboard input. This is
56 # only done when popups (might) be open (not on every keystroke) for
57 # performance reasons.
58 return if @anyPopupsOpen()
59 @popupPassthrough = false # No popup was actually open.
60
61 return unless vim = @vimfx.getCurrentVim(@window)
62
63 @consumeKeyEvent(vim, event)
64 if @suppress
65 utils.suppressEvent(event) # This also suppresses the 'keypress' event.
66 else
67 # If this keydown event wasn’t suppressed, it’s an obvious interaction
68 # with the page. If it _was_ suppressed, though, it’s an interaction
69 # depending on the command triggered; if _it_ calls
70 # `vim.markPageInteraction()` or not.
71 vim.markPageInteraction() if vim.isUIEvent(event)
72 )
73
74 @listen('keyup', (event) =>
75 utils.suppressEvent(event) if @suppress
76 @setHeldModifiers(event, {filterCurrentOnly: true})
77 )
78
79 @listen('focus', => @setFocusType())
80 @listen('blur', =>
81 @window.setTimeout((=>
82 @setFocusType()
83 ), @vimfx.options.blur_timeout)
84 )
85
86 @listen('click', (event) =>
87 target = event.originalTarget
88 return unless vim = @vimfx.getCurrentVim(@window)
89
90 vim.hideNotification()
91
92 # In multi-process, clicks simulated by VimFx cannot be caught here. In
93 # non-multi-process, they unfortunately can. This hack should be
94 # sufficient for that case until non-multi-process is removed from
95 # Firefox.
96 isVimFxGeneratedEvent = (
97 event.layerX == 0 and event.layerY == 0 and
98 event.movementX == 0 and event.movementY == 0
99 )
100
101 # If the user clicks the reload button or a link when in hints mode, we’re
102 # going to end up in hints mode without any markers. Or if the user clicks
103 # a text input, then that input will be focused, but you can’t type in it
104 # (instead markers will be matched). So if the user clicks anything in
105 # hints mode it’s better to leave it.
106 if vim.mode == 'hints' and not isVimFxGeneratedEvent and
107 # Exclude the VimFx button, though, since clicking it returns to normal
108 # mode. Otherwise we’d first return to normal mode and then the button
109 # would open the help dialog.
110 target != button.getButton(@window)
111 vim._enterMode('normal')
112
113 vim._send('clearHover') unless isVimFxGeneratedEvent
114 )
115
116 @listen('overflow', (event) =>
117 target = event.originalTarget
118 return unless vim = @vimfx.getCurrentVim(@window)
119 if vim._isUIElement(target)
120 vim._state.scrollableElements.addChecked(target)
121 )
122
123 @listen('underflow', (event) =>
124 target = event.originalTarget
125 return unless vim = @vimfx.getCurrentVim(@window)
126 if vim._isUIElement(target)
127 vim._state.scrollableElements.deleteChecked(target)
128 )
129
130 @listen('TabSelect', (event) =>
131 target = event.originalTarget
132 target.setAttribute('VimFx-visited', 'true')
133 @vimfx.emit('TabSelect', {event})
134
135 return unless vim = @vimfx.getCurrentVim(@window)
136 vim.hideNotification()
137 @vimfx.emit('focusTypeChange', {vim})
138 )
139
140 @listen('TabClose', (event) =>
141 browser = @window.gBrowser.getBrowserForTab(event.originalTarget)
142 return unless vim = @vimfx.vims.get(browser)
143 # Note: `lastClosedVim` must be stored so that any window can access it.
144 @vimfx.lastClosedVim = vim
145 )
146
147 messageManager.listen('cachedPageshow', ((data, callback, browser) =>
148 [oldVim, @vimfx.lastClosedVim] = [@vimfx.lastClosedVim, null]
149 unless oldVim
150 callback(false)
151 return
152
153 if @vimfx.vims.has(browser)
154 vim = @vimfx.vims.get(browser)
155 if vim._messageManager == vim.browser.messageManager
156 callback(false)
157 return
158
159 # If we get here, it means that we’ve detected a tab dragged from one
160 # window to another. If so, the `vim` object from the last closed tab (the
161 # moved tab) should be re-used. See the commit message for commit bb70257d
162 # for more details.
163 oldVim._setBrowser(browser)
164 @vimfx.vims.set(browser, oldVim)
165 @vimfx.emit('modeChange', {vim: oldVim})
166 callback(true)
167 ), {messageManager: @window.messageManager})
168
169 setFocusType: ->
170 return unless vim = @vimfx.getCurrentVim(@window)
171
172 try
173 # Throws "TypeError: utils is null" when clicking on a doorhanger.
174 activeElement = utils.getActiveElement(@window)
175 catch
176 return
177
178 if activeElement == @window.gBrowser.selectedBrowser
179 vim._send('checkFocusType')
180 return
181
182 focusType = utils.getFocusType(activeElement)
183 vim._setFocusType(focusType)
184
185 if focusType == 'editable' and vim.mode == 'caret'
186 vim._enterMode('normal')
187
188 consumeKeyEvent: (vim, event) ->
189 match = vim._consumeKeyEvent(event)
190
191 if typeof match == 'boolean'
192 @suppress = match
193 return
194
195 if match
196 if match.specialKeys['<late>']
197 @suppress = false
198 @consumeLateKeydown(vim, event, match)
199 else
200 @suppress = vim._onInput(match, event)
201 else
202 @suppress = null
203 @setHeldModifiers(event)
204
205 consumeLateKeydown: (vim, event, match) ->
206 @late = true
207
208 # The passed in `event` is the regular non-late browser UI keydown event.
209 # It is only used to set held keys. This is easier than sending an event
210 # subset from frame scripts.
211 listener = ({defaultPrevented}) =>
212 # `@late` is reset on every keydown. If it is no longer `true`, it means
213 # that the page called `event.stopPropagation()`, which prevented this
214 # listener from running for that event.
215 return unless @late
216 @suppress =
217 if defaultPrevented
218 false
219 else
220 vim._onInput(match, event)
221 @setHeldModifiers(event)
222 return @suppress
223
224 if vim.isUIEvent(event)
225 @listenOnce('keydown', ((lateEvent) =>
226 listener(lateEvent)
227 if @suppress
228 utils.suppressEvent(lateEvent)
229 @listenOnce('keyup', utils.suppressEvent, false)
230 ), false)
231 else
232 # Hack to avoid synchronous messages on every keydown (see
233 # events-frame.coffee).
234 prefs.set('late', true)
235 vim._listenOnce('lateKeydown', listener)
236
237 setHeldModifiers: (event, {filterCurrentOnly = false} = {}) ->
238 mainWindow = @window.document.documentElement
239 modifiers =
240 if filterCurrentOnly
241 mainWindow.getAttribute(HELD_MODIFIERS_ATTRIBUTE) or ''
242 else
243 if @suppress == null then 'alt ctrl meta shift' else ''
244 isHeld = (modifier) -> event["#{modifier}Key"]
245 mainWindow.setAttribute(
246 HELD_MODIFIERS_ATTRIBUTE, modifiers.split(' ').filter(isHeld).join(' ')
247 )
248
249 anyPopupsOpen: ->
250 # The autocomplete popup in text inputs (for example) is technically a
251 # panel, but it does not respond to keypresses. Therefore
252 # `[ignorekeys="true"]` is excluded.
253 #
254 # coffeelint: disable=max_line_length
255 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
256 # coffeelint: enable=max_line_length
257 popups = utils.querySelectorAllDeep(
258 @window, ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
259 )
260 for popup in popups
261 return true if popup.state == 'open'
262 return false
263
264 module.exports = UIEventManager
Imprint / Impressum