]> git.gir.st - VimFx.git/blob - extension/lib/options.coffee
Merge branch 'master' into develop
[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
33 observe = (options) ->
34 observer = new Observer(options)
35 utils.observe('addon-options-displayed', observer)
36 utils.observe('addon-options-hidden', observer)
37 module.onShutdown(->
38 observer.destroy()
39 )
40
41 # Generalized observer.
42 class BaseObserver
43 constructor: (@options) ->
44 @document = null
45 @container = null
46 @listeners = []
47
48 useCapture: true
49
50 listen: (element, event, action) ->
51 element.addEventListener(event, action, @useCapture)
52 @listeners.push([element, event, action, @useCapture])
53
54 unlisten: ->
55 for [element, event, action, useCapture] in @listeners
56 element.removeEventListener(event, action, useCapture)
57 @listeners.length = 0
58
59 type: (value) -> TYPE_MAP[typeof value]
60
61 injectSettings: ->
62
63 appendSetting: (attributes) ->
64 setting = @document.createElement('setting')
65 utils.setAttributes(setting, attributes)
66 @container.appendChild(setting)
67 return setting
68
69 observe: (@document, topic, addonId) ->
70 return unless addonId == @options.id
71 switch topic
72 when 'addon-options-displayed'
73 @init()
74 when 'addon-options-hidden'
75 @destroy()
76
77 init: ->
78 @container = @document.getElementById('detail-rows')
79 @injectSettings()
80
81 destroy: ->
82 @unlisten()
83
84 # VimFx specific observer.
85 class Observer extends BaseObserver
86 constructor: (@vimfx) ->
87 super({id: @vimfx.id})
88
89 injectSettings: ->
90 @injectInstructions()
91 @injectOptions()
92 @injectShortcuts()
93 @setupKeybindings()
94 @setupValidation()
95
96 if @vimfx.goToCommand
97 utils.nextTick(@document.ownerGlobal, =>
98 {pref} = @vimfx.goToCommand
99 setting = @container.querySelector("setting[pref='#{pref}']")
100 setting.scrollIntoView()
101 setting.input.select()
102 @vimfx.goToCommand = null
103 )
104
105 injectInstructions: ->
106 setting = @appendSetting({
107 type: 'control'
108 title: translate('prefs.instructions.title')
109 desc: translate(
110 '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 )
116 'first-row': 'true'
117 })
118 href = "#{@vimfx.info.homepageURL}/tree/master/documentation"
119 docsLink = @document.createElement('label')
120 utils.setAttributes(docsLink, {
121 value: translate('prefs.documentation')
122 href
123 crop: 'end'
124 class: 'text-link'
125 })
126 setting.appendChild(docsLink)
127
128 injectOptions: ->
129 for key, value of defaults.options
130 setting = @appendSetting({
131 pref: "#{defaults.BRANCH}#{key}"
132 type: @type(value)
133 title: translate("pref.#{key}.title")
134 desc: translate("pref.#{key}.desc")
135 })
136 return
137
138 injectShortcuts: ->
139 for mode in @vimfx.getGroupedCommands()
140 @appendSetting({
141 type: 'control'
142 title: mode.name
143 'first-row': 'true'
144 })
145
146 for category in mode.categories
147 if category.name
148 @appendSetting({
149 type: 'control'
150 title: category.name
151 'first-row': 'true'
152 })
153
154 for {command} in category.commands
155 @appendSetting({
156 pref: command.pref
157 type: 'string'
158 title: command.description
159 desc: @generateErrorMessage(command.pref)
160 class: 'is-shortcut'
161 })
162
163 return
164
165 generateErrorMessage: (pref) ->
166 commandErrors = @vimfx.errors[pref] ? []
167 return commandErrors.map(({id, context, subject}) ->
168 return translate("error.#{id}", context ? subject, subject)
169 ).join('\n')
170
171 setupKeybindings: ->
172 # Note that `setting = event.originalTarget` does _not_ return the correct
173 # element in these listeners!
174 quote = false
175 @listen(@container, 'keydown', (event) =>
176 setting = event.target
177 isString = (setting.type == 'string')
178
179 {input, pref} = setting
180 keyString = @vimfx.stringifyKeyEvent(event)
181
182 # Some shortcuts only make sense for string settings. We still match
183 # those shortcuts and suppress the default behavior for _all_ types of
184 # settings for consistency. For example, pressing <c-d> in a number input
185 # (which looks like a text input) would otherwise bookmark the page, and
186 # <c-q> would close the window!
187 switch
188 when not keyString
189 return
190 when quote
191 break unless isString
192 utils.insertText(input, keyString)
193 quote = false
194 when keyString == @vimfx.options['options.key.quote']
195 break unless isString
196 quote = true
197 # Override `<force>` commands (such as `<escape>` and `<tab>`).
198 return unless vim = @vimfx.getCurrentVim(utils.getCurrentWindow())
199 @vimfx.modes.normal.commands.quote.run({vim, count: 1})
200 when keyString == @vimfx.options['options.key.insert_default']
201 break unless isString
202 utils.insertText(input, prefs.root.default.get(pref))
203 when keyString == @vimfx.options['options.key.reset_default']
204 prefs.root.set(pref, null)
205 else
206 return
207
208 event.preventDefault()
209 setting.valueToPreference()
210 @refreshShortcutErrors()
211 )
212 @listen(@container, 'blur', -> quote = false)
213
214 setupValidation: ->
215 @listen(@container, 'input', (event) =>
216 setting = event.target
217 # Disable default behavior of updating the pref of the setting on each
218 # input. Do it on the 'change' event instead (see below), because all
219 # settings are validated and auto-adjusted as soon as the pref changes.
220 event.stopPropagation()
221 if setting.classList.contains('is-shortcut')
222 # However, for the shortcuts we _do_ want live validation, because they
223 # cannot be auto-adjusted. Instead an error message is shown.
224 setting.valueToPreference()
225 @refreshShortcutErrors()
226 )
227
228 @listen(@container, 'change', (event) ->
229 setting = event.target
230 unless setting.classList.contains('is-shortcut')
231 setting.valueToPreference()
232 )
233
234 refreshShortcutErrors: ->
235 for setting in @container.getElementsByClassName('is-shortcut')
236 setting.setAttribute('desc', @generateErrorMessage(setting.pref))
237 return
238
239 module.exports = {
240 observe
241 }
Imprint / Impressum