show style settings in a dialog (#1374)

+ simplify css/html
+ save button and autosave checkbox just like in config-dialog
+ generalize can-close-on-esc
+ add `props` parameter to helpPopup.show
+ deduplicate usage of #help-popup id
+ uniform padding in popups
+ disambiguate style settings from editor options
This commit is contained in:
tophf 2021-12-29 22:57:22 +03:00 committed by GitHub
parent 8128100cef
commit 906508832b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 248 additions and 309 deletions

View File

@ -378,8 +378,8 @@
"editorCodeLabel": { "editorCodeLabel": {
"message": "Code" "message": "Code"
}, },
"editorSettingLabel": { "editorSettings": {
"message": "Settings" "message": "Editor settings"
}, },
"enableStyleLabel": { "enableStyleLabel": {
"message": "Enable", "message": "Enable",
@ -1449,6 +1449,10 @@
"message": "Sections", "message": "Sections",
"description": "Header for the table of contents block listing style section names in the left panel of the classic editor" "description": "Header for the table of contents block listing style section names in the left panel of the classic editor"
}, },
"settings": {
"message": "Settings",
"description": "Generic label/title for settings"
},
"shortcuts": { "shortcuts": {
"message": "Shortcuts", "message": "Shortcuts",
"description": "Go to shortcut configuration" "description": "Go to shortcut configuration"
@ -1620,6 +1624,10 @@
"message": "Save", "message": "Save",
"description": "Label for save button for style editing" "description": "Label for save button for style editing"
}, },
"styleSettings": {
"message": "Style settings",
"description": "Label/title for style settings dialog"
},
"styleToMozillaFormatHelp": { "styleToMozillaFormatHelp": {
"message": "The Mozilla format of the code can be submitted to userstyles.org and used with the classic Stylish for Firefox", "message": "The Mozilla format of the code can be submitted to userstyles.org and used with the classic Stylish for Firefox",
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format" "description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"

102
edit.html
View File

@ -17,7 +17,6 @@
<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>
@ -62,7 +61,6 @@
<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">
@ -216,11 +214,11 @@
</template> </template>
<template data-id="keymapHelp"> <template data-id="keymapHelp">
<table class="keymap-list"> <table class="keymap-list can-close-on-esc">
<thead> <thead>
<tr> <tr>
<th><input i18n-placeholder="helpKeyMapHotkey" type="search" class="can-close-on-esc"></th> <th><input i18n-placeholder="helpKeyMapHotkey" type="search"></th>
<th><input i18n-placeholder="helpKeyMapCommand" type="search" class="can-close-on-esc" spellcheck="false"></th> <th><input i18n-placeholder="helpKeyMapCommand" type="search"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -232,6 +230,44 @@
</table> </table>
</template> </template>
<template data-id="styleSettings">
<div>
<fieldset class="style-settings can-close-on-esc">
<label i18n-text="styleUpdateUrlLabel">
<input id="ss-update-url" type="text">
</label>
<div>
<div i18n-text="installPreferSchemeLabel"></div>
<label i18n-text-append="installPreferSchemeNone">
<input name="ss-scheme" type="radio" value="none">
</label>
<label i18n-text-append="installPreferSchemeDark">
<input name="ss-scheme" type="radio" value="dark">
</label>
<label i18n-text-append="installPreferSchemeLight">
<input name="ss-scheme" type="radio" value="light">
</label>
</div>
<label i18n-text="styleIncludeLabel">
<textarea id="ss-inclusions" spellcheck="false"
placeholder="*://site1.com/*&#10;*://site2.com/*"></textarea>
</label>
<label i18n-text="styleExcludeLabel">
<textarea id="ss-exclusions" spellcheck="false"
placeholder="*://site1.com/*&#10;*://site2.com/*"></textarea>
</label>
</fieldset>
<div class="buttons">
<button id="ss-save" i18n-text="confirmSave" disabled></button>
<label i18n-title="configOnChangeTooltip" i18n-text-append="configOnChange">
<input id="config.autosave" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<button id="ss-close" i18n-text="confirmClose"></button>
</div>
</div>
</template>
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet"> <link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet"> <link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
<link href="vendor/codemirror/addon/fold/foldgutter.css" rel="stylesheet"> <link href="vendor/codemirror/addon/fold/foldgutter.css" rel="stylesheet">
@ -241,8 +277,6 @@
<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">
@ -278,7 +312,8 @@
<div> <div>
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button> <button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button>
<button id="beautify" i18n-text="styleBeautify"></button> <button id="beautify" i18n-text="styleBeautify"></button>
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a> <button id="style-settings-btn" i18n-text="settings"></button>
<button id="cancel-button" i18n-title="styleCancelEditLabel"></button>
</div> </div>
<div id="mozilla-format-buttons" class="sectioned-only"> <div id="mozilla-format-buttons" class="sectioned-only">
<button id="from-mozilla" i18n-text="importLabel"></button> <button id="from-mozilla" i18n-text="importLabel"></button>
@ -291,7 +326,7 @@
</section> </section>
<div id="details-wrapper"> <div id="details-wrapper">
<details id="options" data-pref="editor.options.expanded" class="ignore-pref-if-compact"> <details id="options" data-pref="editor.options.expanded" class="ignore-pref-if-compact">
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary> <summary><h2 id="options-heading" i18n-text="editorSettings"></h2></summary>
<div id="options-wrapper"> <div id="options-wrapper">
<div class="options-column"> <div class="options-column">
<div class="option"> <div class="option">
@ -437,53 +472,7 @@
target="_blank"></a> target="_blank"></a>
</div> </div>
</div> </div>
<div class="main tab-container"> <section id="sections"></section>
<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 spellcheck="false" placeholder="*://site1.com/*&#10;*://site2.com/*"></textarea>
</label>
<label class="form-group style-exclude">
<span class="form-label" i18n-text="styleExcludeLabel"></span>
<textarea spellcheck="false" placeholder="*://site1.com/*&#10;*://site2.com/*"></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>
@ -528,6 +517,5 @@
</symbol> </symbol>
</svg> </svg>
<script src="edit/tab.js"></script>
</body> </body>
</html> </html>

View File

@ -14,14 +14,13 @@
tryJSONparse tryJSONparse
tryURL tryURL
*/// toolbox.js */// toolbox.js
/* global EventEmitter */
'use strict'; 'use strict';
/** /**
* @type Editor * @type Editor
* @namespace Editor * @namespace Editor
*/ */
const editor = Object.assign(EventEmitter(), { const editor = {
style: null, style: null,
dirty: DirtyReporter(), dirty: DirtyReporter(),
isUsercss: false, isUsercss: false,
@ -36,7 +35,9 @@ const editor = Object.assign(EventEmitter(), {
previewDelay: 200, // Chrome devtools uses 200 previewDelay: 200, // Chrome devtools uses 200
scrollInfo: null, scrollInfo: null,
onStyleUpdated() { cancel: () => location.assign('/manage.html'),
updateClass() {
document.documentElement.classList.toggle('is-new-style', !editor.style.id); document.documentElement.classList.toggle('is-new-style', !editor.style.id);
}, },
@ -48,7 +49,7 @@ const editor = Object.assign(EventEmitter(), {
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
@ -90,7 +91,7 @@ const baseInit = (() => {
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded // switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss')); editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
editor.style = style; editor.style = style;
editor.onStyleUpdated(); editor.updateClass();
editor.updateTitle(false); editor.updateTitle(false);
document.documentElement.classList.toggle('usercss', editor.isUsercss); document.documentElement.classList.toggle('usercss', editor.isUsercss);
sessionStore.justEditedStyleId = style.id || ''; sessionStore.justEditedStyleId = style.id || '';
@ -292,16 +293,10 @@ baseInit.ready.then(() => {
} }
} }
getOwnTab().then(async tab => { getOwnTab().then(tab => {
ownTabId = tab.id; ownTabId = tab.id;
// use browser history back when 'back to manage' is clicked
if (sessionStore['manageStylesHistory' + ownTabId] === location.href) { if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
await baseInit.domReady; editor.cancel = () => history.back();
$('#cancel-button').onclick = event => {
event.stopPropagation();
event.preventDefault();
history.back();
};
} }
}); });

View File

@ -65,7 +65,7 @@ function beautifyEditor(cm, options, ui) {
window.scrollTo(scrollX, scrollY); window.scrollTo(scrollX, scrollY);
cm.beautifyChange[cm.changeGeneration()] = true; cm.beautifyChange[cm.changeGeneration()] = true;
if (ui) { if (ui) {
$('#help-popup button[role="close"]').disabled = false; $('button[role="close"]', helpPopup.div).disabled = false;
} }
} }
} }
@ -87,7 +87,7 @@ function createBeautifyUI(scope, options) {
$create('span', t('styleBeautifyHint') + '\u00A0'), $create('span', t('styleBeautifyHint') + '\u00A0'),
createHotkeyInput('editor.beautify.hotkey', { createHotkeyInput('editor.beautify.hotkey', {
buttons: false, buttons: false,
onDone: () => moveFocus($('#help-popup'), 0), onDone: () => moveFocus(helpPopup.div, 0),
}), }),
]), ]),
$create('.buttons', [ $create('.buttons', [
@ -113,9 +113,10 @@ function createBeautifyUI(scope, options) {
}, },
}, t(scope.length === 1 ? 'undo' : 'undoGlobal')), }, t(scope.length === 1 ? 'undo' : 'undoGlobal')),
]), ]),
])); ]),
{
$('#help-popup').className = 'wide'; className: 'wide',
});
$('.beautify-options').onchange = ({target}) => { $('.beautify-options').onchange = ({target}) => {
const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0; const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;

View File

@ -34,6 +34,7 @@ a:hover {
} }
html.is-new-style #preview-label, html.is-new-style #preview-label,
html.is-new-style #style-settings-btn,
html.is-new-style #publish, html.is-new-style #publish,
.hidden { .hidden {
display: none !important; display: none !important;
@ -105,11 +106,8 @@ label {
#header h1 { #header h1 {
margin-top: 0; margin-top: 0;
} }
.main {
padding-left: 280px;
height: 100%;
}
#sections { #sections {
padding-left: 280px;
min-height: 0; min-height: 0;
height: 100%; height: 100%;
} }
@ -284,10 +282,6 @@ input:invalid {
margin: 0 .2rem .5rem 0; margin: 0 .2rem .5rem 0;
} }
#actions #cancel-button {
margin: 0;
}
#options:not([open]) + #lint h2 { #options:not([open]) + #lint h2 {
margin-top: 0; margin-top: 0;
} }
@ -417,10 +411,6 @@ input:invalid {
.edit-actions button { .edit-actions button {
margin-right: .2rem; margin-right: .2rem;
} }
.dirty > label::before {
content: "*";
font-weight: bold;
}
#sections { #sections {
counter-reset: codebox; counter-reset: codebox;
} }
@ -736,6 +726,9 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
} }
/************ help popup ************/ /************ help popup ************/
#help-popup { #help-popup {
--pad-x: 1.5rem;
--pad-y: 1rem;
--pad-y2: calc(var(--pad-y) / 1.5);
top: 3rem; top: 3rem;
right: 3rem; right: 3rem;
max-width: 50vw; max-width: 50vw;
@ -743,7 +736,7 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
display: none; display: none;
background-color: white; background-color: white;
box-shadow: 3px 3px 30px rgba(0, 0, 0, 0.5); box-shadow: 3px 3px 30px rgba(0, 0, 0, 0.5);
padding: 0.5rem; padding: var(--pad-y) var(--pad-x) 0;
z-index: 99; z-index: 99;
} }
#help-popup.big, #help-popup.big,
@ -761,22 +754,19 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
#help-popup .title { #help-popup .title {
font-weight: bold; font-weight: bold;
background-color: rgba(0,0,0,0.05); background-color: rgba(0,0,0,0.05);
margin: -0.5rem -0.5rem 0.5rem; margin: calc(-1 * var(--pad-y)) calc(-1 * var(--pad-x)) 0;
padding: .5rem 32px .5rem .5rem; padding: var(--pad-y2) var(--pad-x);
} }
#help-popup .contents { #help-popup .contents {
max-height: calc(100vh - 8rem); max-height: calc(100vh - 8rem);
overflow-y: auto; overflow-y: auto;
} padding: var(--pad-y) 0;
#help-popup .settings {
min-width: 500px;
min-height: 200px;
max-width: 48vw;
} }
#help-popup .dismiss { #help-popup .dismiss {
position: absolute; position: absolute;
right: 4px; right: 0;
top: .5em; top: 0;
padding: var(--pad-y2) .5em;
} }
#help-popup input[type="search"], #help-popup input[type="search"],
#help-popup .CodeMirror { #help-popup .CodeMirror {
@ -804,12 +794,14 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
} }
#help-popup .buttons { #help-popup .buttons {
text-align: center; display: flex;
margin-top: .75em; justify-content: center;
align-items: center;
margin: var(--pad-y2) 0 calc(var(--pad-y2) - var(--pad-y)) 0;
} }
.non-windows #help-popup .buttons { .non-windows #help-popup .buttons {
direction: rtl; direction: rtl;
text-align: right; justify-content: start;
} }
#help-popup button[name^="import"] { #help-popup button[name^="import"] {
line-height: 1.5rem; line-height: 1.5rem;
@ -831,8 +823,8 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
#help-popup .rules p { #help-popup .rules p {
margin: .25em 0; margin: .25em 0;
} }
#help-popup .buttons button:nth-child(n + 2) { #help-popup .buttons > :nth-child(n + 2) {
margin-left: .5em; margin-inline: .5em 0;
} }
/************ lint ************/ /************ lint ************/
@ -1180,16 +1172,13 @@ body.linter-disabled .hidden-unless-compact {
#lint:not([open]) + #footer { #lint:not([open]) + #footer {
margin: .25em 0 -1em .25em; margin: .25em 0 -1em .25em;
} }
.main { #sections {
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 msg API */// msg.js /* global API msg */// msg.js
/* global CodeMirror */ /* global CodeMirror */
/* global SectionsEditor */ /* global SectionsEditor */
/* global SourceEditor */ /* global SourceEditor */
@ -11,7 +11,6 @@
/* 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
@ -19,7 +18,6 @@
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);
@ -32,6 +30,7 @@ baseInit.ready.then(async () => {
// enabling after init to prevent flash of validation failure on an empty name // enabling after init to prevent flash of validation failure on an empty name
$('#name').required = !editor.isUsercss; $('#name').required = !editor.isUsercss;
$('#save-button').onclick = editor.save; $('#save-button').onclick = editor.save;
$('#cancel-button').onclick = editor.cancel;
const elSec = $('#sections-list'); const elSec = $('#sections-list');
// editor.toc.expanded pref isn't saved in compact-layout so prefs.subscribe won't work // editor.toc.expanded pref isn't saved in compact-layout so prefs.subscribe won't work
@ -48,6 +47,11 @@ baseInit.ready.then(async () => {
require(['/edit/linter-dialogs'], () => linterMan.showLintConfig()); require(['/edit/linter-dialogs'], () => linterMan.showLintConfig());
$('#lint-help').onclick = () => $('#lint-help').onclick = () =>
require(['/edit/linter-dialogs'], () => linterMan.showLintHelp()); require(['/edit/linter-dialogs'], () => linterMan.showLintHelp());
$('#style-settings-btn').onclick = () => require([
'/edit/settings.css',
'/edit/settings', /* global StyleSettings */
], () => StyleSettings());
require([ require([
'/edit/autocomplete', '/edit/autocomplete',
'/edit/global-search', '/edit/global-search',
@ -70,14 +74,7 @@ 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)) {
if (request.reason === 'toggle') { handleExternalUpdate(request);
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':
@ -91,6 +88,31 @@ msg.onExtension(request => {
} }
}); });
async function handleExternalUpdate({style, reason}) {
if (reason === 'toggle') {
if (editor.dirty.isDirty()) {
editor.toggleStyle(style.enabled);
} else {
Object.assign(editor.style, style);
}
editor.updateMeta();
editor.updateLivePreview();
return;
}
style = await API.styles.get(style.id);
if (reason === 'config') {
delete style.sourceCode;
delete style.sections;
delete style.name;
delete style.enabled;
Object.assign(editor.style, style);
editor.updateLivePreview();
} else {
await editor.replaceStyle(style);
}
window.dispatchEvent(new Event('styleSettings'));
}
window.on('beforeunload', e => { window.on('beforeunload', e => {
let pos; let pos;
if (editor.isWindowed && if (editor.isWindowed &&
@ -169,7 +191,7 @@ window.on('beforeunload', e => {
} }
}, },
toggleStyle(enabled = style.enabled) { toggleStyle(enabled = !style.enabled) {
$('#enabled').checked = enabled; $('#enabled').checked = enabled;
editor.updateEnabledness(enabled); editor.updateEnabledness(enabled);
}, },

View File

@ -23,7 +23,7 @@ function SectionsEditor() {
let headerOffset; // in compact mode the header is at the top so it reduces the available height let headerOffset; // in compact mode the header is at the top so it reduces the available height
let cmExtrasHeight; // resize grip + borders let cmExtrasHeight; // resize grip + borders
updateHeader(); updateMeta();
rerouteHotkeys.toggle(true); // enabled initially because we don't always focus a CodeMirror rerouteHotkeys.toggle(true); // enabled initially because we don't always focus a CodeMirror
editor.livePreview.init(); editor.livePreview.init();
container.classList.add('section-editor'); container.classList.add('section-editor');
@ -44,6 +44,7 @@ function SectionsEditor() {
closestVisible, closestVisible,
updateLivePreview, updateLivePreview,
updateMeta,
getEditors() { getEditors() {
return sections.filter(s => !s.removed).map(s => s.cm); return sections.filter(s => !s.removed).map(s => s.cm);
@ -89,8 +90,8 @@ function SectionsEditor() {
// FIXME: avoid recreating all editors? // FIXME: avoid recreating all editors?
await initSections(newStyle.sections, {replace: true}); await initSections(newStyle.sections, {replace: true});
Object.assign(style, newStyle); Object.assign(style, newStyle);
editor.onStyleUpdated(); editor.updateClass();
updateHeader(); updateMeta();
// 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}`);
@ -108,9 +109,6 @@ 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,28 +126,6 @@ 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;
@ -489,7 +465,7 @@ function SectionsEditor() {
} }
} }
function updateHeader() { function updateMeta() {
$('#name').value = style.customName || style.name || ''; $('#name').value = style.customName || style.name || '';
$('#enabled').checked = style.enabled !== false; $('#enabled').checked = style.enabled !== false;
$('#url').href = style.url || ''; $('#url').href = style.url || '';

View File

@ -1,46 +1,43 @@
#help-popup.style-settings-popup.dirty .title::after {
content: ' *';
}
.compact-layout #help-popup.style-settings-popup {
width: 90%;
}
.style-settings { .style-settings {
padding: 0.7rem 1.7rem; padding: 0;
border: 0; border: 0;
margin: 0; margin: 0;
} }
.form-group { .style-settings > * {
display: block; display: block;
margin: .6em 0; margin: 1rem 0;
padding: 0; padding: 0;
} }
.form-label { .style-settings > :first-child {
display: inline-block; margin-top: 0;
margin: .3em 0;
} }
[disabled] .form-label { .style-settings > :last-child {
opacity: 0.4; margin-bottom: 0;
} }
.form-group input[type=text], .style-settings input[type=radio] {
.form-group input[type=number], margin-left: -.5em; /* compensate for label's 16px margin in edit.css */
.form-group select, }
.form-group textarea { .style-settings input[type=text],
.style-settings input[type=number],
.style-settings select,
.style-settings textarea {
display: block; display: block;
width: 100%; width: 100%;
margin-top: .25em;
box-sizing: border-box; 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-settings textarea { .style-settings textarea {
resize: vertical; resize: vertical;
min-width: 33vw;
min-height: 2.5em; min-height: 2.5em;
max-height: 50vh; max-height: 50vh;
} }
.style-settings textarea:not(:placeholder-shown) {
min-width: 50vw;
}

View File

@ -1,84 +1,97 @@
/* global $ $$ */// dom.js /* global $ $$ moveFocus setupLivePrefs */// dom.js
/* global API */// msg.js /* global API */// msg.js
/* global editor */
/* global helpPopup */// util.js
/* global t */// localization.js
/* global debounce */// toolbox.js
/* exported StyleSettings */ /* exported StyleSettings */
'use strict'; 'use strict';
function StyleSettings(editor) { function StyleSettings() {
let {style} = editor; const AUTOSAVE_DELAY = 500; // same as config-dialog.js
const SS_ID = 'styleSettings';
const inputs = [ const {style} = editor;
createInput('.style-update-url input', () => style.updateUrl || '', const ui = t.template[SS_ID].cloneNode(true);
e => API.styles.config(style.id, 'updateUrl', e.target.value)), const elAuto = $('[id="config.autosave"]', ui);
createRadio('.style-prefer-scheme input', () => style.preferScheme || 'none', const elSave = $('#ss-save', ui);
e => API.styles.config(style.id, 'preferScheme', e.target.value)), const pendingSetters = new Map();
...[ const updaters = [
['.style-include', 'inclusions'], initInput('#ss-update-url', () => style.updateUrl || '',
['.style-exclude', 'exclusions'], val => API.styles.config(style.id, 'updateUrl', val)),
].map(createArea), initRadio('ss-scheme', () => style.preferScheme || 'none',
val => API.styles.config(style.id, 'preferScheme', val)),
initArea('inclusions'),
initArea('exclusions'),
]; ];
update();
window.on(SS_ID, update);
window.on('closeHelp', () => window.off(SS_ID, update), {once: true});
helpPopup.show(t(SS_ID), ui, {
className: 'style-settings-popup',
});
elSave.onclick = save;
$('#ss-close', ui).onclick = helpPopup.close;
setupLivePrefs([elAuto.id]);
moveFocus(ui, 0);
update(style); function autosave(el, setter) {
pendingSetters.set(el, setter);
editor.on('styleChange', update); helpPopup.div.classList.add('dirty');
elSave.disabled = false;
function textToList(text) { if (elAuto.checked) debounce(save, AUTOSAVE_DELAY);
const list = text.split(/\s*\r?\n\s*/g);
return list.filter(Boolean);
} }
function update(newStyle, reason) { function initArea(type) {
if (!newStyle.id) return; const selector = `#ss-${type}`;
if (reason === 'editSave') return; const el = $(selector, ui);
style = newStyle; el.oninput = () => {
$('.style-settings').disabled = false;
inputs.forEach(i => i.update());
}
function createArea([parentSel, type]) {
const sel = parentSel + ' textarea';
const el = $(sel);
el.on('input', () => {
const val = el.value; const val = el.value;
el.rows = val.match(/^/gm).length + !val.endsWith('\n'); el.rows = val.match(/^/gm).length + !val.endsWith('\n');
}); };
return createInput(sel, return initInput(selector,
() => { () => {
const list = style[type] || []; const list = style[type] || [];
const text = list.join('\n'); const text = list.join('\n');
el.rows = (list.length || 1) + 1; el.rows = (list.length || 1) + 1;
return text; return text;
}, },
() => API.styles.config(style.id, type, textToList(el.value)) val => API.styles.config(style.id, type, textToList(val))
); );
} }
function createRadio(selector, getter, setter) { function initInput(selector, getter, setter) {
const els = $$(selector); const el = $(selector, ui);
for (const el of els) { el.oninput = () => autosave(el, setter);
el.addEventListener('change', e => { return () => {
if (el.checked) { const val = getter();
setter(e); // Skipping if unchanged to preserve the Undo history of the input
} if (el.value !== val) el.value = val;
});
}
return {
update() {
for (const el of els) {
if (el.value === getter()) {
el.checked = true;
}
}
},
}; };
} }
function createInput(selector, getter, setter) { function initRadio(name, getter, setter) {
const el = $(selector); for (const el of $$(`[name="${name}"]`, ui)) {
el.addEventListener('change', setter); el.onchange = () => {
return { if (el.checked) autosave(el, setter);
update() {
el.value = getter();
},
}; };
} }
return () => {
$(`[name="${name}"][value="${getter()}"]`, ui).checked = true;
};
}
function save() {
pendingSetters.forEach((fn, el) => fn(el.value));
pendingSetters.clear();
helpPopup.div.classList.remove('dirty');
elSave.disabled = true;
}
function textToList(text) {
return text.split(/\n/).map(s => s.trim()).filter(Boolean);
}
function update() {
updaters.forEach(fn => fn());
}
} }

View File

@ -45,6 +45,7 @@ function SourceEditor() {
sections: sectionFinder.sections, sections: sectionFinder.sections,
replaceStyle, replaceStyle,
updateLivePreview, updateLivePreview,
updateMeta,
closestVisible: () => cm, closestVisible: () => cm,
getEditors: () => [cm], getEditors: () => [cm],
getEditorTitle: () => '', getEditorTitle: () => '',
@ -70,9 +71,6 @@ 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);
} }
@ -116,26 +114,6 @@ 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({
@ -231,7 +209,7 @@ function SourceEditor() {
cm.setPreprocessor((style.usercssData || {}).preprocessor); cm.setPreprocessor((style.usercssData || {}).preprocessor);
} }
function replaceStyle(newStyle) { async 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) {
@ -243,8 +221,8 @@ function SourceEditor() {
return; return;
} }
Promise.resolve(messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))).then(ok => { // TODO: also confirm in sections-editor?
if (!ok) return; if (await messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))) {
updateEnvironment(); updateEnvironment();
if (!sameCode) { if (!sameCode) {
const cursor = cm.getCursor(); const cursor = cm.getCursor();
@ -257,7 +235,7 @@ function SourceEditor() {
updateLivePreview(); updateLivePreview();
} }
dirty.clear(); dirty.clear();
}); }
function updateEnvironment() { function updateEnvironment() {
if (style.id !== newStyle.id) { if (style.id !== newStyle.id) {
@ -265,7 +243,7 @@ function SourceEditor() {
} }
sessionStore.justEditedStyleId = newStyle.id; sessionStore.justEditedStyleId = newStyle.id;
Object.assign(style, newStyle); Object.assign(style, newStyle);
editor.onStyleUpdated(); editor.updateClass();
updateMeta(); updateMeta();
} }
} }

View File

@ -7,20 +7,28 @@
const helpPopup = { const helpPopup = {
show(title = '', body) { /**
* @param {string} title - plain text
* @param {string|Node} body - Node, html or plain text
* @param {Node} [props] - DOM props for the popup element
* @returns {Element} the popup
*/
show(title = '', body, props) {
const div = $('#help-popup'); const div = $('#help-popup');
const contents = $('.contents', div); const contents = $('.contents', div);
div.style = '';
div.className = ''; div.className = '';
contents.textContent = ''; contents.textContent = '';
Object.assign(div, props);
if (body) { if (body) {
contents.appendChild(typeof body === 'string' ? t.HTML(body) : body); contents.appendChild(typeof body === 'string' ? t.HTML(body) : body);
} }
$('.title', div).textContent = title; $('.title', div).textContent = title;
$('.dismiss', div).onclick = helpPopup.close; $('.dismiss', div).onclick = helpPopup.close;
window.on('keydown', helpPopup.close, true); window.on('keydown', helpPopup.close, true);
// reset any inline styles div.style.display = 'block';
div.style = 'display: block';
helpPopup.originalFocus = document.activeElement; helpPopup.originalFocus = document.activeElement;
helpPopup.div = div;
return div; return div;
}, },
@ -31,11 +39,13 @@ const helpPopup = {
getEventKeyName(event) === 'Escape' && getEventKeyName(event) === 'Escape' &&
!$('.CodeMirror-hints, #message-box') && ( !$('.CodeMirror-hints, #message-box') && (
!document.activeElement || !document.activeElement ||
!document.activeElement.closest('#search-replace-dialog') && !document.activeElement.closest('#search-replace-dialog') && (
document.activeElement.matches(':not(input), .can-close-on-esc') document.activeElement.tagName !== 'INPUT' ||
document.activeElement.closest('.can-close-on-esc')
)
) )
); );
const div = $('#help-popup'); const {div} = helpPopup;
if (!canClose || !div) { if (!canClose || !div) {
return; return;
} }
@ -170,8 +180,7 @@ function createHotkeyInput(prefId, {buttons = true, onDone}) {
/* exported showCodeMirrorPopup */ /* exported showCodeMirrorPopup */
function showCodeMirrorPopup(title, html, options) { function showCodeMirrorPopup(title, html, options) {
const popup = helpPopup.show(title, html); const popup = helpPopup.show(title, html, {className: 'big'});
popup.classList.add('big');
let cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({ let cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({
mode: 'css', mode: 'css',

View File

@ -1,37 +0,0 @@
/* 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);
}
}
},
};
}