]> git.gir.st - VimFx.git/blob - extension/lib/options.coffee
Allow to click commands in the help dialog to edit them
[VimFx.git] / extension / lib / options.coffee
1 ###
2 # Copyright Simon Lydell 2015, 2016.
3 #
4 # This file is part of VimFx.
5 #
6 # VimFx is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # VimFx is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
18 ###
19
20 # This file constructs VimFx’s options UI in the Add-ons Manager.
21
22 defaults = require('./defaults')
23 translate = require('./l10n')
24 prefs = require('./prefs')
25 utils = require('./utils')
26
27 TYPE_MAP =
28 string: 'string'
29 number: 'integer'
30 boolean: 'bool'
31
32 observe = (options) ->
33 observer = new Observer(options)
34 utils.observe('addon-options-displayed', observer)
35 utils.observe('addon-options-hidden', observer)
36 module.onShutdown(->
37 observer.destroy()
38 )
39
40 # Generalized observer.
41 class BaseObserver
42 constructor: (@options) ->
43 @document = null
44 @container = null
45 @listeners = []
46
47 useCapture: true
48
49 listen: (element, event, action) ->
50 element.addEventListener(event, action, @useCapture)
51 @listeners.push([element, event, action, @useCapture])
52
53 unlisten: ->
54 for [element, event, action, useCapture] in @listeners
55 element.removeEventListener(event, action, useCapture)
56 @listeners.length = 0
57
58 type: (value) -> TYPE_MAP[typeof value]
59
60 injectSettings: ->
61
62 appendSetting: (attributes) ->
63 setting = @document.createElement('setting')
64 utils.setAttributes(setting, attributes)
65 @container.appendChild(setting)
66 return setting
67
68 observe: (@document, topic, addonId) ->
69 return unless addonId == @options.id
70 switch topic
71 when 'addon-options-displayed'
72 @init()
73 when 'addon-options-hidden'
74 @destroy()
75
76 init: ->
77 @container = @document.getElementById('detail-rows')
78 @injectSettings()
79
80 destroy: ->
81 @unlisten()
82
83 # VimFx specific observer.
84 class Observer extends BaseObserver
85 constructor: (@vimfx) ->
86 super({id: @vimfx.id})
87
88 injectSettings: ->
89 @injectInstructions()
90 @injectOptions()
91 @injectShortcuts()
92 @setupKeybindings()
93 @setupValidation()
94
95 if @vimfx.goToCommand
96 return unless vim = @vimfx.getCurrentVim(utils.getCurrentWindow())
97 vim.markPageInteraction()
98 utils.nextTick(vim.window, =>
99 {pref} = @vimfx.goToCommand
100 setting = @container.querySelector("setting[pref='#{pref}']")
101 setting.scrollIntoView()
102 setting.input.select()
103 @vimfx.goToCommand = null
104 )
105
106 injectInstructions: ->
107 setting = @appendSetting({
108 type: 'control'
109 title: translate('prefs.instructions.title')
110 desc: translate('prefs.instructions.desc',
111 @vimfx.options['options.key.quote'],
112 @vimfx.options['options.key.insert_default'],
113 @vimfx.options['options.key.reset_default'],
114 '<c-z>')
115 'first-row': 'true'
116 })
117 href = "#{@vimfx.info.homepageURL}/tree/master/documentation"
118 docsLink = @document.createElement('label')
119 utils.setAttributes(docsLink, {
120 value: translate('prefs.documentation')
121 href
122 crop: 'end'
123 class: 'text-link'
124 })
125 setting.appendChild(docsLink)
126
127 injectOptions: ->
128 for key, value of defaults.options
129 setting = @appendSetting({
130 pref: "#{defaults.BRANCH}#{key}"
131 type: @type(value)
132 title: translate("pref.#{key}.title")
133 desc: translate("pref.#{key}.desc")
134 })
135 return
136
137 injectShortcuts: ->
138 for mode in @vimfx.getGroupedCommands()
139 @appendSetting({
140 type: 'control'
141 title: mode.name
142 'first-row': 'true'
143 })
144
145 for category in mode.categories
146 if category.name
147 @appendSetting({
148 type: 'control'
149 title: category.name
150 'first-row': 'true'
151 })
152
153 for {command} in category.commands
154 @appendSetting({
155 pref: command.pref
156 type: 'string'
157 title: command.description()
158 desc: @generateErrorMessage(command.pref)
159 class: 'is-shortcut'
160 })
161
162 return
163
164 generateErrorMessage: (pref) ->
165 commandErrors = @vimfx.errors[pref] ? []
166 return commandErrors.map(({id, context, subject}) ->
167 return translate("error.#{id}", context ? subject, subject)
168 ).join('\n')
169
170 setupKeybindings: ->
171 # Note that `setting = event.originalTarget` does _not_ return the correct
172 # element in these listeners!
173 quote = false
174 @listen(@container, 'keydown', (event) =>
175 setting = event.target
176 isString = (setting.type == 'string')
177
178 {input, pref} = setting
179 keyString = @vimfx.stringifyKeyEvent(event)
180
181 # Some shortcuts only make sense for string settings. We still match
182 # those shortcuts and suppress the default behavior for _all_ types of
183 # settings for consistency. For example, pressing <c-d> in a number input
184 # (which looks like a text input) would otherwise bookmark the page, and
185 # <c-q> would close the window!
186 switch
187 when not keyString
188 return
189 when quote
190 break unless isString
191 utils.insertText(input, keyString)
192 quote = false
193 when keyString == @vimfx.options['options.key.quote']
194 break unless isString
195 quote = true
196 # Override `<force>` commands (such as `<escape>` and `<tab>`).
197 return unless vim = @vimfx.getCurrentVim(utils.getCurrentWindow())
198 @vimfx.modes.normal.commands.quote.run({vim, count: 1})
199 when keyString == @vimfx.options['options.key.insert_default']
200 break unless isString
201 utils.insertText(input, prefs.root.default.get(pref))
202 when keyString == @vimfx.options['options.key.reset_default']
203 prefs.root.set(pref, null)
204 else
205 return
206
207 event.preventDefault()
208 setting.valueToPreference()
209 @refreshShortcutErrors()
210 )
211 @listen(@container, 'blur', -> quote = false)
212
213 setupValidation: ->
214 @listen(@container, 'input', (event) =>
215 setting = event.target
216 # Disable default behavior of updating the pref of the setting on each
217 # input. Do it on the 'change' event instead (see below), because all
218 # settings are validated and auto-adjusted as soon as the pref changes.
219 event.stopPropagation()
220 if setting.classList.contains('is-shortcut')
221 # However, for the shortcuts we _do_ want live validation, because they
222 # cannot be auto-adjusted. Instead an error message is shown.
223 setting.valueToPreference()
224 @refreshShortcutErrors()
225 )
226
227 @listen(@container, 'change', (event) ->
228 setting = event.target
229 unless setting.classList.contains('is-shortcut')
230 setting.valueToPreference()
231 )
232
233 refreshShortcutErrors: ->
234 for setting in @container.getElementsByClassName('is-shortcut')
235 setting.setAttribute('desc', @generateErrorMessage(setting.pref))
236 return
237
238 module.exports = {
239 observe
240 }
Imprint / Impressum