]> git.gir.st - VimFx.git/blob - extension/lib/events-frame.coffee
Rework "move tab to window" detection logic
[VimFx.git] / extension / lib / events-frame.coffee
1 ###
2 # Copyright Simon Lydell 2015.
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 class FrameEventManager
28 constructor: (@vim) ->
29 @numFocusToSuppress = 0
30 @keepInputs = false
31 @currentUrl = false
32
33 MINIMUM_SCROLLABLE_ELEMENT_AREA: 25
34
35 listen: utils.listen.bind(null, FRAME_SCRIPT_ENVIRONMENT)
36 listenOnce: utils.listenOnce.bind(null, FRAME_SCRIPT_ENVIRONMENT)
37
38 addListeners: ->
39 # If the page already was loaded when VimFx was initialized, send the
40 # 'DOMWindowCreated' message straight away.
41 if @vim.content.document.readyState in ['interactive', 'complete']
42 messageManager.send('DOMWindowCreated')
43 else
44 @listen('DOMWindowCreated', (event) ->
45 messageManager.send('DOMWindowCreated')
46 )
47
48 @listen('readystatechange', (event) =>
49 target = event.originalTarget
50 @currentUrl = @vim.content.location.href
51
52 # If the topmost document starts loading, it means that we have navigated
53 # to a new page or refreshed the page.
54 if target == @vim.content.document and target.readyState == 'interactive'
55 messageManager.send('locationChange', @currentUrl)
56 )
57
58 @listen('pageshow', (event) =>
59 [oldUrl, @currentUrl] = [@currentUrl, @vim.content.location.href]
60
61 # When navigating the history, `event.persisted` is `true` (meaning that
62 # the page loaded from cache) and 'readystatechange' won’t be fired, so
63 # send a 'locationChange' message to make sure that the blacklist is
64 # applied etc. The reason we don’t simply _always_ do this on the
65 # 'pageshow' event, is because it usually fires too late. However, it also
66 # fires after having moved a tab to another window. In that case it is
67 # _not_ a location change; the blacklist should not be applied.
68 if event.persisted
69 url = @vim.content.location.href
70 messageManager.send('cachedPageshow', null, (movedToNewTab) =>
71 if not movedToNewTab and oldUrl != @currentUrl
72 messageManager.send('locationChange', @currentUrl)
73 )
74 )
75
76 @listen('pagehide', (event) =>
77 target = event.originalTarget
78 @currentUrl = null
79
80 # If the target isn’t the topmost document, it means that a frame has
81 # changed: It could have been removed or its `src` attribute could have
82 # been changed. If the frame contains other frames, 'pagehide' events have
83 # already been fired for them.
84 @vim.resetState(target)
85 )
86
87 @listen('click', (event) =>
88 if @vim.mode == 'hints' and event.isTrusted
89 messageManager.send('enterMode', {mode: 'normal'})
90 )
91
92 @listen('overflow', (event) =>
93 target = event.originalTarget
94
95 return unless computedStyle = @vim.content.getComputedStyle(target)
96 unless (computedStyle.getPropertyValue('overflow-y') == 'hidden' and
97 computedStyle.getPropertyValue('overflow-x') == 'hidden') or
98 # There’s no need to track elements so small that they don’t even
99 # fit the scrollbars. For example, Gmail has lots of tiny
100 # overflowing iframes. Filter those out.
101 utils.area(target) < @MINIMUM_SCROLLABLE_ELEMENT_AREA or
102 # On some pages, such as Google Groups, 'overflow' events may occur
103 # for elements that aren’t even scrollable.
104 not @vim.state.scrollableElements.isScrollable(target)
105 @vim.state.scrollableElements.add(target)
106 )
107
108 @listen('underflow', (event) =>
109 target = event.originalTarget
110
111 # On some pages, such as Gmail, 'underflow' events may occur for elements
112 # that are actually still scrollable! If so, keep the element.
113 unless @vim.state.scrollableElements.isScrollable(target)
114 @vim.state.scrollableElements.delete(target)
115 )
116
117 @listen('keydown', (event) =>
118 @keepInputs = false
119
120 suppress = @vim.onInput(event)
121
122 # This also suppresses the 'keypress' and 'keyup' events. (Yes, in frame
123 # scripts, suppressing the 'keydown' events does seem to even suppress
124 # the 'keyup' event!)
125 utils.suppressEvent(event) if suppress
126
127 # From this line on, the rest of the code in `addListeners` is more or
128 # less devoted to autofocus prevention. When enabled, focus events that
129 # occur before the user has interacted with page are prevented.
130 #
131 # If this keydown event wasn’t suppressed (`not suppress`), it’s an
132 # obvious interaction with the page. If it _was_ suppressed, though, it’s
133 # an interaction depending on the command triggered; if it calls
134 # `vim.markPageInteraction()` or not.
135 @vim.markPageInteraction() unless suppress
136 )
137
138 @listen('keydown', ((event) =>
139 suppress = messageManager.get('lateKeydown', {
140 defaultPrevented: event.defaultPrevented
141 })
142
143 if @vim.state.inputs and @vim.mode == 'normal' and not suppress and
144 not event.defaultPrevented
145 # There is no need to take `ignore_keyboard_layout` and `translations`
146 # into account here, since we want to override the _native_ `<tab>`
147 # behavior. Then, `event.key` is the way to go. (Unless the prefs are
148 # customized. YAGNI until requested.)
149 keyStr = notation.stringify(event)
150 options = @vim.options(['focus_previous_key', 'focus_next_key'])
151 direction = switch keyStr
152 when '' then null
153 when options.focus_previous_key then -1
154 when options.focus_next_key then +1
155 else null
156 if direction?
157 suppress = commands.move_focus({@vim, direction})
158 @keepInputs = true
159
160 utils.suppressEvent(event) if suppress
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 focusType = utils.getFocusType(utils.getActiveElement(@vim.content))
182 messageManager.send('focusType', focusType)
183
184 @listen('focus', (event) =>
185 target = event.originalTarget
186
187 if @numFocusToSuppress > 0
188 utils.suppressEvent(event)
189 @numFocusToSuppress--
190 return
191
192 sendFocusType()
193
194 # Reset `hasInteraction` when (re-)selecting a tab, or coming back from
195 # another window, in order to prevent the common “automatically re-focus
196 # when switching back to the tab” behaviour many sites have, unless a text
197 # input _should_ be re-focused when coming back to the tab (see the 'blur'
198 # event below).
199 if target == @vim.content.document
200 if @vim.state.shouldRefocus
201 @vim.state.hasInteraction = true
202 @vim.state.shouldRefocus = false
203 else
204 @vim.state.hasInteraction = false
205 return
206
207 # Save the last focused text input regardless of whether that input might
208 # be blurred because of autofocus prevention.
209 if utils.isTextInputElement(target)
210 @vim.state.lastFocusedTextInput = target
211
212 focusManager = Cc['@mozilla.org/focus-manager;1']
213 .getService(Ci.nsIFocusManager)
214
215 # When moving a tab to another window, there is a short period of time
216 # when there’s no listener for this call.
217 return unless options = @vim.options(
218 ['prevent_autofocus', 'prevent_autofocus_modes']
219 )
220
221 # Blur the focus target, if autofocus prevention is enabled…
222 if options.prevent_autofocus and
223 @vim.mode in options.prevent_autofocus_modes and
224 # …and the user has interacted with the page…
225 not @vim.state.hasInteraction and
226 # …and the event is programmatic (not caused by clicks or keypresses)…
227 focusManager.getLastFocusMethod(null) == 0 and
228 # …and the target may steal most keystrokes.
229 (utils.isTypingElement(target) or utils.isContentEditable(target))
230 # Some sites (such as icloud.com) re-focuses inputs if they are blurred,
231 # causing an infinite loop of autofocus prevention and re-focusing.
232 # Therefore, blur events that happen just after an autofocus prevention
233 # are suppressed.
234 @listenOnce('blur', utils.suppressEvent)
235 target.blur()
236 )
237
238 @listen('blur', (event) =>
239 target = event.originalTarget
240
241 sendFocusType()
242
243 # If a text input is blurred immediately before the document loses focus,
244 # it most likely means that the user switched tab, for example by pressing
245 # `<c-tab>`, or switched to another window, while the text input was
246 # focused. In this case, when switching back to that tab, the text input
247 # will, and should, be re-focused (because it was focused when you left
248 # the tab). This case is kept track of so that the autofocus prevention
249 # does not catch it.
250 if utils.isTypingElement(target) or utils.isContentEditable(target)
251 utils.nextTick(@vim.content, =>
252 @vim.state.shouldRefocus = not @vim.content.document.hasFocus()
253
254 # “gi mode” ends when blurring a text input, unless `<tab>` was just
255 # pressed.
256 unless @vim.state.shouldRefocus or @keepInputs
257 commands.clear_inputs({@vim})
258 )
259 )
260
261 module.exports = FrameEventManager
Imprint / Impressum