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