From cc7eba979eb9ad0dabed8a7b654b50db085a757c Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 19 Jan 2022 14:45:45 +0300 Subject: [PATCH] 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 --- _locales/en/messages.json | 7 +++---- edit.html | 9 ++++++--- edit/edit.css | 21 ++------------------- edit/edit.js | 6 +++++- edit/source-editor.js | 36 +++++++++++------------------------- global.css | 34 ++++++++++++++++++++++++++++++++++ js/dom-on-load.js | 38 ++++++++++++++++++++++++++++++++------ js/dom.js | 2 ++ js/localization.js | 1 - 9 files changed, 95 insertions(+), 59 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 84649365..4a88c107 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1352,6 +1352,9 @@ "retrieveDropboxSync": { "message": "Dropbox Import" }, + "saveAsTemplate": { + "message": "Save as template" + }, "search": { "message": "Search", "description": "Label before the search input field in the editor shown on Ctrl-F" @@ -1812,10 +1815,6 @@ "usercssReplaceTemplateConfirmation": { "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": { "message": "Insert code here...", "description": "The code placeholder comment in a new style created by clicking 'Write style' in the popup" diff --git a/edit.html b/edit.html index c74a4b66..8907cd5b 100644 --- a/edit.html +++ b/edit.html @@ -271,13 +271,16 @@
-
- +
+
+ +
-
+
* { +#actions .buttons { display: inline-flex; flex-wrap: wrap; -} - -#mozilla-format-buttons { - display: flex; - flex-wrap: wrap; align-items: center; } -#actions > div > a { - height: min-content; -} - -#actions button, -#actions > div > a { +#actions .buttons > * { margin: 0 .2rem .5rem 0; } @@ -1064,13 +1054,6 @@ body.linter-disabled .hidden-unless-compact { margin-left: 1rem; padding: .25rem 0 .5rem; } - #actions { - display: flex; - flex-wrap: wrap; - white-space: nowrap; - margin: 0; - box-sizing: border-box; - } #header input[type="checkbox"] { vertical-align: middle; } diff --git a/edit/edit.js b/edit/edit.js index 19a94ac7..eda76e9b 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -4,6 +4,7 @@ /* global SectionsEditor */ /* global SourceEditor */ /* global baseInit */ +/* global chromeSync */// storage-util.js /* global clipString createHotkeyInput helpPopup */// util.js /* global closeCurrentTab deepEqual sessionStore tryJSONparse */// toolbox.js /* global cmFactory */ @@ -16,7 +17,10 @@ //#region init 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)(); await editor.ready; editor.ready = true; diff --git a/edit/source-editor.js b/edit/source-editor.js index 5683c519..fb118b1c 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -17,20 +17,19 @@ function SourceEditor() { const {style, /** @type DirtyReporter */dirty} = editor; let savedGeneration; - let placeholderName = ''; let prevMode = NaN; $$remove('.sectioned-only'); $('#header').on('wheel', headerOnScroll); $('#sections').textContent = ''; $('#sections').appendChild($create('.single-editor')); - - if (!style.id) setupNewStyle(style); + $('#save-button').onauxclick = e => e.detail === 'tpl' && saveTemplate(); const cm = cmFactory.create($('.single-editor')); const sectionFinder = MozSectionFinder(cm); const sectionWidget = MozSectionWidget(cm, sectionFinder); editor.livePreview.init(preprocess); + if (!style.id) setupNewStyle(); createMetaCompiler(meta => { style.usercssData = meta; style.name = meta.name; @@ -75,13 +74,7 @@ function SourceEditor() { } showLog(res); } catch (err) { - const i = err.index; - const isNameEmpty = i > 0 && - err.code === 'missingValue' && - sourceCode.slice(sourceCode.lastIndexOf('\n', i - 1), i).trim().endsWith('@name'); - return isNameEmpty - ? saveTemplate(sourceCode) - : showSaveError(err); + showSaveError(err); } }, scrollToEditor: () => {}, @@ -160,7 +153,7 @@ function SourceEditor() { return name; } - async function setupNewStyle(style) { + function setupNewStyle() { style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) + `/* ${t('usercssReplaceTemplateSectionBody')} */`; let section = MozDocMapper.styleToCss(style); @@ -177,17 +170,11 @@ function SourceEditor() { @author Me ==/UserStyle== */ `.replace(/^\s+/gm, ''); - - 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}`); + style.name = [style.name, new Date().toLocaleString()].filter(Boolean).join(' - '); // 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.setValue(style.sourceCode); cm.clearHistory(); @@ -199,9 +186,7 @@ function SourceEditor() { function updateMeta() { const name = style.customName || style.name; - if (name !== placeholderName) { - $('#name').value = name; - } + $('#name').value = name; $('#enabled').checked = style.enabled; $('#url').href = style.url; editor.updateName(); @@ -236,9 +221,10 @@ function SourceEditor() { } } - async function saveTemplate(code) { + async function saveTemplate() { if (await messageBoxProxy.confirm(t('usercssReplaceTemplateConfirmation'))) { const key = chromeSync.LZ_KEY.usercssTemplate; + const code = cm.getValue(); await chromeSync.setLZValue(key, code); if (await chromeSync.getLZValue(key) !== code) { messageBoxProxy.alert(t('syncStorageErrorSaving')); diff --git a/global.css b/global.css index 95440557..e9f1b246 100644 --- a/global.css +++ b/global.css @@ -33,6 +33,7 @@ button { border: 1px solid hsl(0, 0%, 62%); font: inherit; font-size: 13px; + line-height: 1.2; color: #000; background-color: hsl(0, 0%, 100%); background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAeCAYAAADtlXTHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QwGBBwIHvKt6QAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAL0lEQVQI12NoaGgQZ2JgYGBkYmBgYGZiYGBggrMY4VxsYsyoskQQCB2MWAxAMhkADVECDhlW9CoAAAAASUVORK5CYII='); @@ -307,6 +308,39 @@ body.resizing-v > * { } /* 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) { .moz-appearance-bug .svg-icon.checked, .moz-appearance-bug .onoffswitch input, diff --git a/js/dom-on-load.js b/js/dom-on-load.js index 828d14c8..634ae5dd 100644 --- a/js/dom-on-load.js +++ b/js/dom-on-load.js @@ -1,4 +1,4 @@ -/* global $ $$ focusAccessibility getEventKeyName */// dom.js +/* global $$ $ $create focusAccessibility getEventKeyName moveFocus */// dom.js /* global debounce */// toolbox.js /* global t */// localization.js 'use strict'; @@ -6,13 +6,14 @@ /** DOM housekeeping after a page finished loading */ (() => { + const SPLIT_BTN_MENU = '.split-btn-menu'; splitLongTooltips(); addTooltipsToEllipsized(); window.on('mousedown', suppressFocusRingOnClick, {passive: true}); window.on('keydown', keepFocusRingOnTabbing, {passive: true}); window.on('keypress', clickDummyLinkOnEnter); 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)); // Removing transition-suppressor rule const {sheet} = $('link[href$="global.css"]'); @@ -78,19 +79,44 @@ let el = document.activeElement; if (el) { 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}) { const el = focusAccessibility.closest(target); if (el) { focusAccessibility.lastFocusedViaClick = true; - if (el.dataset.focusedViaClick === undefined) { - el.dataset.focusedViaClick = ''; - } + focusAccessibility.toggle(el, true); } } diff --git a/js/dom.js b/js/dom.js index 5cb50aeb..a650a387 100644 --- a/js/dom.js +++ b/js/dom.js @@ -31,6 +31,8 @@ Object.assign(EventTarget.prototype, { const focusAccessibility = { // last event's focusedViaClick 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 // we modify the closest focusable element (like input or button or anything with tabindex=0) closest(el) { diff --git a/js/localization.js b/js/localization.js index b413e255..b34497eb 100644 --- a/js/localization.js +++ b/js/localization.js @@ -73,7 +73,6 @@ Object.assign(t, { if (toInsert) { node.insertBefore(toInsert, before || null); } - node.removeAttribute(name); } } },