save-as-template button in editor (#1385)

+ keep i18n attributes to use them as CSS selectors
+ reduce flicker when creating a new style
+ split button
This commit is contained in:
tophf 2022-01-19 14:45:45 +03:00 committed by GitHub
parent 594ca3520c
commit cc7eba979e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 95 additions and 59 deletions

View File

@ -1352,6 +1352,9 @@
"retrieveDropboxSync": { "retrieveDropboxSync": {
"message": "Dropbox Import" "message": "Dropbox Import"
}, },
"saveAsTemplate": {
"message": "Save as template"
},
"search": { "search": {
"message": "Search", "message": "Search",
"description": "Label before the search input field in the editor shown on Ctrl-F" "description": "Label before the search input field in the editor shown on Ctrl-F"
@ -1812,10 +1815,6 @@
"usercssReplaceTemplateConfirmation": { "usercssReplaceTemplateConfirmation": {
"message": "Replace the default template for new Usercss styles with the current code?" "message": "Replace the default template for new Usercss styles with the current code?"
}, },
"usercssReplaceTemplateName": {
"message": "Empty @name replaces the default template",
"description": "The text shown after @name when creating a new Usercss style"
},
"usercssReplaceTemplateSectionBody": { "usercssReplaceTemplateSectionBody": {
"message": "Insert code here...", "message": "Insert code here...",
"description": "The code placeholder comment in a new style created by clicking 'Write style' in the popup" "description": "The code placeholder comment in a new style created by clicking 'Write style' in the popup"

View File

@ -271,13 +271,16 @@
</div> </div>
</section> </section>
<section id="actions"> <section id="actions">
<div> <div class="buttons">
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button> <div class="split-btn">
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button
><button class="split-btn-pedal usercss-only" i18n-menu-tpl="saveAsTemplate"></button>
</div>
<button id="beautify" i18n-text="styleBeautify"></button> <button id="beautify" i18n-text="styleBeautify"></button>
<button id="style-settings-btn" i18n-text="settings"></button> <button id="style-settings-btn" i18n-text="settings"></button>
<button id="cancel-button" i18n-title="styleCancelEditLabel"></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="buttons sectioned-only">
<button id="from-mozilla" i18n-text="importLabel"></button> <button id="from-mozilla" i18n-text="importLabel"></button>
<button id="to-mozilla" i18n-text="exportLabel"></button> <button id="to-mozilla" i18n-text="exportLabel"></button>
<a id="to-mozilla-help" class="svg-inline-wrapper" tabindex="0" <a id="to-mozilla-help" class="svg-inline-wrapper" tabindex="0"

View File

@ -260,23 +260,13 @@ input:invalid {
margin-top: .5rem; margin-top: .5rem;
} }
#actions > * { #actions .buttons {
display: inline-flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
}
#mozilla-format-buttons {
display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
} }
#actions > div > a { #actions .buttons > * {
height: min-content;
}
#actions button,
#actions > div > a {
margin: 0 .2rem .5rem 0; margin: 0 .2rem .5rem 0;
} }
@ -1064,13 +1054,6 @@ body.linter-disabled .hidden-unless-compact {
margin-left: 1rem; margin-left: 1rem;
padding: .25rem 0 .5rem; padding: .25rem 0 .5rem;
} }
#actions {
display: flex;
flex-wrap: wrap;
white-space: nowrap;
margin: 0;
box-sizing: border-box;
}
#header input[type="checkbox"] { #header input[type="checkbox"] {
vertical-align: middle; vertical-align: middle;
} }

View File

@ -4,6 +4,7 @@
/* global SectionsEditor */ /* global SectionsEditor */
/* global SourceEditor */ /* global SourceEditor */
/* global baseInit */ /* global baseInit */
/* global chromeSync */// storage-util.js
/* global clipString createHotkeyInput helpPopup */// util.js /* global clipString createHotkeyInput helpPopup */// util.js
/* global closeCurrentTab deepEqual sessionStore tryJSONparse */// toolbox.js /* global closeCurrentTab deepEqual sessionStore tryJSONparse */// toolbox.js
/* global cmFactory */ /* global cmFactory */
@ -16,7 +17,10 @@
//#region init //#region init
baseInit.ready.then(async () => { baseInit.ready.then(async () => {
await waitForSheet(); [editor.template] = await Promise.all([
editor.isUsercss && !editor.style.id && chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate),
waitForSheet(),
]);
(editor.isUsercss ? SourceEditor : SectionsEditor)(); (editor.isUsercss ? SourceEditor : SectionsEditor)();
await editor.ready; await editor.ready;
editor.ready = true; editor.ready = true;

View File

@ -17,20 +17,19 @@
function SourceEditor() { function SourceEditor() {
const {style, /** @type DirtyReporter */dirty} = editor; const {style, /** @type DirtyReporter */dirty} = editor;
let savedGeneration; let savedGeneration;
let placeholderName = '';
let prevMode = NaN; let prevMode = NaN;
$$remove('.sectioned-only'); $$remove('.sectioned-only');
$('#header').on('wheel', headerOnScroll); $('#header').on('wheel', headerOnScroll);
$('#sections').textContent = ''; $('#sections').textContent = '';
$('#sections').appendChild($create('.single-editor')); $('#sections').appendChild($create('.single-editor'));
$('#save-button').onauxclick = e => e.detail === 'tpl' && saveTemplate();
if (!style.id) setupNewStyle(style);
const cm = cmFactory.create($('.single-editor')); const cm = cmFactory.create($('.single-editor'));
const sectionFinder = MozSectionFinder(cm); const sectionFinder = MozSectionFinder(cm);
const sectionWidget = MozSectionWidget(cm, sectionFinder); const sectionWidget = MozSectionWidget(cm, sectionFinder);
editor.livePreview.init(preprocess); editor.livePreview.init(preprocess);
if (!style.id) setupNewStyle();
createMetaCompiler(meta => { createMetaCompiler(meta => {
style.usercssData = meta; style.usercssData = meta;
style.name = meta.name; style.name = meta.name;
@ -75,13 +74,7 @@ function SourceEditor() {
} }
showLog(res); showLog(res);
} catch (err) { } catch (err) {
const i = err.index; showSaveError(err);
const isNameEmpty = i > 0 &&
err.code === 'missingValue' &&
sourceCode.slice(sourceCode.lastIndexOf('\n', i - 1), i).trim().endsWith('@name');
return isNameEmpty
? saveTemplate(sourceCode)
: showSaveError(err);
} }
}, },
scrollToEditor: () => {}, scrollToEditor: () => {},
@ -160,7 +153,7 @@ function SourceEditor() {
return name; return name;
} }
async function setupNewStyle(style) { function setupNewStyle() {
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) + style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) +
`/* ${t('usercssReplaceTemplateSectionBody')} */`; `/* ${t('usercssReplaceTemplateSectionBody')} */`;
let section = MozDocMapper.styleToCss(style); let section = MozDocMapper.styleToCss(style);
@ -177,17 +170,11 @@ function SourceEditor() {
@author Me @author Me
==/UserStyle== */ ==/UserStyle== */
`.replace(/^\s+/gm, ''); `.replace(/^\s+/gm, '');
style.name = [style.name, new Date().toLocaleString()].filter(Boolean).join(' - ');
dirty.clear('sourceGeneration');
style.sourceCode = '';
placeholderName = `${style.name || t('usercssReplaceTemplateName')} - ${new Date().toLocaleString()}`;
let code = await chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate);
code = code || DEFAULT_CODE;
code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) =>
`${str}${space ? '' : ' '}${placeholderName}`);
// strip the last dummy section if any, add an empty line followed by the section // strip the last dummy section if any, add an empty line followed by the section
style.sourceCode = code.replace(/\s*@-moz-document[^{]*{[^}]*}\s*$|\s+$/g, '') + '\n\n' + section; style.sourceCode = (editor.template || DEFAULT_CODE)
.replace(/(@name)(?:([\t\x20]+).*|\n)/, (_, k, space) => `${k}${space || ' '}${style.name}`)
.replace(/\s*@-moz-document[^{]*{[^}]*}\s*$|\s+$/g, '') + '\n\n' + section;
cm.startOperation(); cm.startOperation();
cm.setValue(style.sourceCode); cm.setValue(style.sourceCode);
cm.clearHistory(); cm.clearHistory();
@ -199,9 +186,7 @@ function SourceEditor() {
function updateMeta() { function updateMeta() {
const name = style.customName || style.name; const name = style.customName || style.name;
if (name !== placeholderName) { $('#name').value = name;
$('#name').value = name;
}
$('#enabled').checked = style.enabled; $('#enabled').checked = style.enabled;
$('#url').href = style.url; $('#url').href = style.url;
editor.updateName(); editor.updateName();
@ -236,9 +221,10 @@ function SourceEditor() {
} }
} }
async function saveTemplate(code) { async function saveTemplate() {
if (await messageBoxProxy.confirm(t('usercssReplaceTemplateConfirmation'))) { if (await messageBoxProxy.confirm(t('usercssReplaceTemplateConfirmation'))) {
const key = chromeSync.LZ_KEY.usercssTemplate; const key = chromeSync.LZ_KEY.usercssTemplate;
const code = cm.getValue();
await chromeSync.setLZValue(key, code); await chromeSync.setLZValue(key, code);
if (await chromeSync.getLZValue(key) !== code) { if (await chromeSync.getLZValue(key) !== code) {
messageBoxProxy.alert(t('syncStorageErrorSaving')); messageBoxProxy.alert(t('syncStorageErrorSaving'));

View File

@ -33,6 +33,7 @@ button {
border: 1px solid hsl(0, 0%, 62%); border: 1px solid hsl(0, 0%, 62%);
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
line-height: 1.2;
color: #000; color: #000;
background-color: hsl(0, 0%, 100%); background-color: hsl(0, 0%, 100%);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAeCAYAAADtlXTHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QwGBBwIHvKt6QAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAL0lEQVQI12NoaGgQZ2JgYGBkYmBgYGZiYGBggrMY4VxsYsyoskQQCB2MWAxAMhkADVECDhlW9CoAAAAASUVORK5CYII='); background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAeCAYAAADtlXTHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QwGBBwIHvKt6QAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAL0lEQVQI12NoaGgQZ2JgYGBkYmBgYGZiYGBggrMY4VxsYsyoskQQCB2MWAxAMhkADVECDhlW9CoAAAAASUVORK5CYII=');
@ -307,6 +308,39 @@ body.resizing-v > * {
} }
/* header resizer - end */ /* header resizer - end */
.split-btn {
position: relative;
}
.split-btn-pedal {
margin-left: -1px !important;
padding-left: .2em !important;
padding-right: .2em !important;
}
.split-btn-pedal::after {
content: '\25BC'; /* down triangle */
font-size: 90%;
}
.split-btn-pedal.active {
box-shadow: inset 0 0 100px rgba(0, 0, 0, .2);
}
.split-btn-menu {
background: #fff;
position: absolute;
box-shadow: 2px 3px 7px rgba(0, 0, 0, .5);
border: 1px solid hsl(180deg, 50%, 50%);
white-space: nowrap;
cursor: pointer;
padding: .25em 0;
}
.split-btn-menu > * {
padding: .5em 1em;
display: block;
}
.split-btn-menu > :hover {
background-color: hsla(180deg, 50%, 50%, .25);
color: #000;
}
@supports (-moz-appearance: none) { @supports (-moz-appearance: none) {
.moz-appearance-bug .svg-icon.checked, .moz-appearance-bug .svg-icon.checked,
.moz-appearance-bug .onoffswitch input, .moz-appearance-bug .onoffswitch input,

View File

@ -1,4 +1,4 @@
/* global $ $$ focusAccessibility getEventKeyName */// dom.js /* global $$ $ $create focusAccessibility getEventKeyName moveFocus */// dom.js
/* global debounce */// toolbox.js /* global debounce */// toolbox.js
/* global t */// localization.js /* global t */// localization.js
'use strict'; 'use strict';
@ -6,13 +6,14 @@
/** DOM housekeeping after a page finished loading */ /** DOM housekeeping after a page finished loading */
(() => { (() => {
const SPLIT_BTN_MENU = '.split-btn-menu';
splitLongTooltips(); splitLongTooltips();
addTooltipsToEllipsized(); addTooltipsToEllipsized();
window.on('mousedown', suppressFocusRingOnClick, {passive: true}); window.on('mousedown', suppressFocusRingOnClick, {passive: true});
window.on('keydown', keepFocusRingOnTabbing, {passive: true}); window.on('keydown', keepFocusRingOnTabbing, {passive: true});
window.on('keypress', clickDummyLinkOnEnter); window.on('keypress', clickDummyLinkOnEnter);
window.on('wheel', changeFocusedInputOnWheel, {capture: true, passive: false}); window.on('wheel', changeFocusedInputOnWheel, {capture: true, passive: false});
window.on('click', showTooltipNote); window.on('click', e => splitMenu(e) || showTooltipNote(e));
window.on('resize', () => debounce(addTooltipsToEllipsized, 100)); window.on('resize', () => debounce(addTooltipsToEllipsized, 100));
// Removing transition-suppressor rule // Removing transition-suppressor rule
const {sheet} = $('link[href$="global.css"]'); const {sheet} = $('link[href$="global.css"]');
@ -78,19 +79,44 @@
let el = document.activeElement; let el = document.activeElement;
if (el) { if (el) {
el = el.closest('[data-focused-via-click]'); el = el.closest('[data-focused-via-click]');
if (el) delete el.dataset.focusedViaClick; focusAccessibility.toggle(el, false);
} }
}); });
} }
} }
function splitMenu(event) {
const prevMenu = $(SPLIT_BTN_MENU);
const prevPedal = (prevMenu || {}).previousElementSibling;
const pedal = event.target.closest('.split-btn-pedal');
const entry = prevMenu && event.target.closest(SPLIT_BTN_MENU + '>*');
if (prevMenu) prevMenu.remove();
if (prevPedal) prevPedal.classList.remove('active');
if (pedal && pedal !== prevPedal) {
const menu = $create(SPLIT_BTN_MENU,
Array.from(pedal.attributes, ({name, value}) =>
name.startsWith('menu-') &&
$create('a', {tabIndex: 0, __cmd: name.split('-').pop()}, value)
));
menu.on('focusout', e => e.target === menu && splitMenu(e));
pedal.classList.toggle('active');
pedal.after(menu);
moveFocus(menu, 0);
focusAccessibility.toggle(menu.firstChild, focusAccessibility.get(pedal));
}
if (entry) {
prevPedal.previousElementSibling.dispatchEvent(new CustomEvent('auxclick', {
detail: entry.__cmd,
bubbles: true,
}));
}
}
function suppressFocusRingOnClick({target}) { function suppressFocusRingOnClick({target}) {
const el = focusAccessibility.closest(target); const el = focusAccessibility.closest(target);
if (el) { if (el) {
focusAccessibility.lastFocusedViaClick = true; focusAccessibility.lastFocusedViaClick = true;
if (el.dataset.focusedViaClick === undefined) { focusAccessibility.toggle(el, true);
el.dataset.focusedViaClick = '';
}
} }
} }

View File

@ -31,6 +31,8 @@ Object.assign(EventTarget.prototype, {
const focusAccessibility = { const focusAccessibility = {
// last event's focusedViaClick // last event's focusedViaClick
lastFocusedViaClick: false, lastFocusedViaClick: false,
get: el => el && el.dataset.focusedViaClick != null,
toggle: (el, state) => el && toggleDataset(el, 'focusedViaClick', state),
// to avoid a full layout recalc due to changes on body/root // to avoid a full layout recalc due to changes on body/root
// we modify the closest focusable element (like input or button or anything with tabindex=0) // we modify the closest focusable element (like input or button or anything with tabindex=0)
closest(el) { closest(el) {

View File

@ -73,7 +73,6 @@ Object.assign(t, {
if (toInsert) { if (toInsert) {
node.insertBefore(toInsert, before || null); node.insertBefore(toInsert, before || null);
} }
node.removeAttribute(name);
} }
} }
}, },