]> git.gir.st - VimFx.git/blob - extension/packages/events.coffee
Merge branch 'develop' into better-hintmarkers
[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 { unloader } = require 'unloader'
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
26 # keyboard input, allowing 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, {
62 blacklisted: vim.blacklisted
63 insertMode: vim.mode == 'insert'
64 })
65
66 # The following listeners are installed on every top level Chrome window.
67 windowsListeners =
68 keydown: (event) ->
69 try
70 # No matter what, always reset the `suppress` flag, so we don't suppress
71 # more than intended.
72 suppress = false
73
74 if popupPassthrough
75 # The `popupPassthrough` flag is set a bit unreliably. Sometimes it can
76 # be stuck as `true` even though no popup is shown, effectively
77 # disabling the extension. Therefore we check if there actually _are_
78 # any open popups before stopping processing keyboard input. This is
79 # only done when popups (might) be open (not on every keystroke) of
80 # performance reasons.
81 #
82 # The autocomplete popup in text inputs (for example) is technically a
83 # panel, but it does not respond to key presses. Therefore
84 # `[ignorekeys="true"]` is excluded.
85 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
86 return unless rootWindow = utils.getEventRootWindow(event)
87 popups = rootWindow.document.querySelectorAll(
88 ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
89 )
90 for popup in popups
91 return if popup.state == 'open'
92 popupPassthrough = false # No popup was actually open: Reset the flag.
93
94 return unless vim = getVimFromEvent(event)
95
96 markLastInteraction(event, vim)
97
98 return unless keyStr = keyStrFromEvent(event)
99 suppress = vim.onInput(keyStr, event)
100
101 suppressEvent(event) if suppress
102
103 catch error
104 console.error("#{ error }\n#{ error.stack?.replace(/@.+-> /g, '@') }")
105
106 # Note that the below event listeners can suppress the event even in
107 # blacklisted sites. That's intentional. For example, if you press 'x' to
108 # close the current tab, it will close before keyup fires. So keyup (and
109 # perhaps keypress) will fire in another tab. Even if that particular tab is
110 # blacklisted, we must suppress the event, so that 'x' isn't sent to the page.
111 # The rule is simple: If the `suppress` flag is `true`, the event should be
112 # suppressed, no matter what. It has the highest priority.
113 keypress: (event) -> suppressEvent(event) if suppress
114 keyup: (event) -> suppressEvent(event) if suppress
115
116 popupshown: checkPassthrough
117 popuphidden: checkPassthrough
118
119 focus: (event) ->
120 target = event.originalTarget
121 return unless vim = getVimFromEvent(event)
122
123 findBar = vim.rootWindow.gBrowser.getFindBar()
124 if target == findBar._findField.mInputField
125 vim.enterMode('find')
126 return
127
128 # If the user has interacted with the page and the `window` of the page gets
129 # focus, it means that the user just switched back to the page from another
130 # window or tab. If a text input was focused when the user focused _away_
131 # from the page Firefox blurs it and then re-focuses it when the user
132 # switches back. Therefore we count this case as an interaction, so the
133 # re-focus event isn’t caught as autofocus.
134 if vim.lastInteraction != null and target == vim.window
135 vim.lastInteraction = Date.now()
136
137 # Autofocus prevention. Strictly speaking, autofocus may only happen during
138 # page load, which means that we should only prevent focus events during
139 # page load. However, it is very difficult to reliably determine when the
140 # page load ends. Moreover, a page may load very slowly. Then it is likely
141 # that the user tries to focus something before the page has loaded fully.
142 # Therefore focus events that aren’t reasonably close to a user interaction
143 # (click or key press) are blurred (regardless of whether the page is loaded
144 # or not -- but that isn’t so bad: if the user doesn’t like autofocus, he
145 # doesn’t like any automatic focusing, right? This is actually useful on
146 # devdocs.io). There is a slight risk that the user presses a key just
147 # before an autofocus, causing it not to be blurred, but that’s not likely.
148 # Lastly, the autofocus prevention is restricted to `<input>` elements,
149 # since only such elements are commonly autofocused. Many sites have
150 # buttons which inserts a `<textarea>` when clicked (which might take up to
151 # a second) and then focuses the `<textarea>`. Such focus events should
152 # _not_ be blurred.
153 if getPref('prevent_autofocus') and
154 target.ownerDocument instanceof HTMLDocument and
155 target instanceof HTMLInputElement and
156 (vim.lastInteraction == null or Date.now() - vim.lastInteraction > 100)
157 target.blur()
158
159 blur: (event) ->
160 target = event.originalTarget
161 return unless vim = getVimFromEvent(event)
162
163 findBar = vim.rootWindow.gBrowser.getFindBar()
164 if target == findBar._findField.mInputField
165 vim.enterMode('normal')
166 return
167
168 click: (event) ->
169 target = event.originalTarget
170 return unless vim = getVimFromEvent(event)
171
172 # If the user clicks the reload button or a link when in hints mode, we’re
173 # going to end up in hints mode without any markers. Or if the user clicks a
174 # text input, then that input will be focused, but you can’t type in it
175 # (instead markers will be matched). So if the user clicks anything in hints
176 # mode it’s better to leave it.
177 if vim.mode == 'hints' and not utils.isEventSimulated(event)
178 vim.enterMode('normal')
179 return
180
181 mousedown: markLastInteraction
182 mouseup: markLastInteraction
183
184 # When the top level window closes we should release all Vims that were
185 # associated with tabs in this window.
186 DOMWindowClose: (event) ->
187 { gBrowser } = event.originalTarget
188 return unless gBrowser
189 for tab in gBrowser.tabs
190 removeVimFromTab(tab, gBrowser)
191
192 TabClose: (event) ->
193 { gBrowser } = utils.getEventRootWindow(event) ? {}
194 return unless gBrowser
195 tab = event.originalTarget
196 removeVimFromTab(tab, gBrowser)
197
198 # Update the toolbar button icon to reflect the blacklisted state.
199 TabSelect: (event) ->
200 return unless window = event.originalTarget?.linkedBrowser?.contentDocument?.defaultView
201 return unless vim = vimBucket.get(window)
202 updateButton(vim)
203
204
205 # This listener works on individual tabs within Chrome Window.
206 tabsListener =
207 onLocationChange: (browser, webProgress, request, location) ->
208 return unless vim = vimBucket.get(browser.contentWindow)
209
210 # There hasn’t been any interaction on the page yet, so reset it.
211 vim.lastInteraction = null
212
213 # Update the blacklist state.
214 vim.blacklisted = utils.isBlacklisted(location.spec)
215 updateButton(vim)
216
217 addEventListeners = (window) ->
218 for name, listener of windowsListeners
219 window.addEventListener(name, listener, true)
220
221 window.gBrowser.addTabsProgressListener(tabsListener)
222
223 unloader.add(->
224 for name, listener of windowsListeners
225 window.removeEventListener(name, listener, true)
226
227 window.gBrowser.removeTabsProgressListener(tabsListener)
228 )
229
230 exports.addEventListeners = addEventListeners
231 exports.vimBucket = vimBucket
Imprint / Impressum