stylus/manage/config-dialog.js

447 lines
13 KiB
JavaScript
Raw Normal View History

/* global messageBox */
2017-09-05 20:26:01 +00:00
'use strict';
function configDialog(style) {
2017-12-07 22:08:25 +00:00
const AUTOSAVE_DELAY = 500;
let saving = false;
const data = style.usercssData;
const varsHash = deepCopy(data.vars) || {};
const varNames = Object.keys(varsHash);
const vars = varNames.map(name => varsHash[name]);
let varsInitial = getInitialValues(varsHash);
const elements = [];
2017-11-27 06:56:22 +00:00
const colorpicker = window.colorpicker();
const isPopup = location.href.includes('popup.html');
const buttons = {};
2017-09-05 20:26:01 +00:00
buildConfigForm();
renderValues();
vars.forEach(renderValueState);
2017-09-05 20:26:01 +00:00
return messageBox({
title: `${style.name} v${data.version}`,
className: 'config-dialog' + (isPopup ? ' stylus-popup' : ''),
contents: [
$create('.config-heading', data.supportURL &&
$createLink({className: '.external-support', href: data.supportURL}, t('externalFeedback'))),
$create('.config-body', elements)
],
buttons: [{
textContent: t('confirmSave'),
dataset: {cmd: 'save'},
disabled: true,
onclick: save,
}, {
textContent: t('genericResetLabel'),
title: t('optionsReset'),
dataset: {cmd: 'default'},
onclick: useDefault,
}, {
textContent: t('confirmClose'),
dataset: {cmd: 'close'},
}],
onshow,
}).then(onhide);
function getInitialValues(source) {
const data = {};
for (const name of varNames) {
const va = source[name];
data[name] = isDefault(va) ? va.default : va.value;
}
return data;
}
function onshow(box) {
$('button', box).insertAdjacentElement('afterend',
$create('label#config-autosave-wrapper', {
title: t('configOnChangeTooltip'),
}, [
$create('input', {id: 'config.autosave', type: 'checkbox'}),
$create('SVG:svg.svg-icon.checked',
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})),
t('configOnChange'),
]));
setupLivePrefs(['config.autosave']);
if (isPopup) {
adjustSizeForPopup(box);
box.style.animationDuration = '0s';
}
box.addEventListener('change', onchange);
buttons.save = $('[data-cmd="save"]', box);
buttons.default = $('[data-cmd="default"]', box);
buttons.close = $('[data-cmd="close"]', box);
updateButtons();
}
function onhide() {
document.body.style.minWidth = '';
document.body.style.minHeight = '';
colorpicker.hide();
}
function onchange({target, justSaved = false}) {
// invoked after element's own onchange so 'va' contains the updated value
const va = target.va;
if (va) {
va.dirty = varsInitial[va.name] !== (isDefault(va) ? va.default : va.value);
if (prefs.get('config.autosave') && !justSaved) {
debounce(save, 100, {anyChangeIsDirty: true});
return;
}
renderValueState(va);
if (!justSaved) {
updateButtons();
}
}
}
function updateButtons() {
const someDirty = vars.some(va => va.dirty);
buttons.save.disabled = !someDirty;
buttons.default.disabled = vars.every(isDefault);
buttons.close.textContent = t(someDirty ? 'confirmCancel' : 'confirmClose');
}
2018-01-01 17:02:49 +00:00
function save({anyChangeIsDirty = false} = {}, bgStyle) {
if (saving) {
debounce(save, 0, ...arguments);
return;
}
2017-12-07 22:08:25 +00:00
if (!vars.length ||
!vars.some(va => va.dirty || anyChangeIsDirty && va.value !== va.savedValue)) {
return;
}
2018-01-01 17:02:49 +00:00
if (!bgStyle) {
API.getStyles({id: style.id, omitCode: !BG})
.then(([bgStyle]) => save({anyChangeIsDirty}, bgStyle || {}));
return;
}
style = style.sections ? Object.assign({}, style) : style;
style.enabled = true;
style.reason = 'config';
2018-01-01 17:02:49 +00:00
style.sourceCode = null;
style.sections = null;
const styleVars = style.usercssData.vars;
2018-01-01 17:02:49 +00:00
const bgVars = (bgStyle.usercssData || {}).vars || {};
const invalid = [];
let numValid = 0;
for (const va of vars) {
const bgva = bgVars[va.name];
let error;
if (!bgva) {
error = 'deleted';
delete styleVars[va.name];
} else
if (bgva.type !== va.type) {
error = ['type ', '*' + va.type, ' != ', '*' + bgva.type];
} else
if ((va.type === 'select' || va.type === 'dropdown') &&
!isDefault(va) &&
bgva.options.every(o => o.name !== va.value)) {
error = `'${va.value}' not in the updated '${va.type}' list`;
2017-12-07 22:08:25 +00:00
} else if (!va.dirty && (!anyChangeIsDirty || va.value === va.savedValue)) {
continue;
} else {
styleVars[va.name].value = va.value;
2017-12-07 22:08:25 +00:00
va.savedValue = va.value;
numValid++;
continue;
}
invalid.push(['*' + va.name, ': ', ...error].map(e =>
e[0] === '*' && $create('b', e.slice(1)) || e));
if (bgva) {
styleVars[va.name].value = deepCopy(bgva);
}
}
if (invalid.length) {
onhide();
messageBox.alert([
$create('div', {style: 'max-width: 34em'}, t('usercssConfigIncomplete')),
$create('ol', {style: 'text-align: left'},
invalid.map(msg =>
$create({tag: 'li', appendChild: msg}))),
], 'pre');
2017-09-05 20:26:01 +00:00
}
if (!numValid) {
return;
}
saving = true;
2018-01-01 17:02:49 +00:00
return API.saveUsercss(style)
.then(saved => {
2018-01-01 17:02:49 +00:00
varsInitial = getInitialValues(saved.usercssData.vars);
vars.forEach(va => onchange({target: va.input, justSaved: true}));
renderValues();
updateButtons();
2017-12-07 22:08:25 +00:00
$.remove('.config-error');
})
2017-12-07 22:08:25 +00:00
.catch(errors => {
const el = $('.config-error', messageBox.element) ||
$('#message-box-buttons').insertAdjacentElement('afterbegin', $create('.config-error'));
el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors;
})
.then(() => {
saving = false;
2017-12-07 22:08:25 +00:00
});
}
function useDefault() {
for (const va of vars) {
va.value = null;
onchange({target: va.input});
}
renderValues();
}
function isDefault(va) {
return va.value === null || va.value === undefined || va.value === va.default;
}
2017-09-05 20:26:01 +00:00
2018-09-16 22:17:15 +00:00
function rangeToProps(range = []) {
const dataset = {};
2018-09-16 22:17:15 +00:00
if (range.length > 0) {
dataset.min = range[0];
}
2018-09-16 22:17:15 +00:00
if (range.length > 1) {
dataset.max = range[1];
}
2018-09-16 22:17:15 +00:00
if (range[2]) {
dataset.step = range[2];
}
2018-09-16 22:17:15 +00:00
return dataset;
}
2017-09-05 20:26:01 +00:00
function buildConfigForm() {
let resetter =
$create('a.config-reset-icon', {href: '#'}, [
$create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'}, [
$create('SVG:title', t('genericResetLabel')),
$create('SVG:polygon', {
points: '16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5 ' +
'5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10',
})
])
]);
for (const va of vars) {
let children;
2017-11-09 01:00:46 +00:00
switch (va.type) {
case 'color':
children = [
$create('.colorview-swatch.config-value', [
va.input = $create('a.color-swatch', {
va,
href: '#',
onclick: showColorpicker
}),
]),
];
2017-11-09 01:00:46 +00:00
break;
case 'checkbox':
children = [
$create('span.onoffswitch.config-value', [
va.input = $create('input.slider', {
va,
type: 'checkbox',
2017-12-07 22:08:25 +00:00
onchange: updateVarOnChange,
}),
$create('span'),
]),
2017-11-09 01:00:46 +00:00
];
break;
case 'select':
case 'dropdown':
case 'image':
// TODO: a image picker input?
children = [
$create('.select-resizer.config-value', [
va.input = $create('select', {
va,
2017-12-07 22:08:25 +00:00
onchange: updateVarOnChange,
},
va.options.map(o =>
$create('option', {value: o.name}, o.label))),
$create('SVG:svg.svg-icon.select-arrow',
$create('SVG:use', {'xlink:href': '#svg-icon-select-arrow'})),
]),
];
2017-11-09 01:00:46 +00:00
break;
2018-09-16 22:17:15 +00:00
case 'range':
2018-09-16 22:25:46 +00:00
case 'number':
2018-09-16 22:17:15 +00:00
children = [
va.type === 'range' && $create('span.current-value', {textContent: va.value + va.units}),
va.input = $create('input.config-value', {
va,
type: va.type,
value: va.default,
...rangeToProps(va.range),
onchange: updateVarOnChange
})
];
break;
default: {
const options = {
2018-09-05 02:16:40 +00:00
va,
type: va.type,
onchange: updateVarOnChange,
oninput: updateVarOnInput,
2018-09-05 12:28:55 +00:00
onfocus: selectAllOnFocus,
2018-09-05 02:16:40 +00:00
};
2018-09-16 22:17:15 +00:00
children = [
va.input = $create('input.config-value', options),
];
}
2018-09-05 02:16:40 +00:00
2017-09-05 20:26:01 +00:00
}
resetter = resetter.cloneNode(true);
resetter.va = va;
resetter.onclick = resetOnClick;
elements.push(
$create(`label.config-${va.type}`, [
$create('span.config-name', tWordBreak(va.label)),
...children,
resetter,
]));
va.savedValue = va.value;
2017-09-05 20:26:01 +00:00
}
}
2017-09-05 20:26:01 +00:00
2017-12-07 22:08:25 +00:00
function updateVarOnChange() {
2018-09-08 12:22:03 +00:00
if (this.type === 'number') {
this.value = this.va.value = clampValue(this.value, this.va.range);
} else if (this.type === 'range') {
this.va.value = parseFloat(this.value);
2018-09-16 22:17:15 +00:00
$('.current-value', this.closest('.config-range')).textContent = this.va.value + (this.va.units || '');
} else {
this.va.value = this.type !== 'checkbox' ? this.value : this.checked ? '1' : '0';
2018-09-05 02:16:40 +00:00
}
2017-12-07 22:08:25 +00:00
}
2018-09-08 12:22:03 +00:00
// Clamp input[type=number] to a valid range
function clampValue(value, [min = 0, max = 100, step]) {
if (value < min) {
return min;
2018-09-16 22:17:15 +00:00
}
if (value > max) {
2018-09-08 12:22:03 +00:00
return max;
}
2018-09-16 22:17:15 +00:00
const inv = 1 / (step || 1);
2018-09-08 12:22:03 +00:00
// Don't restrict to integer values if step is undefined.
2018-09-16 22:17:15 +00:00
return typeof step !== 'undefined' ? Math.floor(inv * value) / inv : value;
2018-09-08 12:22:03 +00:00
}
2017-12-07 22:08:25 +00:00
function updateVarOnInput(event, debounced = false) {
if (debounced) {
event.target.dispatchEvent(new Event('change', {bubbles: true}));
} else {
debounce(updateVarOnInput, AUTOSAVE_DELAY, event, true);
}
}
2018-09-05 12:28:55 +00:00
function selectAllOnFocus(event) {
2018-09-05 02:17:36 +00:00
event.target.select();
}
function renderValues(varsToRender = vars) {
for (const va of varsToRender) {
const value = isDefault(va) ? va.default : va.value;
if (va.type === 'color') {
va.input.style.backgroundColor = value;
if (colorpicker.options.va === va) {
colorpicker.setColor(value);
2017-09-05 20:26:01 +00:00
}
} else if (va.type === 'checkbox') {
va.input.checked = Number(value);
2018-09-05 12:38:22 +00:00
} else if (va.type === 'range') {
const span = $('.current-value', va.input.closest('.config-range'));
2018-09-05 12:38:22 +00:00
va.input.value = value;
2018-09-05 02:16:40 +00:00
if (span) {
span.textContent = value + (va.units || '');
2018-09-05 02:16:40 +00:00
}
2018-09-05 12:38:22 +00:00
} else {
va.input.value = value;
2018-09-05 02:16:40 +00:00
}
if (!prefs.get('config.autosave')) {
renderValueState(va);
}
2017-09-05 20:26:01 +00:00
}
}
2017-09-05 20:26:01 +00:00
function renderValueState(va) {
const el = va.input.closest('label');
el.classList.toggle('dirty', Boolean(va.dirty));
el.classList.toggle('nondefault', !isDefault(va));
$('.config-reset-icon', el).disabled = isDefault(va);
}
function resetOnClick(event) {
event.preventDefault();
this.va.value = null;
renderValues([this.va]);
onchange({target: this.va.input});
}
function showColorpicker(event) {
event.preventDefault();
window.removeEventListener('keydown', messageBox.listeners.key, true);
const box = $('#message-box-contents');
colorpicker.show({
va: this.va,
color: this.va.value || this.va.default,
top: this.getBoundingClientRect().bottom - 5,
left: box.getBoundingClientRect().left - 360,
hideDelay: 1e6,
guessBrightness: box,
callback: onColorChanged,
});
}
2017-09-05 20:26:01 +00:00
function onColorChanged(newColor) {
if (newColor) {
this.va.value = newColor;
this.va.input.style.backgroundColor = newColor;
this.va.input.dispatchEvent(new Event('change', {bubbles: true}));
2017-11-27 06:56:22 +00:00
}
debounce(restoreEscInDialog);
}
2017-11-27 06:56:22 +00:00
function restoreEscInDialog() {
if (!$('.colorpicker-popup') && messageBox.element) {
window.addEventListener('keydown', messageBox.listeners.key, true);
}
2017-09-05 20:26:01 +00:00
}
function adjustSizeForPopup(box) {
const contents = box.firstElementChild;
contents.style = 'max-width: none; max-height: none;'.replace(/;/g, '!important;');
let {offsetWidth: width, offsetHeight: height} = contents;
contents.style = '';
const colorpicker = document.body.appendChild(
$create('.colorpicker-popup', {style: 'display: none!important'}));
2017-12-07 22:08:25 +00:00
const PADDING = 50;
const MIN_WIDTH = parseFloat(getComputedStyle(colorpicker).width) || 350;
2017-12-07 22:08:25 +00:00
const MIN_HEIGHT = 250 + PADDING;
colorpicker.remove();
2017-12-07 22:08:25 +00:00
width = constrain(MIN_WIDTH, 800, width + PADDING);
height = constrain(MIN_HEIGHT, 600, height + PADDING);
document.body.style.setProperty('min-width', width + 'px', 'important');
document.body.style.setProperty('min-height', height + 'px', 'important');
}
2017-12-07 22:08:25 +00:00
function constrain(min, max, value) {
return value < min ? min : value > max ? max : value;
}
2017-09-05 20:26:01 +00:00
}