]> git.gir.st - VimFx.git/blob - extension/lib/events-frame.coffee
Rework `gi` handling of `<tab>`
[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
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 # When the page starts loading `.readyState` changes to 'interactive'.
48 return unless target.readyState == 'interactive'
49
50 # If the topmost document starts loading, it means that we have navigated
51 # to a new page or refreshed the page.
52 if target == @vim.content.document
53 @vim.resetState()
54 messageManager.send('locationChange', @vim.content.location.href)
55 else
56 # If the target isn’t the topmost document, it means that a frame has
57 # started loading. Some sites change the `src` attribute of `<iframe>`s
58 # dynamically. Any scrollable elements for the old frame `src` then
59 # become dead and need to be filtered out.
60 @vim.state.scrollableElements.reject(Cu.isDeadWrapper)
61 )
62
63 @listen('click', (event) =>
64 if @vim.mode == 'hints' and event.isTrusted
65 messageManager.send('enterMode', {mode: 'normal'})
66 )
67
68 @listen('overflow', (event) =>
69 target = event.originalTarget
70 return unless computedStyle = @vim.content.getComputedStyle(target)
71 return if computedStyle.getPropertyValue('overflow') == 'hidden'
72 @vim.state.scrollableElements.add(target)
73 )
74
75 @listen('underflow', (event) =>
76 target = event.originalTarget
77 @vim.state.scrollableElements.delete(target)
78 )
79
80 @listen('keydown', (event) =>
81 suppress = @vim.onInput(event)
82
83 # This also suppresses the 'keypress' and 'keyup' events. (Yes, in frame
84 # scripts, suppressing the 'keydown' events does seem to even suppress
85 # the 'keyup' event!)
86 utils.suppressEvent(event) if suppress
87
88 # From this line on, the rest of the code in `addListeners` is more or
89 # less devoted to autofocus prevention. When enabled, focus events that
90 # occur before the user has interacted with page are prevented.
91 #
92 # If this keydown event wasn’t suppressed (`not suppress`), it’s an
93 # obvious interaction with the page. If it _was_ suppressed, though, it’s
94 # an interaction depending on the command triggered; if it calls
95 # `vim.markPageInteraction()` or not.
96 @vim.markPageInteraction() unless suppress
97 )
98
99 @listen('keydown', ((event) =>
100 suppress = messageManager.get('lateKeydown', {
101 defaultPrevented: event.defaultPrevented
102 })
103
104 if @vim.state.inputs and @vim.mode == 'normal' and not suppress and
105 not event.defaultPrevented
106 # There is no need to take `ignore_keyboard_layout` and `translations`
107 # into account here, since we want to override the _native_ `<tab>`
108 # behavior. Then, `event.key` is the way to go. (Unless the prefs are
109 # customized. YAGNI until requested.)
110 keyStr = notation.stringify(event)
111 options = @vim.options(['focus_previous_key', 'focus_next_key'])
112 direction = switch keyStr
113 when '' then null
114 when options.focus_previous_key then -1
115 when options.focus_next_key then +1
116 else null
117 if direction?
118 suppress = commands.move_focus({@vim, direction})
119
120 utils.suppressEvent(event) if suppress
121 ), false)
122
123 # Clicks are always counted as page interaction. Listen for 'mousedown'
124 # instead of 'click' to mark the interaction as soon as possible.
125 @listen('mousedown', @vim.markPageInteraction.bind(@vim))
126
127 messageManager.listen('browserRefocus', =>
128 # Suppress the next two focus events (for `document` and `window`; see
129 # `blurActiveBrowserElement`).
130 @numFocusToSuppress = 2
131 )
132
133 @listen('focus', (event) =>
134 target = event.originalTarget
135
136 if @numFocusToSuppress > 0
137 utils.suppressEvent(event)
138 @numFocusToSuppress--
139 return
140
141 # Reset `hasInteraction` when (re-)selecting a tab, or coming back from
142 # another window, in order to prevent the common “automatically re-focus
143 # when switching back to the tab” behaviour many sites have, unless a text
144 # input _should_ be re-focused when coming back to the tab (see the 'blur'
145 # event below).
146 if target == @vim.content.document
147 if @vim.state.shouldRefocus
148 @vim.state.hasInteraction = true
149 @vim.state.shouldRefocus = false
150 else
151 @vim.state.hasInteraction = false
152 return
153
154 # Save the last focused text input regardless of whether that input might
155 # be blurred because of autofocus prevention.
156 if utils.isTextInputElement(target)
157 @vim.state.lastFocusedTextInput = target
158
159 focusManager = Cc['@mozilla.org/focus-manager;1']
160 .getService(Ci.nsIFocusManager)
161
162 # When moving a tab to another window, there is a short period of time
163 # when there’s no listener for this call.
164 return unless options = @vim.options(
165 ['prevent_autofocus', 'prevent_autofocus_modes']
166 )
167
168 # Blur the focus target, if autofocus prevention is enabled…
169 if options.prevent_autofocus and
170 @vim.mode in options.prevent_autofocus_modes and
171 # …and the user has interacted with the page…
172 not @vim.state.hasInteraction and
173 # …and the event is programmatic (not caused by clicks or keypresses)…
174 focusManager.getLastFocusMethod(null) == 0 and
175 # …and the target may steal most keystrokes.
176 (utils.isTypingElement(target) or utils.isContentEditable(target))
177 # Some sites (such as icloud.com) re-focuses inputs if they are blurred,
178 # causing an infinite loop of autofocus prevention and re-focusing.
179 # Therefore, blur events that happen just after an autofocus prevention
180 # are suppressed.
181 @listenOnce('blur', utils.suppressEvent)
182 target.blur()
183 )
184
185 @listen('blur', (event) =>
186 target = event.originalTarget
187
188 # If a text input is blurred immediately before the document loses focus,
189 # it most likely means that the user switched tab, for example by pressing
190 # `<c-tab>`, or switched to another window, while the text input was
191 # focused. In this case, when switching back to that tab, the text input
192 # will, and should, be re-focused (because it was focused when you left
193 # the tab). This case is kept track of so that the autofocus prevention
194 # does not catch it.
195 if utils.isTypingElement(target) or utils.isContentEditable(target)
196 utils.nextTick(@vim.content, =>
197 @vim.state.shouldRefocus = not @vim.content.document.hasFocus()
198 )
199 )
200
201 module.exports = FrameEventManager
Imprint / Impressum