]> git.gir.st - VimFx.git/blob - extension/lib/events-frame.coffee
Fix scrolling issues on Gmail
[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 messageManager = require('./message-manager')
23 utils = require('./utils')
24
25 class FrameEventManager
26 constructor: (@vim) ->
27 @numFocusToSuppress = 0
28
29 MINIMUM_SCROLLABLE_ELEMENT_AREA: 25
30
31 listen: utils.listen.bind(null, FRAME_SCRIPT_ENVIRONMENT)
32 listenOnce: utils.listenOnce.bind(null, FRAME_SCRIPT_ENVIRONMENT)
33
34 addListeners: ->
35 # If the page already was loaded when VimFx was initialized, send the
36 # 'DOMWindowCreated' message straight away.
37 if @vim.content.document.readyState in ['interactive', 'complete']
38 messageManager.send('DOMWindowCreated')
39 else
40 @listen('DOMWindowCreated', (event) ->
41 messageManager.send('DOMWindowCreated')
42 )
43
44 @listen('readystatechange', (event) =>
45 target = event.originalTarget
46
47 # If the topmost document starts loading, it means that we have navigated
48 # to a new page or refreshed the page.
49 if target == @vim.content.document and target.readyState == 'interactive'
50 messageManager.send('locationChange', @vim.content.location.href)
51 )
52
53 @listen('pagehide', (event) =>
54 target = event.originalTarget
55
56 if target == @vim.content.document
57 @vim.resetState()
58 else
59 # If the target isn’t the topmost document, it means that a frame has
60 # changed: It could have been removed or its `src` attribute could have
61 # been changed. Any scrollable elements in the frame then need to be
62 # filtered out. If the frame contains other frames, 'pagehide' events
63 # have already been fired for them. On some sites, such as Gmail, some
64 # elements might be dead at this point.
65 @vim.state.scrollableElements.reject((element) ->
66 return Cu.isDeadWrapper(element) or element.ownerDocument == target
67 )
68 )
69
70 @listen('click', (event) =>
71 if @vim.mode == 'hints' and event.isTrusted
72 messageManager.send('enterMode', {mode: 'normal'})
73 )
74
75 @listen('overflow', (event) =>
76 target = event.originalTarget
77
78 return unless computedStyle = @vim.content.getComputedStyle(target)
79 unless (computedStyle.getPropertyValue('overflow-y') == 'hidden' and
80 computedStyle.getPropertyValue('overflow-x') == 'hidden') or
81 # There’s no need to track elements so small that they don’t even
82 # fit the scrollbars. For example, Gmail has lots of tiny
83 # overflowing iframes. Filter those out.
84 utils.area(target) < @MINIMUM_SCROLLABLE_ELEMENT_AREA
85 @vim.state.scrollableElements.add(target)
86 )
87
88 @listen('underflow', (event) =>
89 target = event.originalTarget
90
91 # On some pages, such as Gmail, 'underflow' events may occur for elements
92 # that are actually still scrollable! If so, keep the element.
93 unless target.scrollHeight > target.clientHeight or
94 target.scrollWidth > target.clientWidth
95 @vim.state.scrollableElements.delete(target)
96 )
97
98 @listen('keydown', (event) =>
99 suppress = @vim.onInput(event)
100
101 # This also suppresses the 'keypress' and 'keyup' events. (Yes, in frame
102 # scripts, suppressing the 'keydown' events does seem to even suppress
103 # the 'keyup' event!)
104 utils.suppressEvent(event) if suppress
105
106 # From this line on, the rest of the code in `addListeners` is more or
107 # less devoted to autofocus prevention. When enabled, focus events that
108 # occur before the user has interacted with page are prevented.
109 #
110 # If this keydown event wasn’t suppressed (`not suppress`), it’s an
111 # obvious interaction with the page. If it _was_ suppressed, though, it’s
112 # an interaction depending on the command triggered; if it calls
113 # `vim.markPageInteraction()` or not.
114 @vim.markPageInteraction() unless suppress
115 )
116
117 @listen('keydown', ((event) ->
118 suppress = messageManager.get('lateKeydown', {
119 defaultPrevented: event.defaultPrevented
120 })
121 utils.suppressEvent(event) if suppress
122 ), false)
123
124 # Clicks are always counted as page interaction. Listen for 'mousedown'
125 # instead of 'click' to mark the interaction as soon as possible.
126 @listen('mousedown', @vim.markPageInteraction.bind(@vim))
127
128 messageManager.listen('browserRefocus', =>
129 # Suppress the next two focus events (for `document` and `window`; see
130 # `blurActiveBrowserElement`).
131 @numFocusToSuppress = 2
132 )
133
134 @listen('focus', (event) =>
135 target = event.originalTarget
136
137 if @numFocusToSuppress > 0
138 utils.suppressEvent(event)
139 @numFocusToSuppress--
140 return
141
142 # Reset `hasInteraction` when (re-)selecting a tab, or coming back from
143 # another window, in order to prevent the common “automatically re-focus
144 # when switching back to the tab” behaviour many sites have, unless a text
145 # input _should_ be re-focused when coming back to the tab (see the 'blur'
146 # event below).
147 if target == @vim.content.document
148 if @vim.state.shouldRefocus
149 @vim.state.hasInteraction = true
150 @vim.state.shouldRefocus = false
151 else
152 @vim.state.hasInteraction = false
153 return
154
155 # Save the last focused text input regardless of whether that input might
156 # be blurred because of autofocus prevention.
157 if utils.isTextInputElement(target)
158 @vim.state.lastFocusedTextInput = target
159
160 focusManager = Cc['@mozilla.org/focus-manager;1']
161 .getService(Ci.nsIFocusManager)
162
163 # When moving a tab to another window, there is a short period of time
164 # when there’s no listener for this call.
165 return unless options = @vim.options(
166 ['prevent_autofocus', 'prevent_autofocus_modes']
167 )
168
169 # Blur the focus target, if autofocus prevention is enabled…
170 if options.prevent_autofocus and
171 @vim.mode in options.prevent_autofocus_modes and
172 # …and the user has interacted with the page…
173 not @vim.state.hasInteraction and
174 # …and the event is programmatic (not caused by clicks or keypresses)…
175 focusManager.getLastFocusMethod(null) == 0 and
176 # …and the target may steal most keystrokes.
177 (utils.isTypingElement(target) or utils.isContentEditable(target))
178 # Some sites (such as icloud.com) re-focuses inputs if they are blurred,
179 # causing an infinite loop of autofocus prevention and re-focusing.
180 # Therefore, blur events that happen just after an autofocus prevention
181 # are suppressed.
182 @listenOnce('blur', utils.suppressEvent)
183 target.blur()
184 )
185
186 @listen('blur', (event) =>
187 target = event.originalTarget
188
189 # If a text input is blurred immediately before the document loses focus,
190 # it most likely means that the user switched tab, for example by pressing
191 # `<c-tab>`, or switched to another window, while the text input was
192 # focused. In this case, when switching back to that tab, the text input
193 # will, and should, be re-focused (because it was focused when you left
194 # the tab). This case is kept track of so that the autofocus prevention
195 # does not catch it.
196 if utils.isTypingElement(target) or utils.isContentEditable(target)
197 utils.nextTick(@vim.content, =>
198 @vim.state.shouldRefocus = not @vim.content.document.hasFocus()
199 )
200 )
201
202 module.exports = FrameEventManager
Imprint / Impressum