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