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:
parent
594ca3520c
commit
cc7eba979e
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'));
|
||||||
|
|
34
global.css
34
global.css
|
@ -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,
|
||||||
|
|
|
@ -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 = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -73,7 +73,6 @@ Object.assign(t, {
|
||||||
if (toInsert) {
|
if (toInsert) {
|
||||||
node.insertBefore(toInsert, before || null);
|
node.insertBefore(toInsert, before || null);
|
||||||
}
|
}
|
||||||
node.removeAttribute(name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user