allow continuous tweaking in usercss config dialog

* don't close on clicking "save" to allow continuous tweaking
* dirty item's label is marked with * and switches to italic
* "save" button is enabled when some value differs from saved
* "use default" button is enabled when some value differs from its default
* "close" becomes "cancel" when there are unsaved changed values
This commit is contained in:
tophf 2017-12-06 02:59:12 +03:00
parent de74c70778
commit e776a45dfa
4 changed files with 116 additions and 69 deletions

View File

@ -41,6 +41,16 @@
cursor: pointer; cursor: pointer;
} }
.config-dialog .dirty:after {
content: "*";
position: absolute;
left: 6px;
}
.config-dialog .dirty {
font-style: italic;
}
.config-dialog input, .config-dialog input,
.config-dialog select, .config-dialog select,
.config-dialog .onoffswitch { .config-dialog .onoffswitch {

View File

@ -6,35 +6,77 @@ function configDialog(style) {
const varsHash = deepCopy(data.vars) || {}; const varsHash = deepCopy(data.vars) || {};
const varNames = Object.keys(varsHash); const varNames = Object.keys(varsHash);
const vars = varNames.map(name => varsHash[name]); const vars = varNames.map(name => varsHash[name]);
let varsInitial = getInitialValues(varsHash);
const elements = []; const elements = [];
const colorpicker = window.colorpicker(); const colorpicker = window.colorpicker();
const isPopup = location.href.includes('popup.html');
const buttons = {};
buildConfigForm(); buildConfigForm();
renderValues(); renderValues();
return messageBox({ return messageBox({
title: `${style.name} v${data.version}`, title: `${style.name} v${data.version}`,
className: 'config-dialog', className: 'config-dialog' + (isPopup ? ' stylus-popup' : ''),
contents: [ contents: [
$create('.config-heading', data.supportURL && $create('.config-heading', data.supportURL &&
$createLink({className: '.external-support', href: data.supportURL}, t('externalFeedback'))), $createLink({className: '.external-support', href: data.supportURL}, t('externalFeedback'))),
$create('.config-body', elements) $create('.config-body', elements)
], ],
buttons: [ buttons: [
t('confirmSave'), {textContent: t('confirmSave'), dataset: {cmd: 'save'}, disabled: true, onclick: save},
{ {textContent: t('confirmDefault'), dataset: {cmd: 'default'}, onclick: useDefault},
textContent: t('confirmDefault'), {textContent: t('confirmClose'), dataset: {cmd: 'close'}},
onclick: useDefault ],
}, onshow,
t('confirmCancel') }).then(() => {
] document.body.style.minWidth = '';
}).then(({button, esc}) => { document.body.style.minHeight = '';
if (button !== 1) { colorpicker.hide();
colorpicker.hide(); });
function getInitialValues(source) {
const data = {};
for (const name of varNames) {
const va = source[name];
data[name] = isDefault(va) ? va.default : va.value;
} }
if (button > 0 || esc || !vars.length || !vars.some(va => va.dirty)) { return data;
}
function onshow(box) {
if (isPopup) {
adjustSizeForPopup(box);
}
box.addEventListener('change', onchange);
buttons.save = $('[data-cmd="save"]', box);
buttons.default = $('[data-cmd="default"]', box);
buttons.close = $('[data-cmd="close"]', box);
}
function onchange({target}) {
// 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);
target.closest('label').classList.toggle('dirty', va.dirty);
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');
}
function save() {
if (!vars.length || !vars.some(va => va.dirty)) {
return; return;
} }
style.enabled = true;
style.reason = 'config'; style.reason = 'config';
const styleVars = style.usercssData.vars; const styleVars = style.usercssData.vars;
const bgStyle = BG.cachedStyles.byId.get(style.id); const bgStyle = BG.cachedStyles.byId.get(style.id);
@ -52,7 +94,7 @@ function configDialog(style) {
error = ['type ', '*' + va.type, ' != ', '*' + bgva.type]; error = ['type ', '*' + va.type, ' != ', '*' + bgva.type];
} else } else
if ((va.type === 'select' || va.type === 'dropdown') && if ((va.type === 'select' || va.type === 'dropdown') &&
va.value !== null && va.value !== undefined && !isDefault(va) &&
bgva.options.every(o => o.name !== va.value)) { bgva.options.every(o => o.name !== va.value)) {
error = `'${va.value}' not in the updated '${va.type}' list`; error = `'${va.value}' not in the updated '${va.type}' list`;
} else if (!va.dirty) { } else if (!va.dirty) {
@ -76,8 +118,24 @@ function configDialog(style) {
$create({tag: 'li', appendChild: msg}))), $create({tag: 'li', appendChild: msg}))),
]); ]);
} }
return numValid && BG.usercssHelper.save(style); return numValid && BG.usercssHelper.save(style).then(saved => {
}); varsInitial = getInitialValues(deepCopy(saved.usercssData.vars));
vars.forEach(va => onchange({target: va.input}));
updateButtons();
});
}
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;
}
function buildConfigForm() { function buildConfigForm() {
for (const va of vars) { for (const va of vars) {
@ -86,7 +144,7 @@ function configDialog(style) {
case 'color': case 'color':
children = [ children = [
$create('.cm-colorview', [ $create('.cm-colorview', [
va.inputColor = $create('.color-swatch', { va.input = $create('.color-swatch', {
va, va,
onclick: showColorpicker onclick: showColorpicker
}), }),
@ -98,10 +156,10 @@ function configDialog(style) {
children = [ children = [
$create('span.onoffswitch', [ $create('span.onoffswitch', [
va.input = $create('input.slider', { va.input = $create('input.slider', {
va,
type: 'checkbox', type: 'checkbox',
onchange() { onchange() {
va.dirty = true; va.value = va.input.checked ? '1' : '0';
va.value = String(Number(va.input.checked));
}, },
}), }),
$create('span'), $create('span'),
@ -116,8 +174,8 @@ function configDialog(style) {
children = [ children = [
$create('.select-resizer', [ $create('.select-resizer', [
va.input = $create('select', { va.input = $create('select', {
va,
onchange() { onchange() {
va.dirty = true;
va.value = this.value; va.value = this.value;
} }
}, },
@ -132,10 +190,11 @@ function configDialog(style) {
default: default:
children = [ children = [
va.input = $create('input', { va.input = $create('input', {
va,
type: 'text', type: 'text',
oninput() { oninput() {
va.dirty = true; va.value = this.value;
va.value = va.input.value; this.dispatchEvent(new Event('change', {bubbles: true}));
}, },
}), }),
]; ];
@ -151,10 +210,9 @@ function configDialog(style) {
function renderValues() { function renderValues() {
for (const va of vars) { for (const va of vars) {
const useDefault = va.value === null || va.value === undefined; const value = isDefault(va) ? va.default : va.value;
const value = useDefault ? va.default : va.value;
if (va.type === 'color') { if (va.type === 'color') {
va.inputColor.style.backgroundColor = value; va.input.style.backgroundColor = value;
if (colorpicker.options.va === va) { if (colorpicker.options.va === va) {
colorpicker.setColor(value); colorpicker.setColor(value);
} }
@ -166,15 +224,6 @@ function configDialog(style) {
} }
} }
function useDefault() {
for (const va of vars) {
const hasValue = va.value !== null && va.value !== undefined;
va.dirty = hasValue && va.value !== va.default;
va.value = null;
}
renderValues();
}
function showColorpicker() { function showColorpicker() {
window.removeEventListener('keydown', messageBox.listeners.key, true); window.removeEventListener('keydown', messageBox.listeners.key, true);
const box = $('#message-box-contents'); const box = $('#message-box-contents');
@ -191,9 +240,9 @@ function configDialog(style) {
function onColorChanged(newColor) { function onColorChanged(newColor) {
if (newColor) { if (newColor) {
this.va.dirty = true;
this.va.value = newColor; this.va.value = newColor;
this.va.inputColor.style.backgroundColor = newColor; this.va.input.style.backgroundColor = newColor;
this.va.input.dispatchEvent(new Event('change', {bubbles: true}));
} }
debounce(restoreEscInDialog); debounce(restoreEscInDialog);
} }
@ -203,4 +252,22 @@ function configDialog(style) {
window.addEventListener('keydown', messageBox.listeners.key, true); window.addEventListener('keydown', messageBox.listeners.key, true);
} }
} }
function adjustSizeForPopup(box) {
box.style = 'white-space: nowrap !important';
box.firstElementChild.style = 'max-width: none; max-height: none;'.replace(/;/g, '!important;');
const {offsetWidth, offsetHeight} = box.firstElementChild;
box.style = box.firstElementChild.style = '';
const colorpicker = document.body.appendChild(
$create('.colorpicker-popup', {style: 'display: none!important'}));
const MIN_WIDTH = parseFloat(getComputedStyle(colorpicker).width) || 350;
const MIN_HEIGHT = 250;
colorpicker.remove();
const width = Math.max(Math.min(offsetWidth / 0.9 + 2, 800), MIN_WIDTH);
const height = Math.max(Math.min(offsetHeight / 0.9 + 2, 600), MIN_HEIGHT);
document.body.style.setProperty('min-width', width + 'px', 'important');
document.body.style.setProperty('min-height', height + 'px', 'important');
}
} }

View File

@ -143,13 +143,6 @@
top: auto; top: auto;
right: auto; right: auto;
} }
#message-box.calculate-size {
white-space: nowrap;
}
#message-box.calculate-size > div {
max-width: none;
max-height: none;
}
@keyframes fadein { @keyframes fadein {
from { from {

View File

@ -12,25 +12,6 @@ function messageBox({
bindGlobalListeners(); bindGlobalListeners();
createElement(); createElement();
document.body.appendChild(messageBox.element); document.body.appendChild(messageBox.element);
if (location.href.includes('popup.html')) {
messageBox.isPopup = true;
messageBox.element.classList.add('stylus-popup');
// calculate size
messageBox.element.classList.add('calculate-size');
const {offsetWidth, offsetHeight} = messageBox.element.children[0];
messageBox.element.classList.remove('calculate-size');
// for colorpicker
const MIN_WIDTH = 350;
const MIN_HEIGHT = 250;
const width = Math.max(Math.min(offsetWidth / 0.9 + 2, 800), MIN_WIDTH);
const height = Math.max(Math.min(offsetHeight / 0.9 + 2, 600), MIN_HEIGHT);
document.body.style.minWidth = `${width}px`;
document.body.style.minHeight = `${height}px`;
}
if (onshow) { if (onshow) {
onshow(messageBox.element); onshow(messageBox.element);
} }
@ -89,11 +70,11 @@ function messageBox({
$create(`#${id}-contents`, tHTML(contents)), $create(`#${id}-contents`, tHTML(contents)),
$create(`#${id}-buttons`, $create(`#${id}-buttons`,
buttons.map((content, buttonIndex) => content && buttons.map((content, buttonIndex) => content &&
$create('button', { $create('button', Object.assign({
buttonIndex, buttonIndex,
textContent: content.textContent || content, textContent: typeof content === 'object' ? '' : content,
onclick: content.onclick || messageBox.listeners.button, onclick: messageBox.listeners.button,
}))), }, typeof content === 'object' && content)))),
]), ]),
]); ]);
} }
@ -115,10 +96,6 @@ function messageBox({
messageBox.element.remove(); messageBox.element.remove();
messageBox.element = null; messageBox.element = null;
messageBox.resolve = null; messageBox.resolve = null;
if (messageBox.isPopup) {
document.body.style.minWidth = '';
document.body.style.minHeight = '';
}
} }
} }