]> git.gir.st - VimFx.git/blob - extension/lib/events-frame.coffee
Avoid sending a synchronous message on every keydown
[VimFx.git] / extension / lib / events-frame.coffee
1 ###
2 # Copyright Simon Lydell 2015, 2016.
3 #
4 # This file is part of VimFx.
5 #
6 # VimFx is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # VimFx is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
18 ###
19
20 # This file is the equivalent to events.coffee, but for frame scripts.
21
22 notation = require('vim-like-key-notation')
23 commands = require('./commands-frame')
24 messageManager = require('./message-manager')
25 prefs = require('./prefs')
26 utils = require('./utils')
27
28 nsIFocusManager = Cc['@mozilla.org/focus-manager;1']
29 .getService(Ci.nsIFocusManager)
30
31 XULDocument = Ci.nsIDOMXULDocument
32
33 class FrameEventManager
34 constructor: (@vim) ->
35 @numFocusToSuppress = 0
36 @keepInputs = false
37 @currentUrl = false
38
39 listen: utils.listen.bind(null, FRAME_SCRIPT_ENVIRONMENT)
40 listenOnce: utils.listenOnce.bind(null, FRAME_SCRIPT_ENVIRONMENT)
41
42 addListeners: ->
43 # If the page already was loaded when VimFx was initialized, send the
44 # 'frameCanReceiveEvents' message straight away.
45 if @vim.content.document.readyState == 'complete'
46 messageManager.send('frameCanReceiveEvents', true)
47
48 @listen('readystatechange', (event) =>
49 target = event.originalTarget
50 topDocument = @vim.content.document
51 [oldUrl, @currentUrl] = [@currentUrl, @vim.content.location.href]
52
53 switch target.readyState
54 when 'interactive'
55 if target == topDocument or
56 # When loading the editor on codepen.io, a frame gets
57 # 'readystatechange' → 'interactive' quite a bit before the
58 # toplevel document does. Checking for this case lets us send
59 # 'locationChange' earlier, allowing to enter Ignore mode earlier,
60 # for example. Be careful not to trigger a 'locationChange' for
61 # frames loading _after_ the toplevel document, though.
62 (topDocument.readyState == 'loading' and oldUrl == null)
63 messageManager.send('locationChange', @currentUrl)
64
65 when 'complete'
66 if target == topDocument
67 messageManager.send('frameCanReceiveEvents', true)
68 )
69
70 @listen('pageshow', (event) =>
71 [oldUrl, @currentUrl] = [@currentUrl, @vim.content.location.href]
72
73 # When navigating the history, `event.persisted` is `true` (meaning that
74 # the page loaded from cache) and 'readystatechange' won’t be fired, so
75 # send a 'locationChange' message to make sure that the blacklist is
76 # applied etc. The reason we don’t simply _always_ do this on the
77 # 'pageshow' event, is because it usually fires too late. However, it also
78 # fires after having moved a tab to another window. In that case it is
79 # _not_ a location change; the blacklist should not be applied.
80 if event.persisted
81 url = @vim.content.location.href
82 messageManager.send('cachedPageshow', null, (movedToNewTab) =>
83 if not movedToNewTab and oldUrl != @currentUrl
84 messageManager.send('locationChange', @currentUrl)
85 )
86 )
87
88 @listen('pagehide', (event) =>
89 target = event.originalTarget
90 @currentUrl = null
91
92 if target == @vim.content.document
93 messageManager.send('frameCanReceiveEvents', false)
94
95 # If the target isn’t the topmost document, it means that a frame has
96 # changed: It could have been removed or its `src` attribute could have
97 # been changed. If the frame contains other frames, 'pagehide' events have
98 # already been fired for them.
99 @vim.resetState(target)
100 )
101
102 messageManager.listen('getMarkableElementsMovements', (data, callback) =>
103 diffs = @vim.state.markerElements.map(({element, originalRect}) ->
104 newRect = element.getBoundingClientRect()
105 return {
106 dx: newRect.left - originalRect.left
107 dy: newRect.top - originalRect.top
108 }
109 )
110 callback(diffs)
111 )
112
113 @listen('overflow', (event) =>
114 target = event.originalTarget
115 @vim.state.scrollableElements.addChecked(target)
116 )
117
118 @listen('underflow', (event) =>
119 target = event.originalTarget
120 @vim.state.scrollableElements.deleteChecked(target)
121 )
122
123 @listen('keydown', (event) =>
124 @keepInputs = false
125 )
126
127 @listen('keydown', ((event) =>
128 suppress = false
129
130 # This message _has_ to be synchronous so we can suppress the event if
131 # needed. To avoid sending a synchronous message on _every_ keydown, this
132 # hack of toggling a pref when a `<late>` shortcut is encountered is used.
133 if prefs.get('late')
134 suppress = messageManager.get('lateKeydown', {
135 defaultPrevented: event.defaultPrevented
136 })
137
138 if @vim.state.inputs and @vim.mode == 'normal' and not suppress and
139 not event.defaultPrevented
140 # There is no need to take `ignore_keyboard_layout` and `translations`
141 # into account here, since we want to override the _native_ `<tab>`
142 # behavior. Then, `event.key` is the way to go. (Unless the prefs are
143 # customized. YAGNI until requested.)
144 keyStr = notation.stringify(event)
145 direction = switch keyStr
146 when ''
147 null
148 when prefs.get('focus_previous_key')
149 -1
150 when prefs.get('focus_next_key')
151 +1
152 else
153 null
154 if direction?
155 suppress = commands.move_focus({@vim, direction})
156 @keepInputs = true
157
158 if suppress
159 utils.suppressEvent(event)
160 @listenOnce('keyup', utils.suppressEvent, false)
161 ), false)
162
163 @listen('mousedown', (event) =>
164 # Allow clicking on another text input without exiting “gi mode”. Listen
165 # for 'mousedown' instead of 'click', because only the former runs before
166 # the 'blur' event. Also, `event.originalTarget` does _not_ work here.
167 @keepInputs = (@vim.state.inputs and event.target in @vim.state.inputs)
168
169 # Clicks are always counted as page interaction. Listen for 'mousedown'
170 # instead of 'click' to mark the interaction as soon as possible.
171 @vim.markPageInteraction()
172 )
173
174 messageManager.listen('browserRefocus', =>
175 # Suppress the next two focus events (for `document` and `window`; see
176 # `blurActiveBrowserElement`).
177 @numFocusToSuppress = 2
178 )
179
180 sendFocusType = =>
181 return unless activeElement = utils.getActiveElement(@vim.content)
182 focusType = utils.getFocusType(activeElement)
183 messageManager.send('focusType', focusType)
184
185 @listen('focus', (event) =>
186 target = event.originalTarget
187
188 if @numFocusToSuppress > 0
189 utils.suppressEvent(event)
190 @numFocusToSuppress -= 1
191 return
192
193 @vim.state.explicitBodyFocus = (target == @vim.content.document.body)
194
195 sendFocusType()
196
197 # Reset `hasInteraction` when (re-)selecting a tab, or coming back from
198 # another window, in order to prevent the common “automatically re-focus
199 # when switching back to the tab” behaviour many sites have, unless a text
200 # input _should_ be re-focused when coming back to the tab (see the 'blur'
201 # event below).
202 if target == @vim.content.document
203 if @vim.state.shouldRefocus
204 @vim.state.hasInteraction = true
205 @vim.state.shouldRefocus = false
206 else
207 @vim.state.hasInteraction = false
208 return
209
210 # Save the last focused text input regardless of whether that input might
211 # be blurred because of autofocus prevention.
212 if utils.isTextInputElement(target)
213 @vim.state.lastFocusedTextInput = target
214 @vim.state.hasFocusedTextInput = true
215
216 # Blur the focus target, if autofocus prevention is enabled…
217 if prefs.get('prevent_autofocus') and
218 @vim.mode in prefs.get('prevent_autofocus_modes').split(/\s+/) and
219 # …and the user has interacted with the page…
220 not @vim.state.hasInteraction and
221 # …and the event is programmatic (not caused by clicks or keypresses)…
222 nsIFocusManager.getLastFocusMethod(null) == 0 and
223 # …and the target may steal most keystrokes…
224 utils.isTypingElement(target) and
225 # …and the page isn’t a Firefox internal page (like `about:config`).
226 @vim.content.document not instanceof XULDocument
227 # Some sites (such as icloud.com) re-focuses inputs if they are blurred,
228 # causing an infinite loop of autofocus prevention and re-focusing.
229 # Therefore, blur events that happen just after an autofocus prevention
230 # are suppressed.
231 @listenOnce('blur', utils.suppressEvent)
232 target.blur()
233 @vim.state.hasFocusedTextInput = false
234 )
235
236 @listen('blur', (event) =>
237 target = event.originalTarget
238
239 sendFocusType()
240
241 # If a text input is blurred immediately before the document loses focus,
242 # it most likely means that the user switched tab, for example by pressing
243 # `<c-tab>`, or switched to another window, while the text input was
244 # focused. In this case, when switching back to that tab, the text input
245 # will, and should, be re-focused (because it was focused when you left
246 # the tab). This case is kept track of so that the autofocus prevention
247 # does not catch it.
248 if utils.isTypingElement(target)
249 utils.nextTick(@vim.content, =>
250 @vim.state.shouldRefocus = not @vim.content.document.hasFocus()
251
252 # “gi mode” ends when blurring a text input, unless `<tab>` was just
253 # pressed.
254 unless @vim.state.shouldRefocus or @keepInputs
255 commands.clear_inputs({@vim})
256 )
257 )
258
259 module.exports = FrameEventManager
Imprint / Impressum