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 messageManager = require('./message-manager')
23 utils = require('./utils')
25 class FrameEventManager
26 constructor: (@vim) ->
27 @numFocusToSuppress = 0
29 listen: utils.listen.bind(null, FRAME_SCRIPT_ENVIRONMENT)
30 listenOnce: utils.listenOnce.bind(null, FRAME_SCRIPT_ENVIRONMENT)
33 # If the page already was loaded when VimFx was initialized, send the
34 # 'DOMWindowCreated' message straight away.
35 if @vim.content.document.readyState in ['interactive', 'complete']
36 messageManager.send('DOMWindowCreated')
38 @listen('DOMWindowCreated', (event) ->
39 messageManager.send('DOMWindowCreated')
42 @listen('readystatechange', (event) =>
44 # Only handle 'readystatechange' for the top-most window, not for frames,
45 # and only before any state about the page has been collected.
46 if window.document.readyState == 'interactive'
48 messageManager.send('locationChange', window.location.href)
51 @listen('click', (event) =>
52 if @vim.mode == 'hints' and event.isTrusted
53 messageManager.send('enterMode', {mode: 'normal'})
56 @listen('overflow', (event) =>
57 target = event.originalTarget
58 return unless computedStyle = @vim.content.getComputedStyle(target)
59 return if computedStyle.getPropertyValue('overflow') == 'hidden'
60 @vim.state.scrollableElements.add(target)
62 # Unfortunately, the 'underflow' event is not triggered when a scrollable
63 # element is removed from the DOM, so that needs to be tracked separately.
64 mutationObserver = new @vim.content.MutationObserver((changes) ->
65 for change in changes then for element in change.removedNodes
67 removeScrollableElement(target)
68 mutationObserver.disconnect()
71 mutationObserver.observe(target.parentNode, {childList: true})
72 module.onShutdown(mutationObserver.disconnect.bind(mutationObserver))
74 if not @vim.state.largestScrollableElement or
75 utils.area(target) > utils.area(@vim.state.largestScrollableElement)
76 @vim.state.largestScrollableElement = target
79 @listen('underflow', (event) ->
80 target = event.originalTarget
81 removeScrollableElement(target)
84 removeScrollableElement = (target) =>
85 @vim.state.scrollableElements.delete(target)
87 if @vim.state.largestScrollableElement == target
88 @vim.state.largestScrollableElement = null
90 # Find a new largest scrollable element (if there are any left).
92 @vim.state.scrollableElements.forEach((element) =>
93 area = utils.area(element)
95 @vim.state.largestScrollableElement = element
99 @listen('keydown', (event) =>
100 suppress = @vim.onInput(event)
102 # This also suppresses the 'keypress' and 'keyup' events. (Yes, in frame
103 # scripts, suppressing the 'keydown' events does seem to even suppress
104 # the 'keyup' event!)
105 utils.suppressEvent(event) if suppress
107 # From this line on, the rest of the code in `addListeners` is more or
108 # less devoted to autofocus prevention. When enabled, focus events that
109 # occur before the user has interacted with page are prevented.
111 # If this keydown event wasn’t suppressed (`not suppress`), it’s an
112 # obvious interaction with the page. If it _was_ suppressed, though, it’s
113 # an interaction depending on the command triggered; if it calls
114 # `vim.markPageInteraction()` or not.
115 @vim.markPageInteraction() unless suppress
118 @listen('keydown', ((event) ->
119 suppress = messageManager.get('lateKeydown', {
120 defaultPrevented: event.defaultPrevented
122 utils.suppressEvent(event) if suppress
125 # Clicks are always counted as page interaction. Listen for 'mousedown'
126 # instead of 'click' to mark the interaction as soon as possible.
127 @listen('mousedown', @vim.markPageInteraction.bind(@vim))
129 messageManager.listen('browserRefocus', =>
130 # Suppress the next two focus events (for `document` and `window`; see
131 # `blurActiveBrowserElement`).
132 @numFocusToSuppress = 2
135 @listen('focus', (event) =>
136 target = event.originalTarget
138 if @numFocusToSuppress > 0
139 utils.suppressEvent(event)
140 @numFocusToSuppress--
143 # Reset `hasInteraction` when (re-)selecting a tab, or coming back from
144 # another window, in order to prevent the common “automatically re-focus
145 # when switching back to the tab” behaviour many sites have, unless a text
146 # input _should_ be re-focused when coming back to the tab (see the 'blur'
148 if target == @vim.content.document
149 if @vim.state.shouldRefocus
150 @vim.state.hasInteraction = true
151 @vim.state.shouldRefocus = false
153 @vim.state.hasInteraction = false
156 # Save the last focused text input regardless of whether that input might
157 # be blurred because of autofocus prevention.
158 if utils.isTextInputElement(target)
159 @vim.state.lastFocusedTextInput = target
161 focusManager = Cc['@mozilla.org/focus-manager;1']
162 .getService(Ci.nsIFocusManager)
164 # When moving a tab to another window, there is a short period of time
165 # when there’s no listener for this call.
166 return unless options = @vim.options(
167 ['prevent_autofocus', 'prevent_autofocus_modes']
170 # Blur the focus target, if autofocus prevention is enabled…
171 if options.prevent_autofocus and
172 @vim.mode in options.prevent_autofocus_modes and
173 # …and the user has interacted with the page…
174 not @vim.state.hasInteraction and
175 # …and the event is programmatic (not caused by clicks or keypresses)…
176 focusManager.getLastFocusMethod(null) == 0 and
177 # …and the target may steal most keystrokes.
178 (utils.isTypingElement(target) or utils.isContentEditable(target))
179 # Some sites (such as icloud.com) re-focuses inputs if they are blurred,
180 # causing an infinite loop of autofocus prevention and re-focusing.
181 # Therefore, blur events that happen just after an autofocus prevention
183 @listenOnce('blur', utils.suppressEvent)
187 @listen('blur', (event) =>
188 target = event.originalTarget
190 # If a text input is blurred immediately before the document loses focus,
191 # it most likely means that the user switched tab, for example by pressing
192 # `<c-tab>`, or switched to another window, while the text input was
193 # focused. In this case, when switching back to that tab, the text input
194 # will, and should, be re-focused (because it was focused when you left
195 # the tab). This case is kept track of so that the autofocus prevention
197 if utils.isTypingElement(target) or utils.isContentEditable(target)
198 utils.nextTick(@vim.content, =>
199 @vim.state.shouldRefocus = not @vim.content.document.hasFocus()
203 module.exports = FrameEventManager