2 # Copyright Simon Lydell 2015.
4 # This file is part of VimFx.
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.
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.
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/>.
20 # This file is the equivalent to events.coffee, but for frame scripts.
22 notation = require('vim-like-key-notation')
23 commands = require('./commands-frame')
24 messageManager = require('./message-manager')
25 utils = require('./utils')
27 class FrameEventManager
28 constructor: (@vim) ->
29 @numFocusToSuppress = 0
32 listen: utils.listen.bind(null, FRAME_SCRIPT_ENVIRONMENT)
33 listenOnce: utils.listenOnce.bind(null, FRAME_SCRIPT_ENVIRONMENT)
36 # If the page already was loaded when VimFx was initialized, send the
37 # 'DOMWindowCreated' message straight away.
38 if @vim.content.document.readyState in ['interactive', 'complete']
39 messageManager.send('DOMWindowCreated')
41 @listen('DOMWindowCreated', (event) ->
42 messageManager.send('DOMWindowCreated')
45 @listen('readystatechange', (event) =>
46 target = event.originalTarget
48 # If the topmost document starts loading, it means that we have navigated
49 # to a new page or refreshed the page.
50 if target == @vim.content.document and target.readyState == 'interactive'
51 messageManager.send('locationChange', @vim.content.location.href)
54 @listen('pagehide', (event) =>
55 target = event.originalTarget
57 if target == @vim.content.document
60 # If the target isn’t the topmost document, it means that a frame has
61 # changed: It could have been removed or its `src` attribute could have
62 # been changed. Any scrollable elements in the frame (or its sub-frames)
63 # then need to be filtered out.
64 @vim.state.scrollableElements
65 .reject(utils.windowContainsDeep.bind(null, target.defaultView))
68 @listen('click', (event) =>
69 if @vim.mode == 'hints' and event.isTrusted
70 messageManager.send('enterMode', {mode: 'normal'})
73 @listen('overflow', (event) =>
74 target = event.originalTarget
75 return unless computedStyle = @vim.content.getComputedStyle(target)
76 unless computedStyle.getPropertyValue('overflow-y') == 'hidden' and
77 computedStyle.getPropertyValue('overflow-x') == 'hidden'
78 @vim.state.scrollableElements.add(target)
81 @listen('underflow', (event) =>
82 target = event.originalTarget
83 @vim.state.scrollableElements.delete(target)
86 @listen('keydown', (event) =>
89 suppress = @vim.onInput(event)
91 # This also suppresses the 'keypress' and 'keyup' events. (Yes, in frame
92 # scripts, suppressing the 'keydown' events does seem to even suppress
94 utils.suppressEvent(event) if suppress
96 # From this line on, the rest of the code in `addListeners` is more or
97 # less devoted to autofocus prevention. When enabled, focus events that
98 # occur before the user has interacted with page are prevented.
100 # If this keydown event wasn’t suppressed (`not suppress`), it’s an
101 # obvious interaction with the page. If it _was_ suppressed, though, it’s
102 # an interaction depending on the command triggered; if it calls
103 # `vim.markPageInteraction()` or not.
104 @vim.markPageInteraction() unless suppress
107 @listen('keydown', ((event) =>
108 suppress = messageManager.get('lateKeydown', {
109 defaultPrevented: event.defaultPrevented
112 if @vim.state.inputs and @vim.mode == 'normal' and not suppress and
113 not event.defaultPrevented
114 # There is no need to take `ignore_keyboard_layout` and `translations`
115 # into account here, since we want to override the _native_ `<tab>`
116 # behavior. Then, `event.key` is the way to go. (Unless the prefs are
117 # customized. YAGNI until requested.)
118 keyStr = notation.stringify(event)
119 options = @vim.options(['focus_previous_key', 'focus_next_key'])
120 direction = switch keyStr
122 when options.focus_previous_key then -1
123 when options.focus_next_key then +1
126 suppress = commands.move_focus({@vim, direction})
129 utils.suppressEvent(event) if suppress
132 @listen('mousedown', (event) =>
133 # Allow clicking on another text input without exiting “gi mode”. Listen
134 # for 'mousedown' instead of 'click', because only the former runs before
135 # the 'blur' event. Also, `event.originalTarget` does _not_ work here.
136 @keepInputs = (@vim.state.inputs and event.target in @vim.state.inputs)
138 # Clicks are always counted as page interaction. Listen for 'mousedown'
139 # instead of 'click' to mark the interaction as soon as possible.
140 @vim.markPageInteraction()
143 messageManager.listen('browserRefocus', =>
144 # Suppress the next two focus events (for `document` and `window`; see
145 # `blurActiveBrowserElement`).
146 @numFocusToSuppress = 2
150 focusType = utils.getFocusType(utils.getActiveElement(@vim.content))
151 messageManager.send('focusType', focusType)
153 @listen('focus', (event) =>
154 target = event.originalTarget
156 if @numFocusToSuppress > 0
157 utils.suppressEvent(event)
158 @numFocusToSuppress--
163 # Reset `hasInteraction` when (re-)selecting a tab, or coming back from
164 # another window, in order to prevent the common “automatically re-focus
165 # when switching back to the tab” behaviour many sites have, unless a text
166 # input _should_ be re-focused when coming back to the tab (see the 'blur'
168 if target == @vim.content.document
169 if @vim.state.shouldRefocus
170 @vim.state.hasInteraction = true
171 @vim.state.shouldRefocus = false
173 @vim.state.hasInteraction = false
176 # Save the last focused text input regardless of whether that input might
177 # be blurred because of autofocus prevention.
178 if utils.isTextInputElement(target)
179 @vim.state.lastFocusedTextInput = target
181 focusManager = Cc['@mozilla.org/focus-manager;1']
182 .getService(Ci.nsIFocusManager)
184 # When moving a tab to another window, there is a short period of time
185 # when there’s no listener for this call.
186 return unless options = @vim.options(
187 ['prevent_autofocus', 'prevent_autofocus_modes']
190 # Blur the focus target, if autofocus prevention is enabled…
191 if options.prevent_autofocus and
192 @vim.mode in options.prevent_autofocus_modes and
193 # …and the user has interacted with the page…
194 not @vim.state.hasInteraction and
195 # …and the event is programmatic (not caused by clicks or keypresses)…
196 focusManager.getLastFocusMethod(null) == 0 and
197 # …and the target may steal most keystrokes.
198 (utils.isTypingElement(target) or utils.isContentEditable(target))
199 # Some sites (such as icloud.com) re-focuses inputs if they are blurred,
200 # causing an infinite loop of autofocus prevention and re-focusing.
201 # Therefore, blur events that happen just after an autofocus prevention
203 @listenOnce('blur', utils.suppressEvent)
207 @listen('blur', (event) =>
208 target = event.originalTarget
212 # If a text input is blurred immediately before the document loses focus,
213 # it most likely means that the user switched tab, for example by pressing
214 # `<c-tab>`, or switched to another window, while the text input was
215 # focused. In this case, when switching back to that tab, the text input
216 # will, and should, be re-focused (because it was focused when you left
217 # the tab). This case is kept track of so that the autofocus prevention
219 if utils.isTypingElement(target) or utils.isContentEditable(target)
220 utils.nextTick(@vim.content, =>
221 @vim.state.shouldRefocus = not @vim.content.document.hasFocus()
223 # “gi mode” ends when blurring a text input, unless `<tab>` was just
225 unless @vim.state.shouldRefocus or @keepInputs
226 commands.clear_inputs({@vim})
230 module.exports = FrameEventManager