]> git.gir.st - VimFx.git/blob - extension/lib/options.coffee
Port settings page to a modern format
[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('vimfx-options-displayed', observer)
18 utils.observe('vimfx-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 outer = @document.createElement('div')
47 outer.classList.add('setting')
48 outer.classList.add('first-row') if attributes['first-row']
49 outer.classList.add(attributes.class)
50 outer.setAttribute('data-pref', attributes.pref)
51 outer.setAttribute('data-type', attributes.type)
52 label = @document.createElement('label')
53 title = @document.createElement('span')
54 title.className = 'title'
55 title.innerText = attributes.title
56 label.appendChild(title)
57 help = @document.createElement('span')
58 help.className = 'desc'
59 help.innerText = attributes.desc or ''
60 input = @document.createElement('input')
61 _this = this
62 switch attributes.type
63 when 'bool'
64 input.type = 'checkbox'
65 input.checked = prefs.root.get(attributes.pref)
66 input.addEventListener('change', ->
67 prefs.root.set(attributes.pref, input.checked)
68 )
69 prefobserver = prefs.root.observe(attributes.pref, ->
70 input.checked = prefs.root.get(attributes.pref)
71 )
72 @document.defaultView.addEventListener('unload', ->
73 prefs?.root.unobserve(attributes.pref, prefobserver)
74 )
75 when 'integer'
76 input.type = 'number'
77 input.value = prefs.root.get(attributes.pref)
78 input.addEventListener('input', ->
79 prefs.root.set(attributes.pref, parseInt(input.value, 10))
80 )
81 prefobserver = prefs.root.observe(attributes.pref, ->
82 input.value = prefs.root.get(attributes.pref)
83 )
84 @document.defaultView.addEventListener('unload', ->
85 prefs?.root.unobserve(attributes.pref, prefobserver)
86 )
87 when 'string'
88 input.type = 'text'
89 input.value = prefs.root.get(attributes.pref)
90 input.addEventListener('input', ->
91 prefs.root.set(attributes.pref, input.value)
92 if outer.classList.contains('is-shortcut')
93 _this.refreshShortcutErrors()
94 )
95 prefobserver = prefs.root.observe(attributes.pref, ->
96 input.value = prefs.root.get(attributes.pref)
97 )
98 @document.defaultView.addEventListener('unload', ->
99 prefs?.root.unobserve(attributes.pref, prefobserver)
100 )
101 when 'control' # injectHeader special case
102 control = @document.createElement('span')
103 control.className = 'control'
104 # can't use <label> when control has multiple buttons:
105 label = @document.createElement('span')
106 label.appendChild(title)
107 label.appendChild(control)
108 label.appendChild(input) if attributes.pref # some nodes are just headlines
109 outer.appendChild(label)
110 outer.appendChild(help) # needed even if empty, as validator puts error here
111 @container.appendChild(outer)
112 return outer
113
114 observe: (@document, topic, addonId) ->
115 switch topic
116 when 'vimfx-options-displayed'
117 @init()
118 when 'vimfx-options-hidden'
119 @destroy()
120
121 init: ->
122 @container = @document.getElementById('detail-rows')
123 @injectSettings()
124
125 destroy: ->
126 @unlisten()
127
128 # VimFx specific observer.
129 class Observer extends BaseObserver
130 constructor: (@vimfx) ->
131 super({id: @vimfx.id})
132
133 injectSettings: ->
134 @injectHeader()
135 @injectOptions()
136 @injectShortcuts()
137 @setupKeybindings()
138
139 if @vimfx.goToCommand
140 utils.nextTick(@document.ownerGlobal, =>
141 {pref} = @vimfx.goToCommand
142 setting = @container.querySelector("[data-pref='#{pref}']")
143 setting.scrollIntoView()
144 setting.querySelector('input').select()
145 @vimfx.goToCommand = null
146 )
147
148 injectHeader: ->
149 setting = @appendSetting({
150 type: 'control'
151 title: translate('prefs.instructions.title')
152 desc: translate(
153 'prefs.instructions.desc',
154 @vimfx.options['options.key.quote'],
155 @vimfx.options['options.key.insert_default'],
156 @vimfx.options['options.key.reset_default'],
157 '<c-z>'
158 )
159 'first-row': 'true'
160 })
161 setting.id = 'header'
162
163 href = "#{@vimfx.info.homepageURL}/tree/master/documentation#contents"
164 docsLink = @document.createElement('a')
165 docsLink.innerText = translate('prefs.documentation')
166 utils.setAttributes(docsLink, {
167 href
168 target: '_blank'
169 })
170 setting.querySelector('.control').appendChild(docsLink)
171
172 for key, fn of BUTTONS
173 button = @document.createElement('button')
174 button.innerText = translate("prefs.#{key}.label")
175 button.onclick = runWithVim.bind(null, @vimfx, fn)
176 setting.querySelector('.control').appendChild(button)
177
178 return
179
180 injectOptions: ->
181 for key, value of defaults.options
182 @appendSetting({
183 pref: "#{defaults.BRANCH}#{key}"
184 type: @type(value)
185 title: translate("pref.#{key}.title")
186 desc: translate("pref.#{key}.desc")
187 })
188 return
189
190 injectShortcuts: ->
191 for mode in @vimfx.getGroupedCommands()
192 @appendSetting({
193 type: 'control'
194 title: mode.name
195 'first-row': 'true'
196 })
197
198 for category in mode.categories
199 if category.name
200 @appendSetting({
201 type: 'control'
202 title: category.name
203 'first-row': 'true'
204 })
205
206 for {command} in category.commands
207 @appendSetting({
208 pref: command.pref
209 type: 'string'
210 title: command.description
211 desc: @generateErrorMessage(command.pref)
212 class: 'is-shortcut'
213 })
214
215 return
216
217 generateErrorMessage: (pref) ->
218 commandErrors = @vimfx.errors[pref] ? []
219 return commandErrors.map(({id, context, subject}) ->
220 return translate("error.#{id}", context ? subject, subject)
221 ).join('\n')
222
223 setupKeybindings: ->
224 # Note that `setting = event.originalTarget` does _not_ return the correct
225 # element in these listeners!
226 quote = false
227 @listen(@container, 'keydown', (event) =>
228 input = event.target
229 isString = (input.type == 'text')
230
231 setting = input.closest('.setting')
232 pref = setting.getAttribute('data-pref')
233 keyString = @vimfx.stringifyKeyEvent(event)
234
235 # Some shortcuts only make sense for string settings. We still match
236 # those shortcuts and suppress the default behavior for _all_ types of
237 # settings for consistency. For example, pressing <c-d> in a number input
238 # (which looks like a text input) would otherwise bookmark the page, and
239 # <c-q> would close the window!
240 switch
241 when not keyString
242 return
243 when quote
244 break unless isString
245 utils.insertText(input, keyString)
246 prefs.root.set(pref, input.value)
247 quote = false
248 when keyString == @vimfx.options['options.key.quote']
249 break unless isString
250 quote = true
251 # Override `<force>` commands (such as `<escape>` and `<tab>`).
252 return unless vim = @vimfx.getCurrentVim(utils.getCurrentWindow())
253 @vimfx.modes.normal.commands.quote.run({vim, count: 1})
254 when keyString == @vimfx.options['options.key.insert_default']
255 break unless isString
256 utils.insertText(input, prefs.root.default.get(pref))
257 prefs.root.set(pref, input.value)
258 when keyString == @vimfx.options['options.key.reset_default']
259 prefs.root.set(pref, null)
260 else
261 return
262
263 event.preventDefault()
264 @refreshShortcutErrors()
265 )
266 @listen(@container, 'blur', -> quote = false)
267
268 refreshShortcutErrors: ->
269 for setting in @container.getElementsByClassName('is-shortcut')
270 setting.querySelector('.desc').innerText =
271 @generateErrorMessage(setting.getAttribute('data-pref'))
272 return
273
274 resetAllPrefs = (vim) ->
275 vim._modal('confirm', [translate('prefs.reset.enter')], (ok) ->
276 return unless ok
277 prefsBulk.resetAll()
278 vim.notify(translate('prefs.reset.success'))
279 )
280
281 exportAllPrefs = (vim) ->
282 exported = prefsBulk.exportAll()
283 if Object.keys(exported).length == 0
284 vim.notify(translate('prefs.export.none'))
285 else
286 utils.writeToClipboard(JSON.stringify(exported, null, 2))
287 vim.notify(translate('prefs.export.success'))
288
289 importExportedPrefs = (vim) ->
290 vim._modal('prompt', [translate('prefs.import.enter')], (input) ->
291 return if input == null or input.trim() == ''
292 result = prefsBulk.importExported(input.trim())
293 if result.errors.length == 0
294 vim.notify(translate('prefs.import.success'))
295 else
296 vim._modal('alert', [prefsBulk.createImportErrorReport(result)])
297 )
298
299 runWithVim = (vimfx, fn) ->
300 return unless vim = vimfx.getCurrentVim(utils.getCurrentWindow())
301 fn(vim)
302
303 BUTTONS = {
304 export: exportAllPrefs
305 import: importExportedPrefs
306 reset: resetAllPrefs
307 }
308
309 module.exports = {
310 observe
311 }
Imprint / Impressum