]> git.gir.st - VimFx.git/blob - extension/lib/options.coffee
Change license to MIT
[VimFx.git] / extension / lib / options.coffee
1 # This file constructs VimFx’s options UI in the Add-ons Manager.
2
3 defaults = require('./defaults')
4 prefs = require('./prefs')
5 prefsBulk = require('./prefs-bulk')
6 translate = require('./translate')
7 utils = require('./utils')
8
9 TYPE_MAP = {
10 string: 'string'
11 number: 'integer'
12 boolean: 'bool'
13 }
14
15 observe = (options) ->
16 observer = new Observer(options)
17 utils.observe('addon-options-displayed', observer)
18 utils.observe('addon-options-hidden', observer)
19 module.onShutdown(->
20 observer.destroy()
21 )
22
23 # Generalized observer.
24 class BaseObserver
25 constructor: (@options) ->
26 @document = null
27 @container = null
28 @listeners = []
29
30 useCapture: true
31
32 listen: (element, event, action) ->
33 element.addEventListener(event, action, @useCapture)
34 @listeners.push([element, event, action, @useCapture])
35
36 unlisten: ->
37 for [element, event, action, useCapture] in @listeners
38 element.removeEventListener(event, action, useCapture)
39 @listeners.length = 0
40
41 type: (value) -> TYPE_MAP[typeof value]
42
43 injectSettings: ->
44
45 appendSetting: (attributes) ->
46 setting = @document.createElement('setting')
47 utils.setAttributes(setting, attributes)
48 @container.appendChild(setting)
49 return setting
50
51 observe: (@document, topic, addonId) ->
52 return unless addonId == @options.id
53 switch topic
54 when 'addon-options-displayed'
55 @init()
56 when 'addon-options-hidden'
57 @destroy()
58
59 init: ->
60 @container = @document.getElementById('detail-rows')
61 @injectSettings()
62
63 destroy: ->
64 @unlisten()
65
66 # VimFx specific observer.
67 class Observer extends BaseObserver
68 constructor: (@vimfx) ->
69 super({id: @vimfx.id})
70
71 injectSettings: ->
72 @injectHeader()
73 @injectOptions()
74 @injectShortcuts()
75 @setupKeybindings()
76 @setupValidation()
77
78 if @vimfx.goToCommand
79 utils.nextTick(@document.ownerGlobal, =>
80 {pref} = @vimfx.goToCommand
81 setting = @container.querySelector("setting[pref='#{pref}']")
82 setting.scrollIntoView()
83 setting.input.select()
84 @vimfx.goToCommand = null
85 )
86
87 injectHeader: ->
88 setting = @appendSetting({
89 type: 'control'
90 title: translate('prefs.instructions.title')
91 desc: translate(
92 'prefs.instructions.desc',
93 @vimfx.options['options.key.quote'],
94 @vimfx.options['options.key.insert_default'],
95 @vimfx.options['options.key.reset_default'],
96 '<c-z>'
97 )
98 'first-row': 'true'
99 })
100
101 href = "#{@vimfx.info.homepageURL}/tree/master/documentation#contents"
102 docsLink = @document.createElement('label')
103 utils.setAttributes(docsLink, {
104 value: translate('prefs.documentation')
105 href
106 crop: 'end'
107 class: 'text-link'
108 })
109 docsLink.style.MozBoxFlex = '1'
110 setting.appendChild(docsLink)
111
112 for key, fn of BUTTONS
113 button = @document.createElement('button')
114 button.setAttribute('label', translate("prefs.#{key}.label"))
115 button.onclick = runWithVim.bind(null, @vimfx, fn)
116 setting.appendChild(button)
117
118 return
119
120 injectOptions: ->
121 for key, value of defaults.options
122 setting = @appendSetting({
123 pref: "#{defaults.BRANCH}#{key}"
124 type: @type(value)
125 title: translate("pref.#{key}.title")
126 desc: translate("pref.#{key}.desc")
127 })
128 return
129
130 injectShortcuts: ->
131 for mode in @vimfx.getGroupedCommands()
132 @appendSetting({
133 type: 'control'
134 title: mode.name
135 'first-row': 'true'
136 })
137
138 for category in mode.categories
139 if category.name
140 @appendSetting({
141 type: 'control'
142 title: category.name
143 'first-row': 'true'
144 })
145
146 for {command} in category.commands
147 @appendSetting({
148 pref: command.pref
149 type: 'string'
150 title: command.description
151 desc: @generateErrorMessage(command.pref)
152 class: 'is-shortcut'
153 })
154
155 return
156
157 generateErrorMessage: (pref) ->
158 commandErrors = @vimfx.errors[pref] ? []
159 return commandErrors.map(({id, context, subject}) ->
160 return translate("error.#{id}", context ? subject, subject)
161 ).join('\n')
162
163 setupKeybindings: ->
164 # Note that `setting = event.originalTarget` does _not_ return the correct
165 # element in these listeners!
166 quote = false
167 @listen(@container, 'keydown', (event) =>
168 setting = event.target
169 isString = (setting.type == 'string')
170
171 {input, pref} = setting
172 keyString = @vimfx.stringifyKeyEvent(event)
173
174 # Some shortcuts only make sense for string settings. We still match
175 # those shortcuts and suppress the default behavior for _all_ types of
176 # settings for consistency. For example, pressing <c-d> in a number input
177 # (which looks like a text input) would otherwise bookmark the page, and
178 # <c-q> would close the window!
179 switch
180 when not keyString
181 return
182 when quote
183 break unless isString
184 utils.insertText(input, keyString)
185 quote = false
186 when keyString == @vimfx.options['options.key.quote']
187 break unless isString
188 quote = true
189 # Override `<force>` commands (such as `<escape>` and `<tab>`).
190 return unless vim = @vimfx.getCurrentVim(utils.getCurrentWindow())
191 @vimfx.modes.normal.commands.quote.run({vim, count: 1})
192 when keyString == @vimfx.options['options.key.insert_default']
193 break unless isString
194 utils.insertText(input, prefs.root.default.get(pref))
195 when keyString == @vimfx.options['options.key.reset_default']
196 prefs.root.set(pref, null)
197 else
198 return
199
200 event.preventDefault()
201 setting.valueToPreference()
202 @refreshShortcutErrors()
203 )
204 @listen(@container, 'blur', -> quote = false)
205
206 setupValidation: ->
207 @listen(@container, 'input', (event) =>
208 setting = event.target
209 # Disable default behavior of updating the pref of the setting on each
210 # input. Do it on the 'change' event instead (see below), because all
211 # settings are validated and auto-adjusted as soon as the pref changes.
212 event.stopPropagation()
213 if setting.classList.contains('is-shortcut')
214 # However, for the shortcuts we _do_ want live validation, because they
215 # cannot be auto-adjusted. Instead an error message is shown.
216 setting.valueToPreference()
217 @refreshShortcutErrors()
218 )
219
220 @listen(@container, 'change', (event) ->
221 setting = event.target
222 unless setting.classList.contains('is-shortcut')
223 setting.valueToPreference()
224 )
225
226 refreshShortcutErrors: ->
227 for setting in @container.getElementsByClassName('is-shortcut')
228 setting.setAttribute('desc', @generateErrorMessage(setting.pref))
229 return
230
231 resetAllPrefs = (vim) ->
232 vim._modal('confirm', [translate('prefs.reset.enter')], (ok) ->
233 return unless ok
234 prefsBulk.resetAll()
235 vim.notify(translate('prefs.reset.success'))
236 )
237
238 exportAllPrefs = (vim) ->
239 exported = prefsBulk.exportAll()
240 if Object.keys(exported).length == 0
241 vim.notify(translate('prefs.export.none'))
242 else
243 utils.writeToClipboard(JSON.stringify(exported, null, 2))
244 vim.notify(translate('prefs.export.success'))
245
246 importExportedPrefs = (vim) ->
247 vim._modal('prompt', [translate('prefs.import.enter')], (input) ->
248 return if input == null or input.trim() == ''
249 result = prefsBulk.importExported(input.trim())
250 if result.errors.length == 0
251 vim.notify(translate('prefs.import.success'))
252 else
253 vim._modal('alert', [prefsBulk.createImportErrorReport(result)])
254 )
255
256 runWithVim = (vimfx, fn) ->
257 return unless vim = vimfx.getCurrentVim(utils.getCurrentWindow())
258 fn(vim)
259
260 BUTTONS = {
261 export: exportAllPrefs
262 import: importExportedPrefs
263 reset: resetAllPrefs
264 }
265
266 module.exports = {
267 observe
268 }
Imprint / Impressum