]> git.gir.st - VimFx.git/blob - extension/packages/events.coffee
Use a `WeakMap` in `utils.Bucket`
[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
12 vimBucket = new utils.Bucket((window) -> new Vim(window))
13
14 keyStrFromEvent = (event) ->
15 { ctrlKey: ctrl, metaKey: meta, altKey: alt, shiftKey: shift } = event
16
17 if !meta and !alt
18 return unless keyChar = keyUtils.keyCharFromCode(event.keyCode, shift)
19 keyStr = keyUtils.applyModifiers(keyChar, ctrl, alt, meta)
20 return keyStr
21
22 return null
23
24 # When a menu or panel is shown VimFx should temporarily stop processing keyboard input, allowing
25 # accesskeys to be used.
26 popupPassthrough = false
27 checkPassthrough = (event) ->
28 if event.target.nodeName in ['menupopup', 'panel']
29 popupPassthrough = switch event.type
30 when 'popupshown' then true
31 when 'popuphidden' then false
32
33 suppress = false
34 suppressEvent = (event) ->
35 event.preventDefault()
36 event.stopPropagation()
37
38 # This function may be run several times, because:
39 #
40 # - It is assigned to several events. Whichever is fired first should mark
41 # `vim` as loaded.
42 # - The events are fired for each frame of the document. Only the first of those
43 # should mark `vim` as loaded. (This is very noticeable on amazon.com.)
44 #
45 # If we’d updated `vim.lastLoad` each time, too many focus events might be
46 # considered to be autofocus events and thus blurred, even though the user
47 # might have focused something on his own.
48 markVimLoaded = (event) ->
49 target = event.originalTarget
50 return unless target instanceof HTMLDocument
51 return unless vim = getVimFromEvent(event)
52
53 unless vim.loaded
54 vim.loaded = true
55 vim.lastLoad = Date.now()
56
57 markVimUnloaded = (event) ->
58 target = event.originalTarget
59 return unless target instanceof HTMLDocument
60 return unless vim = getVimFromEvent(event)
61
62 # Only mark `vim` as unloaded if the main document is the target, not some
63 # frame inside the main document.
64 if target == vim.window.document
65 vim.loaded = false
66
67
68 # Returns the appropriate vim instance for `event`, but only if it’s okay to do
69 # so. VimFx must not be disabled or blacklisted.
70 getVimFromEvent = (event) ->
71 return if getPref('disabled')
72 return unless window = utils.getEventCurrentTabWindow(event)
73 return unless vim = vimBucket.get(window)
74 return if vim.blacklisted
75
76 return vim
77
78 removeVimFromTab = (tab, gBrowser) ->
79 return unless browser = gBrowser.getBrowserForTab(tab)
80 vimBucket.forget(browser.contentWindow)
81
82 updateButton = (vim) ->
83 updateToolbarButton(vim.rootWindow, {blacklisted: vim.blacklisted, insertMode: vim.mode == 'insert'})
84
85 # The following listeners are installed on every top level Chrome window
86 windowsListeners =
87 keydown: (event) ->
88 try
89 # No matter what, always reset the `suppress` flag, so we don't suppress more than intended.
90 suppress = false
91
92 if popupPassthrough
93 # The `popupPassthrough` flag is set a bit unreliably. Sometimes it can be stuck as `true`
94 # even though no popup is shown, effectively disabling the extension. Therefore we check
95 # if there actually _are_ any open popups before stopping processing keyboard input. This is
96 # only done when popups (might) be open (not on every keystroke) of performance reasons.
97 return unless rootWindow = utils.getEventRootWindow(event)
98 popups = rootWindow.document.querySelectorAll('menupopup, panel')
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 return unless keyStr = keyStrFromEvent(event)
105 suppress = vim.onInput(keyStr, event)
106
107 suppressEvent(event) if suppress
108
109 catch error
110 console.error("#{ error }\n#{ error.stack?.replace(/@.+-> /g, '@') }")
111
112 # Note that the below event listeners can suppress the event even in blacklisted sites. That's
113 # intentional. For example, if you press 'x' to close the current tab, it will close before keyup
114 # fires. So keyup (and perhaps keypress) will fire in another tab. Even if that particular tab is
115 # blacklisted, we must suppress the event, so that 'x' isn't sent to the page. The rule is simple:
116 # If the `suppress` flag is `true`, the event should be suppressed, no matter what. It has the
117 # highest priority.
118 keypress: (event) -> suppressEvent(event) if suppress
119 keyup: (event) -> suppressEvent(event) if suppress
120
121 popupshown: checkPassthrough
122 popuphidden: checkPassthrough
123
124 # `DOMContentLoaded` does not fire when going back and forward in the history
125 # (nor does the `load` event), but `pageshow` does. Therefore we assign
126 # `markVimLoaded` to both events. The reason we do not simply only use
127 # `pageshow` is that it fires later than `DOMContentLoaded`: just after the
128 # `load` event, which is a a bit too late, causing too many focus events to
129 # be considered as autofocus events and thus blurred. But it fires quick
130 # enough when going back and forward in the history, since then an in-memory
131 # cache is used.
132 DOMContentLoaded: markVimLoaded
133 pageshow: markVimLoaded
134
135 # It is tempting to mark a `vim` instance as unloaded in the
136 # `onLocationChange` event. However, if `history.pushState()` is used,
137 # `onLocationChange` will fire, but load events such as `DOMContentLoaded`
138 # and `pageshow` won’t. That means that the `vim` instance won’t be marked as
139 # loaded again, which will cause _all_ focus events to be considered as
140 # autofocus events, making it impossible to focus inputs.
141 #
142 # Therefore we use the `pagehide` event instead. It fires when the user
143 # navigates away from the page (clicks a link, goes back and forward in the
144 # history, enters something in the location bar etc.), but not when
145 # `history.pushState()` is used.
146 #
147 # However, we still want to disable autofocus after a `history.pushState()`
148 # call. Therefore we set `vim.lastLoad` in the `onLocationChange` event, so
149 # that all focus events within one second after that get blurred.
150 # `history.pushState()` is usually used together with a quick AJAX call, so
151 # that second should be enough (as opposed to a full page request where
152 # several seconds may pass between the location change and the actual page
153 # load).
154 pagehide: markVimUnloaded
155
156 focus: (event) ->
157 return unless getPref('prevent_autofocus')
158
159 target = event.originalTarget
160 return unless target.ownerDocument instanceof HTMLDocument
161
162 # We only prevent autofocus from editable elements, that is, elements that
163 # can “steal” the keystrokes, in order not to interfere too much.
164 return unless utils.isElementEditable(target)
165
166 return unless vim = getVimFromEvent(event)
167
168 # Focus events can occur before DOMContentLoaded, both when the `autofocus`
169 # attribute is used, and when a script contains `element.focus()`. So if
170 # the `vim` instance isn’t marked as loaded, all focus events should be
171 # blurred. Autofocus events can occur later, too. How much later? One
172 # second seems to be a good compromise.
173 if !vim.loaded or Date.now() - vim.lastLoad < 1000
174 target.blur()
175
176 # When the top level window closes we should release all Vims that were
177 # associated with tabs in this window
178 DOMWindowClose: (event) ->
179 { gBrowser } = event.originalTarget
180 return unless gBrowser
181 for tab in gBrowser.tabs
182 removeVimFromTab(tab, gBrowser)
183
184 TabClose: (event) ->
185 { gBrowser } = utils.getEventRootWindow(event) ? {}
186 return unless gBrowser
187 tab = event.originalTarget
188 removeVimFromTab(tab, gBrowser)
189
190 # Update the toolbar button icon to reflect the blacklisted state
191 TabSelect: (event) ->
192 return unless window = event.originalTarget?.linkedBrowser?.contentDocument?.defaultView
193 return unless vim = vimBucket.get(window)
194 updateButton(vim)
195
196
197 # This listener works on individual tabs within Chrome Window
198 tabsListener =
199 onLocationChange: (browser, webProgress, request, location) ->
200 return unless vim = vimBucket.get(browser.contentWindow)
201
202 vim.lastLoad = Date.now() # See the `pagehide` event.
203
204 # If the location changes when in hints mode (for example because the reload button has been
205 # clicked), we're going to end up in hints mode without any markers. So switch back to normal
206 # mode in that case.
207 if vim.mode == 'hints'
208 vim.enterMode('normal')
209
210 # Update the blacklist state.
211 vim.blacklisted = utils.isBlacklisted(location.spec)
212 updateButton(vim)
213
214 addEventListeners = (window) ->
215 for name, listener of windowsListeners
216 window.addEventListener(name, listener, true)
217
218 window.gBrowser.addTabsProgressListener(tabsListener)
219
220 unload ->
221 for name, listener of windowsListeners
222 window.removeEventListener(name, listener, true)
223
224 window.gBrowser.removeTabsProgressListener(tabsListener)
225
226 exports.addEventListeners = addEventListeners
227 exports.vimBucket = vimBucket
Imprint / Impressum