UNPKG

11.5 kBJavaScriptView Raw
1/**
2 * @module settings-panel
3 */
4'use strict';
5
6const Emitter = require('events').EventEmitter;
7const inherits = require('inherits');
8const extend = require('just-extend');
9const css = require('dom-css');
10const uid = require('get-uid');
11const fs = require('fs');
12const insertCss = require('insert-styles');
13const isPlainObject = require('is-plain-obj');
14const format = require('param-case');
15const px = require('add-px-to-style');
16const scopeCss = require('scope-css');
17
18module.exports = Panel
19
20
21insertCss(fs.readFileSync(__dirname + '/index.css', 'utf-8'));
22
23
24/**
25 * @constructor
26 */
27function Panel (items, opts) {
28 if (!(this instanceof Panel)) return new Panel(items, opts)
29
30 extend(this, opts);
31
32 //ensure container
33 if (this.container === undefined) this.container = document.body || document.documentElement;
34
35 this.container.classList.add('settings-panel-container');
36
37 //create element
38 if (!this.id) this.id = uid();
39 this.element = document.createElement('div')
40 this.element.className = 'settings-panel settings-panel-' + this.id;
41 if (this.className) this.element.className += ' ' + this.className;
42
43 //create title
44 if (this.title) {
45 this.titleEl = this.element.appendChild(document.createElement('h2'));
46 this.titleEl.className = 'settings-panel-title';
47 }
48
49 //create collapse button
50 if (this.collapsible && this.title) {
51 // this.collapseEl = this.element.appendChild(document.createElement('div'));
52 // this.collapseEl.className = 'settings-panel-collapse';
53 this.element.classList.add('settings-panel--collapsible');
54 this.titleEl.addEventListener('click', () => {
55 if (this.collapsed) {
56 this.collapsed = false;
57 this.element.classList.remove('settings-panel--collapsed');
58 }
59 else {
60 this.collapsed = true;
61 this.element.classList.add('settings-panel--collapsed');
62 }
63 });
64 }
65
66 //state is values of items
67 this.state = {};
68
69 //items is all items settings
70 this.items = {};
71
72 //create fields
73 this.set(items);
74
75 if (this.container) {
76 this.container.appendChild(this.element)
77 }
78
79 //create theme style
80 this.update();
81}
82
83inherits(Panel, Emitter);
84
85
86/**
87 * Set item value/options
88 */
89Panel.prototype.set = function (name, value) {
90 //handle list of properties
91 if (Array.isArray(name)) {
92 let items = name;
93 items.forEach((item) => {
94 this.set(item.id || item.label, item);
95 });
96
97 return this;
98 }
99
100 //handle plain object
101 if (isPlainObject(name)) {
102 let items = name;
103 let list = [];
104 for (let key in items) {
105 if (!isPlainObject(items[key])) {
106 items[key] = {value: items[key]};
107 }
108 if (items[key].id == null) items[key].id = key;
109 list.push(items[key]);
110 }
111 list = list.sort((a, b) => (a.order||0) - (b.order||0));
112
113 return this.set(list);
114 }
115
116 //format name
117 name = name || '';
118 name = name.replace(/\-/g,'dash-');
119 name = format(name);
120
121 if (name) {
122 var item = this.items[name];
123 if (!item) item = this.items[name] = { id: name, panel: this };
124 }
125 //noname items should not be saved in state
126 else {
127 var item = {id: null, panel: this};
128 }
129
130 var initialValue = item.value;
131 var isBefore = item.before;
132 var isAfter = item.after;
133
134 if (isPlainObject(value)) {
135 item = extend(item, value);
136 }
137 else {
138 //ignore nothing-changed set
139 if (value === item.value && value !== undefined) return this;
140 item.value = value;
141 }
142
143 if (item.value === undefined) item.value = item.default;
144
145 if (name) this.state[name] = item.value;
146
147 //define label via name
148 if (item.label === undefined && item.id) {
149 item.label = item.id;
150 }
151
152 //detect type
153 if (!item.type) {
154 if (item.value && Array.isArray(item.value)) {
155 if (typeof item.value[0] === 'string') {
156 item.type = 'checkbox';
157 }
158 else {
159 item.type = 'interval'
160 }
161 } else if (item.scale || item.max || item.steps || item.step || typeof item.value === 'number') {
162 item.type = 'range'
163 } else if (item.options) {
164 if (Array.isArray(item.options) && item.options.join('').length < 90 ) {
165 item.type = 'switch'
166 }
167 else {
168 item.type = 'select'
169 }
170 } else if (item.format) {
171 item.type = 'color'
172 } else if (typeof item.value === 'boolean') {
173 item.type = 'checkbox'
174 } else if (item.content != null) {
175 item.type = 'raw'
176 } else {
177 if (item.value && (item.value.length > 140 || /\n/.test(item.value))) {
178 item.type = 'textarea'
179 }
180 else {
181 item.type = 'text'
182 }
183 }
184 }
185
186 var field, fieldId;
187
188 if (item.id != null) {
189 fieldId = 'settings-panel-field-' + item.id;
190 field = this.element.querySelector('#' + fieldId);
191 }
192
193 //create field container
194 if (!field) {
195 field = document.createElement('div');
196 if (fieldId != null) field.id = fieldId;
197 this.element.appendChild(field);
198 item.field = field;
199 }
200 else {
201 //clean previous before/after
202 if (isBefore) {
203 this.element.removeChild(field.prevSibling);
204 }
205 if (isAfter) {
206 this.element.removeChild(field.nextSibling);
207 }
208 }
209
210 field.className = 'settings-panel-field settings-panel-field--' + item.type;
211
212 if (item.orientation) field.className += ' settings-panel-orientation-' + item.orientation;
213
214 if (item.className) field.className += ' ' + item.className;
215
216 if (item.style) {
217 if (isPlainObject(item.style)) {
218 css(field, item.style);
219 }
220 else if (typeof item.style === 'string') {
221 field.style.cssText = item.style;
222 }
223 }
224 else if (item.style !== undefined) {
225 field.style = null;
226 }
227
228 if (item.hidden) {
229 field.setAttribute('hidden', true);
230 }
231 else {
232 field.removeAttribute('hidden');
233 }
234
235 //createe container for the input
236 let inputContainer = field.querySelector('.settings-panel-input');
237
238 if (!inputContainer) {
239 inputContainer = document.createElement('div');
240 inputContainer.className = 'settings-panel-input';
241 item.container = inputContainer;
242 field.appendChild(inputContainer);
243 }
244
245 if (item.disabled) field.className += ' settings-panel-field--disabled';
246
247 let components = this.components;
248 let component = item.component;
249
250 if (!component) {
251 item.component = component = (components[item.type] || components.text)(item);
252
253 if (component.on) {
254 component.on('init', (data) => {
255 item.value = data
256 if (item.id) this.state[item.id] = item.value;
257 let state = extend({}, this.state);
258
259 item.init && item.init(data, state)
260 this.emit('init', item.id, data, state)
261 item.change && item.change(data, state)
262 this.emit('change', item.id, data, state)
263 });
264
265 component.on('input', (data) => {
266 item.value = data
267 if (item.id) this.state[item.id] = item.value;
268 let state = extend({}, this.state);
269
270 item.input && item.input(data, state)
271 this.emit('input', item.id, data, state)
272 item.change && item.change(data, state)
273 this.emit('change', item.id, data, state)
274 });
275
276 component.on('action', () => {
277 let state = extend({}, this.state);
278 item.action && item.action(state);
279 });
280
281 component.on('change', (data) => {
282 item.value = data
283 if (item.id) this.state[item.id] = item.value;
284 let state = extend({}, this.state);
285
286 item.change && item.change(data, state)
287 this.emit('change', item.id, data, state)
288 });
289 }
290 }
291 else {
292 component.update(item);
293 }
294
295 //create field label
296 if (component.label !== false && (item.label || item.label === '')) {
297 let label = field.querySelector('.settings-panel-label');
298 if (!label) {
299 label = document.createElement('label')
300 label.className = 'settings-panel-label';
301 field.insertBefore(label, inputContainer);
302 }
303
304 label.htmlFor = item.id;
305 label.innerHTML = item.label;
306 label.title = item.title || item.label;
307 }
308
309 //handle after and before
310 // if (item.before) {
311 // let before = item.before;
312 // if (before instanceof Function) {
313 // before = item.before.call(item, component);
314 // }
315 // if (before instanceof HTMLElement) {
316 // this.element.insertBefore(before, field);
317 // }
318 // else {
319 // field.insertAdjacentHTML('beforebegin', before);
320 // }
321 // }
322 // if (item.after) {
323 // let after = item.after;
324 // if (after instanceof Function) {
325 // after = item.after.call(item, component);
326 // }
327 // if (after instanceof HTMLElement) {
328 // this.element.insertBefore(after, field.nextSibling);
329 // }
330 // else {
331 // field.insertAdjacentHTML('afterend', after);
332 // }
333 // }
334
335 //emit change
336 if (initialValue !== item.value) {
337 this.emit('change', item.id, item.value, this.state)
338 }
339
340 return this;
341}
342
343
344/**
345 * Return property value or a list
346 */
347Panel.prototype.get = function (name) {
348 if (name == null) return this.state;
349 return this.state[name];
350}
351
352
353/**
354 * Update theme
355 */
356Panel.prototype.update = function (opts) {
357 extend(this, opts);
358
359 //FIXME: decide whether we have to reset these params
360 // if (opts && opts.theme) {
361 // if (opts.theme.fontSize) this.fontSize = opts.theme.fontSize;
362 // if (opts.theme.inputHeight) this.inputHeight = opts.theme.inputHeight;
363 // if (opts.theme.fontFamily) this.fontFamily = opts.theme.fontFamily;
364 // if (opts.theme.labelWidth) this.labelWidth = opts.theme.labelWidth;
365 // if (opts.theme.palette) this.palette = opts.theme.palette;
366 // }
367
368 //update title, if any
369 if (this.titleEl) this.titleEl.innerHTML = this.title;
370
371 //update orientation
372 this.element.classList.remove('settings-panel-orientation-top');
373 this.element.classList.remove('settings-panel-orientation-bottom');
374 this.element.classList.remove('settings-panel-orientation-left');
375 this.element.classList.remove('settings-panel-orientation-right');
376 this.element.classList.add('settings-panel-orientation-' + this.orientation);
377
378 //apply style
379 let cssStr = '';
380 if (this.theme instanceof Function) {
381 cssStr = this.theme.call(this, this);
382 }
383 else if (typeof this.theme === 'string') {
384 cssStr = this.theme;
385 }
386
387 //append extra css
388 if (this.css) {
389 if (this.css instanceof Function) {
390 cssStr += this.css.call(this, this);
391 }
392 else if (typeof this.css === 'string') {
393 cssStr += this.css;
394 }
395 }
396
397 //scope each rule
398 cssStr = scopeCss(cssStr || '', '.settings-panel-' + this.id) || '';
399
400 insertCss(cssStr.trim(), {
401 id: this.id
402 });
403
404 if (this.style) {
405 if (isPlainObject(this.style)) {
406 css(this.element, this.style);
407 }
408 else if (typeof this.style === 'string') {
409 this.element.style.cssText = this.style;
410 }
411 }
412 else if (this.style !== undefined) {
413 this.element.style = null;
414 }
415
416 return this;
417}
418
419//instance theme
420Panel.prototype.theme = require('./theme/none');
421
422/**
423 * Registered components
424 */
425Panel.prototype.components = {
426 range: require('./src/range'),
427
428 button: require('./src/button'),
429 text: require('./src/text'),
430 textarea: require('./src/textarea'),
431
432 checkbox: require('./src/checkbox'),
433 toggle: require('./src/checkbox'),
434
435 switch: require('./src/switch'),
436
437 color: require('./src/color'),
438
439 interval: require('./src/interval'),
440 multirange: require('./src/interval'),
441
442 custom: require('./src/custom'),
443 raw: require('./src/custom'),
444
445 select: require('./src/select')
446};
447
448
449/**
450 * Additional class name
451 */
452Panel.prototype.className;
453
454
455/**
456 * Additional visual setup
457 */
458Panel.prototype.orientation = 'left';
459
460
461/** Display collapse button */
462Panel.prototype.collapsible = false;
\No newline at end of file