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