Add: style settings (#1358)

* Add: style settings

* Change: use radio instead of select for dark/light mode

* Change: x -> Delete

* Change: (in|ex)clusion messages

* Fix: avoid extra space when there is no rule

* Fix: UI in mobile

* Change: delete priority

* Change: use textarea for include/exclude, remove isCodeUpdated

* Fix: separate toggle

* Fix: minor

* Fix: remove codeIsUpdated in styleman
This commit is contained in:
eight 2021-12-07 12:44:49 +08:00 committed by GitHub
parent a59aab73fc
commit 9d1243073b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 359 additions and 28 deletions

View File

@ -375,6 +375,12 @@
}, },
"description": "Title of the page for editing styles" "description": "Title of the page for editing styles"
}, },
"editorCodeLabel": {
"message": "Code"
},
"editorSettingLabel": {
"message": "Settings"
},
"enableStyleLabel": { "enableStyleLabel": {
"message": "Enable", "message": "Enable",
"description": "Label for the button to enable a style" "description": "Label for the button to enable a style"
@ -1634,6 +1640,21 @@
"message": "As a security precaution, the browser prohibits extensions from affecting its built-in pages (like chrome://version, the standard new tab page as of Chrome 61, about:addons, and so on) as well as other extensions' pages. Each browser also restricts access to its own extensions gallery (like Chrome Web Store or AMO).", "message": "As a security precaution, the browser prohibits extensions from affecting its built-in pages (like chrome://version, the standard new tab page as of Chrome 61, about:addons, and so on) as well as other extensions' pages. Each browser also restricts access to its own extensions gallery (like Chrome Web Store or AMO).",
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"styleOriginLabel": {
"message": "Style origin"
},
"styleUpdateUrlLabel": {
"message": "Update URL"
},
"stylePreferSchemeLabel": {
"message": "Dark/Light mode"
},
"styleIncludeLabel": {
"message": "Custom included sites"
},
"styleExcludeLabel": {
"message": "Custom excluded sites"
},
"syncDropboxDeprecated": { "syncDropboxDeprecated": {
"message": "Dropbox import/export is replaced by a more advanced style sync in the options page." "message": "Dropbox import/export is replaced by a more advanced style sync in the options page."
}, },

View File

@ -283,7 +283,7 @@ const styleMan = (() => {
async toggle(id, enabled) { async toggle(id, enabled) {
if (ready.then) await ready; if (ready.then) await ready;
const style = Object.assign({}, id2style(id), {enabled}); const style = Object.assign({}, id2style(id), {enabled});
await saveStyle(style, {reason: 'toggle', codeIsUpdated: false}); await saveStyle(style, {reason: 'toggle'});
return id; return id;
}, },
@ -297,6 +297,13 @@ const styleMan = (() => {
removeExclusion: removeIncludeExclude.bind(null, 'exclusions'), removeExclusion: removeIncludeExclude.bind(null, 'exclusions'),
/** @returns {Promise<?StyleObj>} */ /** @returns {Promise<?StyleObj>} */
removeInclusion: removeIncludeExclude.bind(null, 'inclusions'), removeInclusion: removeIncludeExclude.bind(null, 'inclusions'),
async config(id, prop, value) {
if (ready.then) await ready;
const style = Object.assign({}, id2style(id));
style[prop] = value;
return saveStyle(style, {reason: 'config'});
},
}; };
//#endregion //#endregion
@ -375,7 +382,7 @@ const styleMan = (() => {
throw new Error('The rule already exists'); throw new Error('The rule already exists');
} }
style[type] = list.concat([rule]); style[type] = list.concat([rule]);
return saveStyle(style, {reason: 'styleSettings'}); return saveStyle(style, {reason: 'config'});
} }
async function removeIncludeExclude(type, id, rule) { async function removeIncludeExclude(type, id, rule) {
@ -386,10 +393,10 @@ const styleMan = (() => {
return; return;
} }
style[type] = list.filter(r => r !== rule); style[type] = list.filter(r => r !== rule);
return saveStyle(style, {reason: 'styleSettings'}); return saveStyle(style, {reason: 'config'});
} }
function broadcastStyleUpdated(style, reason, method = 'styleUpdated', codeIsUpdated = true) { function broadcastStyleUpdated(style, reason, method = 'styleUpdated') {
const {id} = style; const {id} = style;
const data = id2data(id); const data = id2data(id);
const excluded = new Set(); const excluded = new Set();
@ -412,7 +419,6 @@ const styleMan = (() => {
return msg.broadcast({ return msg.broadcast({
method, method,
reason, reason,
codeIsUpdated,
style: { style: {
id, id,
md5Url: style.md5Url, md5Url: style.md5Url,
@ -452,7 +458,7 @@ const styleMan = (() => {
return handleSave(style, handlingOptions); return handleSave(style, handlingOptions);
} }
function handleSave(style, {reason, codeIsUpdated, broadcast = true}) { function handleSave(style, {reason, broadcast = true}) {
const data = id2data(style.id); const data = id2data(style.id);
const method = data ? 'styleUpdated' : 'styleAdded'; const method = data ? 'styleUpdated' : 'styleAdded';
if (!data) { if (!data) {
@ -460,7 +466,7 @@ const styleMan = (() => {
} else { } else {
data.style = style; data.style = style;
} }
if (broadcast) broadcastStyleUpdated(style, reason, method, codeIsUpdated); if (broadcast) broadcastStyleUpdated(style, reason, method);
return style; return style;
} }

View File

@ -17,6 +17,7 @@
<script src="content/apply.js"></script> <script src="content/apply.js"></script>
<script src="js/sections-util.js"></script> <script src="js/sections-util.js"></script>
<script src="js/event-emitter.js"></script>
<script src="edit/codemirror-themes.js"></script> <!-- must precede base.js --> <script src="edit/codemirror-themes.js"></script> <!-- must precede base.js -->
<script src="edit/base.js"></script> <script src="edit/base.js"></script>
@ -61,6 +62,7 @@
<script src="edit/sections-editor-section.js"></script> <script src="edit/sections-editor-section.js"></script>
<script src="edit/sections-editor.js"></script> <script src="edit/sections-editor.js"></script>
<script src="edit/usw-integration.js"></script> <script src="edit/usw-integration.js"></script>
<script src="edit/settings.js"></script>
<script src="edit/edit.js"></script> <script src="edit/edit.js"></script>
<template data-id="appliesTo"> <template data-id="appliesTo">
@ -239,6 +241,8 @@
<link href="js/color/color-picker.css" rel="stylesheet"> <link href="js/color/color-picker.css" rel="stylesheet">
<link href="edit/codemirror-default.css" rel="stylesheet"> <link href="edit/codemirror-default.css" rel="stylesheet">
<link href="edit/edit.css" rel="stylesheet"> <link href="edit/edit.css" rel="stylesheet">
<link rel="stylesheet" href="edit/tab.css">
<link rel="stylesheet" href="edit/settings.css">
</head> </head>
<body id="stylus-edit"> <body id="stylus-edit">
@ -433,7 +437,53 @@
target="_blank"></a> target="_blank"></a>
</div> </div>
</div> </div>
<section id="sections"></section> <div class="main tab-container">
<div class="tab-bar">
<div class="tab-bar-item active" i18n-text="editorCodeLabel"></div>
<div class="tab-bar-item" i18n-text="editorSettingLabel"></div>
</div>
<div class="tab-panel">
<section id="sections" class="active"></section>
<fieldset class="style-settings" disabled>
<!-- <label class="style-origin">
<span class="form-label" i18n-text="styleOriginLabel"></span>
<input id="styleOrigin" type="text">
</label> -->
<label class="form-group style-update-url">
<span class="form-label" i18n-text="styleUpdateUrlLabel"></span>
<input type="text">
</label>
<div class="form-group style-prefer-scheme radio-group">
<!-- FIXME: should we use a different message from install page? -->
<span class="form-label" i18n-text="installPreferSchemeLabel"></span>
<label class="radio-item">
<input type="radio" name="preferScheme" value="none">
<span class="radio-label" i18n-text="installPreferSchemeNone"></span>
</label>
<label class="radio-item">
<input type="radio" name="preferScheme" value="dark">
<span class="radio-label" i18n-text="installPreferSchemeDark"></span>
</label>
<label class="radio-item">
<input type="radio" name="preferScheme" value="light">
<span class="radio-label" i18n-text="installPreferSchemeLight"></span>
</label>
</div>
<label class="form-group style-include">
<span class="form-label" i18n-text="styleIncludeLabel"></span>
<textarea></textarea>
</label>
<label class="form-group style-exclude">
<span class="form-label" i18n-text="styleExcludeLabel"></span>
<textarea></textarea>
</label>
<!-- <label class="style-always-important">
<input type="checkbox">
<span class="form-label" i18n-text="styleAlwaysImportantLabel"></span>
</label> -->
</fieldset>
</div>
</div>
<div id="help-popup"> <div id="help-popup">
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg> <div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
<div class="contents"></div> <div class="contents"></div>
@ -478,5 +528,6 @@
</symbol> </symbol>
</svg> </svg>
<script src="edit/tab.js"></script>
</body> </body>
</html> </html>

View File

@ -14,13 +14,14 @@
tryJSONparse tryJSONparse
tryURL tryURL
*/// toolbox.js */// toolbox.js
/* global EventEmitter */
'use strict'; 'use strict';
/** /**
* @type Editor * @type Editor
* @namespace Editor * @namespace Editor
*/ */
const editor = { const editor = Object.assign(EventEmitter(), {
style: null, style: null,
dirty: DirtyReporter(), dirty: DirtyReporter(),
isUsercss: false, isUsercss: false,
@ -47,7 +48,7 @@ const editor = {
customName || name || t('styleMissingName') customName || name || t('styleMissingName')
} - Stylus`; // the suffix enables external utilities to process our windows e.g. pin on top } - Stylus`; // the suffix enables external utilities to process our windows e.g. pin on top
}, },
}; });
//#region pre-init //#region pre-init

View File

@ -105,8 +105,11 @@ label {
#header h1 { #header h1 {
margin-top: 0; margin-top: 0;
} }
#sections { .main {
padding-left: 280px; padding-left: 280px;
height: 100%;
}
#sections {
min-height: 0; min-height: 0;
height: 100%; height: 100%;
} }
@ -1177,13 +1180,16 @@ body.linter-disabled .hidden-unless-compact {
#lint:not([open]) + #footer { #lint:not([open]) + #footer {
margin: .25em 0 -1em .25em; margin: .25em 0 -1em .25em;
} }
#sections { .main {
height: unset !important; height: unset !important;
padding-left: 0; padding-left: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
} }
.tab-bar {
margin-top: var(--fixed-height);
}
#sections > :not(.single-editor) { #sections > :not(.single-editor) {
margin: 0 .5rem; margin: 0 .5rem;
padding: .5rem 0; padding: .5rem 0;

View File

@ -1,5 +1,5 @@
/* global $ $create messageBoxProxy waitForSheet */// dom.js /* global $ $create messageBoxProxy waitForSheet */// dom.js
/* global API msg */// msg.js /* global msg API */// msg.js
/* global CodeMirror */ /* global CodeMirror */
/* global SectionsEditor */ /* global SectionsEditor */
/* global SourceEditor */ /* global SourceEditor */
@ -11,6 +11,7 @@
/* global linterMan */ /* global linterMan */
/* global prefs */ /* global prefs */
/* global t */// localization.js /* global t */// localization.js
/* global StyleSettings */// settings.js
'use strict'; 'use strict';
//#region init //#region init
@ -18,6 +19,7 @@
baseInit.ready.then(async () => { baseInit.ready.then(async () => {
await waitForSheet(); await waitForSheet();
(editor.isUsercss ? SourceEditor : SectionsEditor)(); (editor.isUsercss ? SourceEditor : SectionsEditor)();
StyleSettings(editor);
await editor.ready; await editor.ready;
editor.ready = true; editor.ready = true;
editor.dirty.onChange(editor.updateDirty); editor.dirty.onChange(editor.updateDirty);
@ -59,7 +61,8 @@ const IGNORE_UPDATE_REASONS = [
'editPreview', 'editPreview',
'editPreviewEnd', 'editPreviewEnd',
'editSave', 'editSave',
'config', // https://github.com/openstyles/stylus/issues/807 is closed without fix
// 'config,
]; ];
msg.onExtension(request => { msg.onExtension(request => {
@ -67,8 +70,14 @@ msg.onExtension(request => {
switch (request.method) { switch (request.method) {
case 'styleUpdated': case 'styleUpdated':
if (editor.style.id === style.id && !IGNORE_UPDATE_REASONS.includes(request.reason)) { if (editor.style.id === style.id && !IGNORE_UPDATE_REASONS.includes(request.reason)) {
Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(style.id)) if (request.reason === 'toggle') {
.then(newStyle => editor.replaceStyle(newStyle, request.codeIsUpdated)); editor.emit('styleToggled', request.style);
} else {
API.styles.get(request.style.id)
.then(style => {
editor.emit('styleChange', style, request.reason);
});
}
} }
break; break;
case 'styleDeleted': case 'styleDeleted':
@ -159,9 +168,9 @@ window.on('beforeunload', e => {
} }
}, },
toggleStyle() { toggleStyle(enabled = style.enabled) {
$('#enabled').checked = !style.enabled; $('#enabled').checked = enabled;
editor.updateEnabledness(!style.enabled); editor.updateEnabledness(enabled);
}, },
updateDirty() { updateDirty() {

View File

@ -84,16 +84,13 @@ function SectionsEditor() {
: null; : null;
}, },
async replaceStyle(newStyle, codeIsUpdated) { async replaceStyle(newStyle) {
dirty.clear('name'); dirty.clear();
// FIXME: avoid recreating all editors? // FIXME: avoid recreating all editors?
if (codeIsUpdated !== false) { await initSections(newStyle.sections, {replace: true});
await initSections(newStyle.sections, {replace: true});
}
Object.assign(style, newStyle); Object.assign(style, newStyle);
editor.onStyleUpdated(); editor.onStyleUpdated();
updateHeader(); updateHeader();
dirty.clear();
// Go from new style URL to edit style URL // Go from new style URL to edit style URL
if (style.id && !/[&?]id=/.test(location.search)) { if (style.id && !/[&?]id=/.test(location.search)) {
history.replaceState({}, document.title, `${location.pathname}?id=${style.id}`); history.replaceState({}, document.title, `${location.pathname}?id=${style.id}`);
@ -111,6 +108,9 @@ function SectionsEditor() {
} }
newStyle = await API.styles.editSave(newStyle); newStyle = await API.styles.editSave(newStyle);
destroyRemovedSections(); destroyRemovedSections();
if (!style.id) {
editor.emit('styleChange', newStyle, 'new');
}
sessionStore.justEditedStyleId = newStyle.id; sessionStore.justEditedStyleId = newStyle.id;
editor.replaceStyle(newStyle, false); editor.replaceStyle(newStyle, false);
}, },
@ -128,6 +128,28 @@ function SectionsEditor() {
editor.ready = initSections(style.sections); editor.ready = initSections(style.sections);
editor.on('styleToggled', newStyle => {
if (!dirty.isDirty()) {
Object.assign(style, newStyle);
} else {
editor.toggleStyle(newStyle.enabled);
}
updateHeader();
updateLivePreview();
});
editor.on('styleChange', (newStyle, reason) => {
if (reason === 'new') return; // nothing is new for us
if (reason === 'config') {
delete newStyle.sections;
delete newStyle.name;
delete newStyle.enabled;
Object.assign(style, newStyle);
updateLivePreview();
return;
}
editor.replaceStyle(newStyle);
});
/** @param {EditorSection} section */ /** @param {EditorSection} section */
function fitToContent(section) { function fitToContent(section) {
const {cm, cm: {display: {wrapper, sizer}}} = section; const {cm, cm: {display: {wrapper, sizer}}} = section;

45
edit/settings.css Normal file
View File

@ -0,0 +1,45 @@
.style-settings {
padding: 0.7rem 1.7rem;
border: 0;
margin: 0;
}
.form-group {
display: block;
margin: .6em 0;
padding: 0;
}
.form-label {
display: inline-block;
margin: .3em 0;
}
[disabled] .form-label {
opacity: 0.4;
}
.form-group input[type=text],
.form-group input[type=number],
.form-group select,
.form-group textarea {
display: block;
width: 100%;
box-sizing: border-box;
}
.radio-group .form-label {
display: block;
}
.radio-item {
display: flex;
margin: 0.3em 0 .3em;
padding: 0;
align-items: center;
width: max-content;
}
.radio-item input {
margin: 0 0.6em 0 0;
}
[disabled] .radio-label {
opacity: 0.4;
}
.style-include textarea,
.style-exclude textarea {
height: 12em;
}

65
edit/settings.js Normal file
View File

@ -0,0 +1,65 @@
/* global API */
/* exported StyleSettings */
'use strict';
function StyleSettings(editor) {
let {style} = editor;
const inputs = [
createInput('.style-update-url input', () => style.updateUrl || '',
e => API.styles.config(style.id, 'updateUrl', e.target.value)),
createRadio('.style-prefer-scheme input', () => style.preferScheme || 'none',
e => API.styles.config(style.id, 'preferScheme', e.target.value)),
createInput('.style-include textarea', () => (style.inclusions || []).join('\n'),
e => API.styles.config(style.id, 'inclusions', textToList(e.target.value))),
createInput('.style-exclude textarea', () => (style.exclusions || []).join('\n'),
e => API.styles.config(style.id, 'exclusions', textToList(e.target.value))),
];
update(style);
editor.on('styleChange', update);
function textToList(text) {
const list = text.split(/\s*\r?\n\s*/g);
return list.filter(Boolean);
}
function update(newStyle, reason) {
if (!newStyle.id) return;
if (reason === 'editSave' || reason === 'config') return;
style = newStyle;
document.querySelector('.style-settings').disabled = false;
inputs.forEach(i => i.update());
}
function createRadio(selector, getter, setter) {
const els = document.querySelectorAll(selector);
for (const el of els) {
el.addEventListener('change', e => {
if (el.checked) {
setter(e);
}
});
}
return {
update() {
for (const el of els) {
if (el.value === getter()) {
el.checked = true;
}
}
},
};
}
function createInput(selector, getter, setter) {
const el = document.querySelector(selector);
el.addEventListener('change', setter);
return {
update() {
el.value = getter();
},
};
}
}

View File

@ -70,6 +70,9 @@ function SourceEditor() {
messageBoxProxy.alert(t('usercssAvoidOverwriting'), 'danger', t('genericError')); messageBoxProxy.alert(t('usercssAvoidOverwriting'), 'danger', t('genericError'));
} else { } else {
res = await API.usercss.editSave({customName, enabled, id, sourceCode}); res = await API.usercss.editSave({customName, enabled, id, sourceCode});
if (!id) {
editor.emit('styleChange', res.style, 'new');
}
// Awaiting inside `try` so that exceptions go to our `catch` // Awaiting inside `try` so that exceptions go to our `catch`
await replaceStyle(res.style); await replaceStyle(res.style);
} }
@ -113,6 +116,26 @@ function SourceEditor() {
if (!$isTextInput(document.activeElement)) { if (!$isTextInput(document.activeElement)) {
cm.focus(); cm.focus();
} }
editor.on('styleToggled', newStyle => {
if (dirty.isDirty()) {
editor.toggleStyle(newStyle.enabled);
} else {
style.enabled = newStyle.enabled;
}
updateMeta();
updateLivePreview();
});
editor.on('styleChange', (newStyle, reason) => {
if (reason === 'new') return;
if (reason === 'config') {
delete newStyle.sourceCode;
delete newStyle.name;
Object.assign(style, newStyle);
updateLivePreview();
return;
}
replaceStyle(newStyle);
});
async function preprocess(style) { async function preprocess(style) {
const res = await API.usercss.build({ const res = await API.usercss.build({
@ -208,14 +231,12 @@ function SourceEditor() {
cm.setPreprocessor((style.usercssData || {}).preprocessor); cm.setPreprocessor((style.usercssData || {}).preprocessor);
} }
function replaceStyle(newStyle, codeIsUpdated) { function replaceStyle(newStyle) {
dirty.clear('name'); dirty.clear('name');
const sameCode = newStyle.sourceCode === cm.getValue(); const sameCode = newStyle.sourceCode === cm.getValue();
if (sameCode) { if (sameCode) {
savedGeneration = cm.changeGeneration(); savedGeneration = cm.changeGeneration();
dirty.clear('sourceGeneration'); dirty.clear('sourceGeneration');
}
if (codeIsUpdated === false || sameCode) {
updateEnvironment(); updateEnvironment();
dirty.clear('enabled'); dirty.clear('enabled');
updateLivePreview(); updateLivePreview();

28
edit/tab.css Normal file
View File

@ -0,0 +1,28 @@
.tab-container {
display: flex;
flex-direction: column;
}
.tab-bar {
display: flex;
flex-direction: row;
border-bottom: 1px solid silver;
padding: 5px 5px 0 15px;
}
.tab-bar-item {
margin: 0 0.3em;
padding: 0.3em 0.6em;
border: 1px solid silver;
border-bottom: none;
background: silver;
cursor: pointer;
}
.tab-bar-item.active {
background: white;
}
.tab-panel {
flex-grow: 1;
}
.tab-panel > :not(.active) {
display: none;
}

19
edit/tab.js Normal file
View File

@ -0,0 +1,19 @@
'use strict';
(() => {
for (const container of document.querySelectorAll('.tab-container')) {
init(container);
}
function init(container) {
const tabButtons = [...container.querySelector('.tab-bar').children];
const tabPanels = [...container.querySelector('.tab-panel').children];
tabButtons.forEach((button, i) => button.addEventListener('click', () => activate(i)));
function activate(index) {
const toggleActive = (button, i) => button.classList.toggle('active', i === index);
tabButtons.forEach(toggleActive);
tabPanels.forEach(toggleActive);
}
}
})();

37
js/event-emitter.js Normal file
View File

@ -0,0 +1,37 @@
/* exported EventEmitter */
'use strict';
function EventEmitter() {
const listeners = new Map();
return {
on(ev, cb, opt) {
if (!listeners.has(ev)) {
listeners.set(ev, new Map());
}
listeners.get(ev).set(cb, opt);
if (opt && opt.runNow) {
cb();
}
},
off(ev, cb) {
const cbs = listeners.get(ev);
if (cbs) {
cbs.delete(cb);
}
},
emit(ev, ...args) {
const cbs = listeners.get(ev);
if (!cbs) return;
for (const [cb, opt] of cbs.entries()) {
try {
cb(...args);
} catch (err) {
console.error(err);
}
if (opt && opt.once) {
cbs.delete(cb);
}
}
},
};
}