]> git.gir.st - VimFx.git/blob - extension/lib/vim.coffee
Change license to MIT
[VimFx.git] / extension / lib / vim.coffee
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.
6
7 messageManager = require('./message-manager')
8 ScrollableElements = require('./scrollable-elements')
9 statusPanel = require('./status-panel')
10 utils = require('./utils')
11
12 ChromeWindow = Ci.nsIDOMChromeWindow
13
14 class Vim
15 constructor: (browser, @_parent) ->
16 @mode = undefined
17 @focusType = 'none'
18 @_setBrowser(browser, {addListeners: false})
19 @_storage = {}
20
21 @_resetState()
22
23 Object.defineProperty(this, 'options', {
24 get: => @_parent.options
25 enumerable: true
26 })
27
28 _start: ->
29 @_onLocationChange(@browser.currentURI.spec)
30 @_addListeners()
31 focusType = utils.getFocusType(utils.getActiveElement(@window))
32 @_setFocusType(focusType)
33
34 _addListeners: ->
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}) =>
39 options = {}
40 for pref in prefs
41 options[pref] = @options[pref]
42 return options
43 )
44
45 @_listen('vimMethod', ({method, args = []}, callback = null) =>
46 result = this[method](args...)
47 callback?(result)
48 )
49
50 @_listen('vimMethodSync', ({method, args = []}) =>
51 return this[method](args...)
52 )
53
54 @_listen('locationChange', @_onLocationChange.bind(this))
55
56 @_listen('frameCanReceiveEvents', (value) =>
57 @_state.frameCanReceiveEvents = value
58 )
59
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)
67 )
68
69 _setBrowser: (@browser, {addListeners = true} = {}) ->
70 @window = @browser.ownerGlobal
71 @_messageManager = @browser.messageManager
72
73 @_statusPanel?.remove()
74 @_statusPanel = statusPanel.injectStatusPanel(@browser)
75
76 @_addListeners() if addListeners
77
78 _resetState: ->
79 @_state = {
80 frameCanReceiveEvents: false
81 scrollableElements: new ScrollableElements(@window)
82 lastNotification: null
83 persistentNotification: null
84 enteredKeys: []
85 enteredKeysTimeout: null
86 }
87
88 _isBlacklisted: (url) -> @options.blacklist.some((regex) -> regex.test(url))
89
90 isUIEvent: (event) ->
91 return not @_state.frameCanReceiveEvents or
92 @_isUIElement(event.originalTarget)
93
94 _isUIElement: (element) ->
95 # TODO: The `element.ownerGlobal` check will be redundant when
96 # non-multi-process is removed from Firefox.
97 return element.ownerGlobal instanceof ChromeWindow and
98 element != @window.gBrowser.selectedBrowser
99
100 # `args...` is passed to the mode's `onEnter` method.
101 _enterMode: (mode, args...) ->
102 return if @mode == mode
103
104 unless utils.has(@_parent.modes, mode)
105 modes = Object.keys(@_parent.modes).join(', ')
106 throw new Error(
107 "VimFx: Unknown mode. Available modes are: #{modes}. Got: #{mode}"
108 )
109
110 @_call('onLeave') if @mode?
111 @mode = mode
112 @_call('onEnter', null, args...)
113 @_parent.emit('modeChange', {vim: this})
114 @_send('modeChange', {mode})
115
116 _consumeKeyEvent: (event) ->
117 match = @_parent.consumeKeyEvent(event, this)
118 return match if not match or typeof match == 'boolean'
119
120 if @options.notify_entered_keys
121 if match.type in ['none', 'full'] or match.likelyConflict
122 @_clearEnteredKeys()
123 else
124 @_pushEnteredKey(match.keyStr)
125 else
126 @hideNotification()
127
128 return match
129
130 _onInput: (match, event) ->
131 suppress = @_call('onInput', {event, count: match.count}, match)
132 return suppress
133
134 _onLocationChange: (url) ->
135 switch
136 when @_isBlacklisted(url)
137 @_enterMode('ignore', {type: 'blacklist'})
138 when (@mode == 'ignore' and @_storage.ignore.type == 'blacklist') or
139 not @mode
140 @_enterMode('normal')
141 @_parent.emit('locationChange', {vim: this, location: new @window.URL(url)})
142
143 _call: (method, data = {}, extraArgs...) ->
144 args = Object.assign({vim: this, storage: @_storage[@mode] ?= {}}, data)
145 currentMode = @_parent.modes[@mode]
146 return currentMode[method].call(currentMode, args, extraArgs...)
147
148 _run: (name, data = {}, callback = null) ->
149 @_send('runCommand', {name, data}, callback)
150
151 _messageManagerOptions: (options) ->
152 return Object.assign({
153 messageManager: @_messageManager
154 }, options)
155
156 _listen: (name, listener, options = {}) ->
157 messageManager.listen(name, listener, @_messageManagerOptions(options))
158
159 _listenOnce: (name, listener, options = {}) ->
160 messageManager.listenOnce(name, listener, @_messageManagerOptions(options))
161
162 _send: (name, data, callback = null, options = {}) ->
163 messageManager.send(name, data, callback, @_messageManagerOptions(options))
164
165 notify: (message) ->
166 @_state.lastNotification = message
167 @_parent.emit('notification', {vim: this, message})
168 if @options.notifications_enabled
169 @_statusPanel.setAttribute('label', message)
170 @_statusPanel.removeAttribute('inactive')
171
172 _notifyPersistent: (message) ->
173 @_state.persistentNotification = message
174 @notify(message)
175
176 _refreshPersistentNotification: ->
177 @notify(@_state.persistentNotification) if @_state.persistentNotification
178
179 hideNotification: ->
180 @_parent.emit('hideNotification', {vim: this})
181 @_statusPanel.setAttribute('inactive', 'true')
182 @_state.lastNotification = null
183 @_state.persistentNotification = null
184
185 _clearEnteredKeys: ->
186 @_clearEnteredKeysTimeout()
187 return unless @_state.enteredKeys.length > 0
188
189 @_state.enteredKeys = []
190 if @_state.persistentNotification
191 @notify(@_state.persistentNotification)
192 else
193 @hideNotification()
194
195 _pushEnteredKey: (keyStr) ->
196 @_state.enteredKeys.push(keyStr)
197 @_clearEnteredKeysTimeout()
198 @notify(@_state.enteredKeys.join(''))
199 clear = @_clearEnteredKeys.bind(this)
200 @_state.enteredKeysTimeout =
201 @window.setTimeout((-> clear()), @options.timeout)
202
203 _clearEnteredKeysTimeout: ->
204 if @_state.enteredKeysTimeout?
205 @window.clearTimeout(@_state.enteredKeysTimeout)
206 @_state.enteredKeysTimeout = null
207
208 _modal: (type, args, callback = null) ->
209 @_run('modal', {type, args}, callback)
210
211 markPageInteraction: (value = null) -> @_send('markPageInteraction', value)
212
213 _focusMarkerElement: (elementIndex, options = {}) ->
214 # If you, for example, focus the location bar, unfocus it by pressing
215 # `<esc>` and then try to focus a link or text input in a web page the focus
216 # won’t work unless `@browser` is focused first.
217 @browser.focus()
218 browserOffset = @_getBrowserOffset()
219 @_run('focus_marker_element', {elementIndex, browserOffset, options})
220
221 _setFocusType: (focusType) ->
222 return if focusType == @focusType
223 @focusType = focusType
224 switch
225 when @focusType == 'ignore'
226 @_enterMode('ignore', {type: 'focusType'})
227 when @mode == 'ignore' and @_storage.ignore.type == 'focusType'
228 @_enterMode('normal')
229 when @mode == 'normal' and @focusType == 'findbar'
230 @_enterMode('find')
231 when @mode == 'find' and @focusType != 'findbar'
232 @_enterMode('normal')
233 @_parent.emit('focusTypeChange', {vim: this})
234
235 _getBrowserOffset: ->
236 browserRect = @browser.getBoundingClientRect()
237 return {
238 x: @window.screenX + browserRect.left
239 y: @window.screenY + browserRect.top
240 }
241
242 module.exports = Vim
Imprint / Impressum