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