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