]> git.gir.st - VimFx.git/blob - extension/lib/vim.coffee
VimFx is now officially dead
[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 return element.ownerGlobal instanceof ChromeWindow and
96 element != @window.gBrowser.selectedBrowser
97
98 # `args...` is passed to the mode's `onEnter` method.
99 _enterMode: (mode, args...) ->
100 return if @mode == mode
101
102 unless utils.has(@_parent.modes, mode)
103 modes = Object.keys(@_parent.modes).join(', ')
104 throw new Error(
105 "VimFx: Unknown mode. Available modes are: #{modes}. Got: #{mode}"
106 )
107
108 @_call('onLeave') if @mode?
109 @mode = mode
110 @_call('onEnter', null, args...)
111 @_parent.emit('modeChange', {vim: this})
112 @_send('modeChange', {mode})
113
114 _consumeKeyEvent: (event) ->
115 match = @_parent.consumeKeyEvent(event, this)
116 return match if not match or typeof match == 'boolean'
117
118 if @options.notify_entered_keys
119 if match.type in ['none', 'full'] or match.likelyConflict
120 @_clearEnteredKeys()
121 else
122 @_pushEnteredKey(match.keyStr)
123 else
124 @hideNotification()
125
126 return match
127
128 _onInput: (match, event) ->
129 suppress = @_call('onInput', {event, count: match.count}, match)
130 return suppress
131
132 _onLocationChange: (url) ->
133 switch
134 when @_isBlacklisted(url)
135 @_enterMode('ignore', {type: 'blacklist'})
136 when (@mode == 'ignore' and @_storage.ignore.type == 'blacklist') or
137 not @mode
138 @_enterMode('normal')
139 @_parent.emit('locationChange', {vim: this, location: new @window.URL(url)})
140
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...)
145
146 _run: (name, data = {}, callback = null) ->
147 @_send('runCommand', {name, data}, callback)
148
149 _messageManagerOptions: (options) ->
150 return Object.assign({
151 messageManager: @_messageManager
152 }, options)
153
154 _listen: (name, listener, options = {}) ->
155 messageManager.listen(name, listener, @_messageManagerOptions(options))
156
157 _listenOnce: (name, listener, options = {}) ->
158 messageManager.listenOnce(name, listener, @_messageManagerOptions(options))
159
160 _send: (name, data, callback = null, options = {}) ->
161 messageManager.send(name, data, callback, @_messageManagerOptions(options))
162
163 notify: (message) ->
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')
169
170 _notifyPersistent: (message) ->
171 @_state.persistentNotification = message
172 @notify(message)
173
174 _refreshPersistentNotification: ->
175 @notify(@_state.persistentNotification) if @_state.persistentNotification
176
177 hideNotification: ->
178 @_parent.emit('hideNotification', {vim: this})
179 @_statusPanel.setAttribute('inactive', 'true')
180 @_state.lastNotification = null
181 @_state.persistentNotification = null
182
183 _clearEnteredKeys: ->
184 @_clearEnteredKeysTimeout()
185 return unless @_state.enteredKeys.length > 0
186
187 @_state.enteredKeys = []
188 if @_state.persistentNotification
189 @notify(@_state.persistentNotification)
190 else
191 @hideNotification()
192
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)
200
201 _clearEnteredKeysTimeout: ->
202 if @_state.enteredKeysTimeout?
203 @window.clearTimeout(@_state.enteredKeysTimeout)
204 @_state.enteredKeysTimeout = null
205
206 _modal: (type, args, callback = null) ->
207 @_run('modal', {type, args}, callback)
208
209 markPageInteraction: (value = null) -> @_send('markPageInteraction', value)
210
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.
215 @browser.focus()
216 browserOffset = @_getBrowserOffset()
217 @_run('focus_marker_element', {elementIndex, browserOffset, options})
218
219 _setFocusType: (focusType) ->
220 return if focusType == @focusType
221 @focusType = focusType
222 switch
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'
228 @_enterMode('find')
229 when @mode == 'find' and @focusType != 'findbar'
230 @_enterMode('normal')
231 @_parent.emit('focusTypeChange', {vim: this})
232
233 _getBrowserOffset: ->
234 browserRect = @browser.getBoundingClientRect()
235 return {
236 x: @window.screenX + browserRect.left
237 y: @window.screenY + browserRect.top
238 }
239
240 module.exports = Vim
Imprint / Impressum