1 # This file defines the `vim` API, available to all modes and commands. There is
2 # one `Vim` instance for each tab. Most importantly, it provides access to the
3 # owning Firefox window and the current mode, and allows you to change mode.
4 # `vim` objects are exposed by the config file API. Underscored names are
5 # private and should not be used by API consumers.
7 messageManager = require('./message-manager')
8 ScrollableElements = require('./scrollable-elements')
9 statusPanel = require('./status-panel')
10 utils = require('./utils')
12 ChromeWindow = Ci.nsIDOMChromeWindow
15 constructor: (browser, @_parent) ->
18 @_setBrowser(browser, {addListeners: false})
23 Object.defineProperty(this, 'options', {
24 get: => @_parent.options
29 @_onLocationChange(@browser.currentURI.spec)
31 focusType = utils.getFocusType(utils.getActiveElement(@window))
32 @_setFocusType(focusType)
35 # Require the subset of the options needed to be listed explicitly (as
36 # opposed to sending _all_ options) for performance. Each option access
37 # might trigger an optionOverride.
38 @_listen('options', ({prefs}) =>
41 options[pref] = @options[pref]
45 @_listen('vimMethod', ({method, args = []}, callback = null) =>
46 result = this[method](args...)
50 @_listen('vimMethodSync', ({method, args = []}) =>
51 return this[method](args...)
54 @_listen('locationChange', @_onLocationChange.bind(this))
56 @_listen('frameCanReceiveEvents', (value) =>
57 @_state.frameCanReceiveEvents = value
60 @_listen('focusType', (focusType) =>
61 # If the focus moves from a web page element to a browser UI element, the
62 # focus and blur events happen in the expected order, but the message from
63 # the frame script arrives too late. Therefore, check that the currently
64 # active element isn’t a browser UI element first.
65 unless @_isUIElement(utils.getActiveElement(@window))
66 @_setFocusType(focusType)
69 _setBrowser: (@browser, {addListeners = true} = {}) ->
70 @window = @browser.ownerGlobal
71 @_messageManager = @browser.messageManager
73 @_statusPanel?.remove()
74 @_statusPanel = statusPanel.injectStatusPanel(@browser)
76 @_addListeners() if addListeners
80 frameCanReceiveEvents: false
81 scrollableElements: new ScrollableElements(@window)
82 lastNotification: null
83 persistentNotification: null
85 enteredKeysTimeout: null
88 _isBlacklisted: (url) -> @options.blacklist.some((regex) -> regex.test(url))
91 return not @_state.frameCanReceiveEvents or
92 @_isUIElement(event.originalTarget)
94 _isUIElement: (element) ->
95 return element.ownerGlobal instanceof ChromeWindow and
96 element != @window.gBrowser.selectedBrowser
98 # `args...` is passed to the mode's `onEnter` method.
99 _enterMode: (mode, args...) ->
100 return if @mode == mode
102 unless utils.has(@_parent.modes, mode)
103 modes = Object.keys(@_parent.modes).join(', ')
105 "VimFx: Unknown mode. Available modes are: #{modes}. Got: #{mode}"
108 @_call('onLeave') if @mode?
110 @_call('onEnter', null, args...)
111 @_parent.emit('modeChange', {vim: this})
112 @_send('modeChange', {mode})
114 _consumeKeyEvent: (event) ->
115 match = @_parent.consumeKeyEvent(event, this)
116 return match if not match or typeof match == 'boolean'
118 if @options.notify_entered_keys
119 if match.type in ['none', 'full'] or match.likelyConflict
122 @_pushEnteredKey(match.keyStr)
128 _onInput: (match, event) ->
129 suppress = @_call('onInput', {event, count: match.count}, match)
132 _onLocationChange: (url) ->
134 when @_isBlacklisted(url)
135 @_enterMode('ignore', {type: 'blacklist'})
136 when (@mode == 'ignore' and @_storage.ignore.type == 'blacklist') or
138 @_enterMode('normal')
139 @_parent.emit('locationChange', {vim: this, location: new @window.URL(url)})
141 _call: (method, data = {}, extraArgs...) ->
142 args = Object.assign({vim: this, storage: @_storage[@mode] ?= {}}, data)
143 currentMode = @_parent.modes[@mode]
144 return currentMode[method].call(currentMode, args, extraArgs...)
146 _run: (name, data = {}, callback = null) ->
147 @_send('runCommand', {name, data}, callback)
149 _messageManagerOptions: (options) ->
150 return Object.assign({
151 messageManager: @_messageManager
154 _listen: (name, listener, options = {}) ->
155 messageManager.listen(name, listener, @_messageManagerOptions(options))
157 _listenOnce: (name, listener, options = {}) ->
158 messageManager.listenOnce(name, listener, @_messageManagerOptions(options))
160 _send: (name, data, callback = null, options = {}) ->
161 messageManager.send(name, data, callback, @_messageManagerOptions(options))
164 @_state.lastNotification = message
165 @_parent.emit('notification', {vim: this, message})
166 if @options.notifications_enabled
167 @_statusPanel.setAttribute('label', message)
168 @_statusPanel.removeAttribute('inactive')
170 _notifyPersistent: (message) ->
171 @_state.persistentNotification = message
174 _refreshPersistentNotification: ->
175 @notify(@_state.persistentNotification) if @_state.persistentNotification
178 @_parent.emit('hideNotification', {vim: this})
179 @_statusPanel.setAttribute('inactive', 'true')
180 @_state.lastNotification = null
181 @_state.persistentNotification = null
183 _clearEnteredKeys: ->
184 @_clearEnteredKeysTimeout()
185 return unless @_state.enteredKeys.length > 0
187 @_state.enteredKeys = []
188 if @_state.persistentNotification
189 @notify(@_state.persistentNotification)
193 _pushEnteredKey: (keyStr) ->
194 @_state.enteredKeys.push(keyStr)
195 @_clearEnteredKeysTimeout()
196 @notify(@_state.enteredKeys.join(''))
197 clear = @_clearEnteredKeys.bind(this)
198 @_state.enteredKeysTimeout =
199 @window.setTimeout((-> clear()), @options.timeout)
201 _clearEnteredKeysTimeout: ->
202 if @_state.enteredKeysTimeout?
203 @window.clearTimeout(@_state.enteredKeysTimeout)
204 @_state.enteredKeysTimeout = null
206 _modal: (type, args, callback = null) ->
207 @_run('modal', {type, args}, callback)
209 markPageInteraction: (value = null) -> @_send('markPageInteraction', value)
211 _focusMarkerElement: (elementIndex, options = {}) ->
212 # If you, for example, focus the location bar, unfocus it by pressing
213 # `<esc>` and then try to focus a link or text input in a web page the focus
214 # won’t work unless `@browser` is focused first.
216 browserOffset = @_getBrowserOffset()
217 @_run('focus_marker_element', {elementIndex, browserOffset, options})
219 _setFocusType: (focusType) ->
220 return if focusType == @focusType
221 @focusType = focusType
223 when @focusType == 'ignore'
224 @_enterMode('ignore', {type: 'focusType'})
225 when @mode == 'ignore' and @_storage.ignore.type == 'focusType'
226 @_enterMode('normal')
227 when @mode == 'normal' and @focusType == 'findbar'
229 when @mode == 'find' and @focusType != 'findbar'
230 @_enterMode('normal')
231 @_parent.emit('focusTypeChange', {vim: this})
233 _getBrowserOffset: ->
234 browserRect = @browser.getBoundingClientRect()
236 x: @window.screenX + browserRect.left
237 y: @window.screenY + browserRect.top