]> git.gir.st - VimFx.git/blob - extension/lib/events-frame.coffee
Update copyright years
[VimFx.git] / extension / lib / events-frame.coffee
1 ###
2 # Copyright Simon Lydell 2015, 2016, 2017.
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 @disconnectActiveElementObserver = null
39
40 listen: utils.listen.bind(null, FRAME_SCRIPT_ENVIRONMENT)
41 listenOnce: utils.listenOnce.bind(null, FRAME_SCRIPT_ENVIRONMENT)
42
43 addListeners: ->
44 # If the page already was loaded when VimFx was initialized, send the
45 # 'frameCanReceiveEvents' message straight away.
46 if @vim.content.document.readyState == 'complete'
47 messageManager.send('frameCanReceiveEvents', true)
48
49 @listen('readystatechange', (event) =>
50 target = event.originalTarget
51 topDocument = @vim.content.document
52 [oldUrl, @currentUrl] = [@currentUrl, @vim.content.location.href]
53
54 switch target.readyState
55 when 'interactive'
56 if target == topDocument or
57 # When loading the editor on codepen.io, a frame gets
58 # 'readystatechange' → 'interactive' quite a bit before the
59 # toplevel document does. Checking for this case lets us send
60 # 'locationChange' earlier, allowing to enter Ignore mode earlier,
61 # for example. Be careful not to trigger a 'locationChange' for
62 # frames loading _after_ the toplevel document, though. Finally,
63 # checking for 'uninitialized' is needed to be able to blacklist
64 # some XUL pages.
65 (topDocument.readyState in ['loading', 'uninitialized'] and
66 oldUrl == null)
67 messageManager.send('locationChange', @currentUrl)
68
69 when 'complete'
70 if target == topDocument
71 messageManager.send('frameCanReceiveEvents', true)
72 )
73
74 @listen('pageshow', (event) =>
75 [oldUrl, @currentUrl] = [@currentUrl, @vim.content.location.href]
76
77 # When navigating the history, `event.persisted` is `true` (meaning that
78 # the page loaded from cache) and 'readystatechange' won’t be fired, so
79 # send a 'locationChange' message to make sure that the blacklist is
80 # applied etc. The reason we don’t simply _always_ do this on the
81 # 'pageshow' event, is because it usually fires too late. However, it also
82 # fires after having moved a tab to another window. In that case it is
83 # _not_ a location change; the blacklist should not be applied.
84 if event.persisted
85 url = @vim.content.location.href
86 messageManager.send('cachedPageshow', null, (movedToNewTab) =>
87 if not movedToNewTab and oldUrl != @currentUrl
88 messageManager.send('locationChange', @currentUrl)
89 )
90 )
91
92 @listen('pagehide', (event) =>
93 target = event.originalTarget
94 @currentUrl = null
95
96 if target == @vim.content.document
97 messageManager.send('frameCanReceiveEvents', false)
98 @vim._enterMode('normal') if @vim.mode == 'hints'
99
100 # If the target isn’t the topmost document, it means that a frame has
101 # changed: It could have been removed or its `src` attribute could have
102 # been changed. If the frame contains other frames, 'pagehide' events have
103 # already been fired for them.
104 @vim.resetState(target)
105 )
106
107 messageManager.listen('getMarkableElementsMovements', (data, callback) =>
108 diffs = @vim.state.markerElements.map(({element, originalRect}) ->
109 newRect = element.getBoundingClientRect()
110 return {
111 dx: newRect.left - originalRect.left
112 dy: newRect.top - originalRect.top
113 }
114 )
115 callback(diffs)
116 )
117
118 messageManager.listen('highlightMarkableElements', (data) =>
119 {elements, strings} = data
120 utils.clearSelectionDeep(@vim.content)
121 for {elementIndex, selectAll} in elements
122 {element} = @vim.state.markerElements[elementIndex]
123 if selectAll
124 utils.selectElement(element)
125 else
126 for string in strings
127 utils.selectAllSubstringMatches(
128 element, string, {caseSensitive: false}
129 )
130 return
131 )
132
133 @listen('overflow', (event) =>
134 target = event.originalTarget
135 @vim.state.scrollableElements.addChecked(target)
136 )
137
138 @listen('underflow', (event) =>
139 target = event.originalTarget
140 @vim.state.scrollableElements.deleteChecked(target)
141 )
142
143 @listen('submit', ((event) ->
144 return if event.defaultPrevented
145 target = event.originalTarget
146 {activeElement} = target.ownerDocument
147 if activeElement?.form == target and utils.isTypingElement(activeElement)
148 activeElement.blur()
149 ), false)
150
151 @listen('keydown', (event) =>
152 @keepInputs = false
153 )
154
155 @listen('keydown', ((event) =>
156 suppress = false
157
158 # This message _has_ to be synchronous so we can suppress the event if
159 # needed. To avoid sending a synchronous message on _every_ keydown, this
160 # hack of toggling a pref when a `<late>` shortcut is encountered is used.
161 if prefs.get('late')
162 suppress = messageManager.get('lateKeydown', {
163 defaultPrevented: event.defaultPrevented
164 })
165
166 if @vim.state.inputs and @vim.mode == 'normal' and not suppress and
167 not event.defaultPrevented
168 # There is no need to take `ignore_keyboard_layout` and `translations`
169 # into account here, since we want to override the _native_ `<tab>`
170 # behavior. Then, `event.key` is the way to go. (Unless the prefs are
171 # customized. YAGNI until requested.) Also, since 'keydown' is fired so
172 # often the options are read directly from the prefs system for
173 # performance. That means you can’t override them with
174 # `vimfx.addOptionOverrides`. YAGNI until requested.
175 keyStr = notation.stringify(event)
176 direction = switch keyStr
177 when ''
178 null
179 when prefs.get('focus_previous_key')
180 -1
181 when prefs.get('focus_next_key')
182 +1
183 else
184 null
185 if direction?
186 suppress = commands.move_focus({@vim, direction})
187 @keepInputs = true
188
189 if suppress
190 utils.suppressEvent(event)
191 @listenOnce('keyup', utils.suppressEvent, false)
192 ), false)
193
194 @listen('mousedown', (event) =>
195 # Allow clicking on another text input without exiting “gi mode”. Listen
196 # for 'mousedown' instead of 'click', because only the former runs before
197 # the 'blur' event. Also, `event.originalTarget` does _not_ work here.
198 @keepInputs = (@vim.state.inputs and event.target in @vim.state.inputs)
199
200 # Clicks are always counted as page interaction. Listen for 'mousedown'
201 # instead of 'click' to mark the interaction as soon as possible.
202 @vim.markPageInteraction()
203
204 @vim.hideNotification()
205 )
206
207 messageManager.listen('browserRefocus', =>
208 # Suppress the next two focus events (for `document` and `window`; see
209 # `blurActiveBrowserElement`).
210 @numFocusToSuppress = 2
211 )
212
213 @listen('focus', (event) =>
214 target = event.originalTarget
215
216 if @numFocusToSuppress > 0
217 utils.suppressEvent(event)
218 @numFocusToSuppress -= 1
219 return
220
221 @vim.state.explicitBodyFocus = (target == @vim.content.document.body)
222
223 @sendFocusType()
224
225 # Reset `hasInteraction` when (re-)selecting a tab, or coming back from
226 # another window, in order to prevent the common “automatically re-focus
227 # when switching back to the tab” behaviour many sites have, unless a text
228 # input _should_ be re-focused when coming back to the tab (see the 'blur'
229 # event below).
230 if target == @vim.content.document
231 if @vim.state.shouldRefocus
232 @vim.state.hasInteraction = true
233 # When Firefox is re-focused after using a keyboard shortcut to switch
234 # keyboard layout in GNOME, _two_ focus events for the document are
235 # triggered, about 50ms apart. Therefore, reset the `shouldRefocus`
236 # after a timeout.
237 @vim.content.setTimeout((=>
238 @vim.state.shouldRefocus = false
239 ), prefs.get('refocus_timeout'))
240 else
241 @vim.state.hasInteraction = false
242 return
243
244 if utils.isTextInputElement(target)
245 # Save the last focused text input regardless of whether that input
246 # might be blurred because of autofocus prevention.
247 @vim.state.lastFocusedTextInput = target
248 @vim.state.hasFocusedTextInput = true
249
250 if @vim.mode == 'caret' and not utils.isContentEditable(target)
251 @vim._enterMode('normal')
252
253 # When moving a tab to another window, there is a short period of time
254 # when there’s no listener for this call.
255 return unless options = @vim.options(
256 ['prevent_autofocus', 'prevent_autofocus_modes']
257 )
258
259 # Blur the focus target, if autofocus prevention is enabled…
260 if options.prevent_autofocus and
261 @vim.mode in options.prevent_autofocus_modes and
262 # …and the user has interacted with the page…
263 not @vim.state.hasInteraction and
264 # …and the event is programmatic (not caused by clicks or keypresses)…
265 nsIFocusManager.getLastFocusMethod(null) == 0 and
266 # …and the target may steal most keystrokes…
267 utils.isTypingElement(target) and
268 # …and the page isn’t a Firefox internal page (like `about:config`).
269 @vim.content.document not instanceof XULDocument
270 # Some sites (such as icloud.com) re-focuses inputs if they are blurred,
271 # causing an infinite loop of autofocus prevention and re-focusing.
272 # Therefore, blur events that happen just after an autofocus prevention
273 # are suppressed.
274 @listenOnce('blur', utils.suppressEvent)
275 target.blur()
276 @vim.state.hasFocusedTextInput = false
277 )
278
279 @listen('blur', (event) =>
280 target = event.originalTarget
281
282 if target == @vim.state.lastHover.element and
283 # Facebook “like” button exception. The “emoji picker” immediately
284 # closes otherwise.
285 not target.classList?.contains('UFILikeLink')
286 @vim.clearHover()
287
288 @vim.content.setTimeout((=>
289 @sendFocusType()
290 ), prefs.get('blur_timeout'))
291
292 # If a text input is blurred immediately before the document loses focus,
293 # it most likely means that the user switched tab, for example by pressing
294 # `<c-tab>`, or switched to another window, while the text input was
295 # focused. In this case, when switching back to that tab, the text input
296 # will, and should, be re-focused (because it was focused when you left
297 # the tab). This case is kept track of so that the autofocus prevention
298 # does not catch it.
299 if utils.isTypingElement(target)
300 utils.nextTick(@vim.content, =>
301 @vim.state.shouldRefocus = not @vim.content.document.hasFocus()
302
303 # “gi mode” ends when blurring a text input, unless `<tab>` was just
304 # pressed.
305 unless @vim.state.shouldRefocus or @keepInputs
306 commands.clear_inputs({@vim})
307 )
308 )
309
310 messageManager.listen('checkFocusType', @sendFocusType.bind(this))
311
312 sendFocusType: ({ignore = []} = {}) ->
313 return unless activeElement = utils.getActiveElement(@vim.content)
314 focusType = utils.getFocusType(activeElement)
315 messageManager.send('focusType', focusType) unless focusType in ignore
316
317 # If a text input is removed from the DOM while it is focused, no 'focus'
318 # or 'blur' events will be fired, making VimFx think that the text input is
319 # still focused. Therefore we add a temporary observer for the currently
320 # focused element and re-send the focusType if it gets removed.
321 @disconnectActiveElementObserver?()
322 @disconnectActiveElementObserver =
323 if focusType == 'none'
324 null
325 else
326 utils.onRemoved(activeElement, @sendFocusType.bind(this))
327
328 module.exports = FrameEventManager
Imprint / Impressum