diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 73b20638..6ed037fb 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1273,6 +1273,10 @@ "message": "Beautify", "description": "Label for the CSS-beautifier button on the edit style page" }, + "styleBeautifyHint": { + "message": "Hint: right-click the “Beautify” button or use the keyboard shortcut defined below to beautify without showing this panel", + "description": "Hint shown inside the CSS-beautifier panel" + }, "styleBeautifyIndentConditional": { "message": "Indent @media, @supports", "description": "CSS-beautifier option" diff --git a/edit/beautify.js b/edit/beautify.js index 5a6048a3..0f097fce 100644 --- a/edit/beautify.js +++ b/edit/beautify.js @@ -1,8 +1,49 @@ /* global loadScript css_beautify showHelp prefs t $ $create */ -/* exported beautify */ +/* global editor createHotkeyInput moveFocus CodeMirror */ +/* exported initBeautifyButton */ 'use strict'; -function beautify(scope) { +const HOTKEY_ID = 'editor.beautify.hotkey'; + +prefs.initializing.then(() => { + CodeMirror.defaults.extraKeys[prefs.get(HOTKEY_ID) || ''] = 'beautify'; + CodeMirror.commands.beautify = cm => { + // using per-section mode when code editor or applies-to block is focused + const isPerSection = cm.display.wrapper.parentElement.contains(document.activeElement); + beautify(isPerSection ? [cm] : editor.getEditors(), false); + }; +}); + +prefs.subscribe([HOTKEY_ID], (key, value) => { + const {extraKeys} = CodeMirror.defaults; + for (const [key, cmd] of Object.entries(extraKeys)) { + if (cmd === 'beautify') { + delete extraKeys[key]; + break; + } + } + if (value) { + extraKeys[value] = 'beautify'; + } +}); + +/** + * @param {HTMLElement} btn - the button element shown in the UI + * @param {function():CodeMirror[]} getScope + */ +function initBeautifyButton(btn, getScope) { + btn.addEventListener('click', () => beautify(getScope())); + btn.addEventListener('contextmenu', e => { + e.preventDefault(); + beautify(getScope(), false); + }); +} + +/** + * @param {CodeMirror[]} scope + * @param {?boolean} ui + */ +function beautify(scope, ui = true) { loadScript('/vendor-overwrites/beautify/beautify-css-mod.js') .then(() => { if (!window.css_beautify && window.exports) { @@ -19,7 +60,41 @@ function beautify(scope) { } options.indent_size = tabs ? 1 : prefs.get('editor.tabSize'); options.indent_char = tabs ? '\t' : ' '; + if (ui) { + createBeautifyUI(scope, options); + } + for (const cm of scope) { + setTimeout(doBeautifyEditor, 0, cm, options); + } + } + function doBeautifyEditor(cm, options) { + const pos = options.translate_positions = + [].concat.apply([], cm.doc.sel.ranges.map(r => + [Object.assign({}, r.anchor), Object.assign({}, r.head)])); + const text = cm.getValue(); + const newText = css_beautify(text, options); + if (newText !== text) { + if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) { + // clear the list if last change wasn't a css-beautify + cm.beautifyChange = {}; + } + cm.setValue(newText); + const selections = []; + for (let i = 0; i < pos.length; i += 2) { + selections.push({anchor: pos[i], head: pos[i + 1]}); + } + const {scrollX, scrollY} = window; + cm.setSelections(selections); + window.scrollTo(scrollX, scrollY); + cm.beautifyChange[cm.changeGeneration()] = true; + if (ui) { + $('#help-popup button[role="close"]').disabled = false; + } + } + } + + function createBeautifyUI(scope, options) { showHelp(t('styleBeautify'), $create([ $create('.beautify-options', [ @@ -32,6 +107,10 @@ function beautify(scope) { $createLabeledCheckbox('preserve_newlines', 'styleBeautifyPreserveNewlines'), $createLabeledCheckbox('indent_conditional', 'styleBeautifyIndentConditional'), ]), + $create('p.beautify-hint', [ + $create('span', t('styleBeautifyHint') + '\u00A0'), + createHotkeyInput(HOTKEY_ID, () => moveFocus($('#help-popup'), 1)), + ]), $create('.buttons', [ $create('button', { attributes: {role: 'close'}, @@ -60,32 +139,6 @@ function beautify(scope) { $('#help-popup').className = 'wide'; - scope.forEach(cm => { - setTimeout(() => { - const pos = options.translate_positions = - [].concat.apply([], cm.doc.sel.ranges.map(r => - [Object.assign({}, r.anchor), Object.assign({}, r.head)])); - const text = cm.getValue(); - const newText = css_beautify(text, options); - if (newText !== text) { - if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) { - // clear the list if last change wasn't a css-beautify - cm.beautifyChange = {}; - } - cm.setValue(newText); - const selections = []; - for (let i = 0; i < pos.length; i += 2) { - selections.push({anchor: pos[i], head: pos[i + 1]}); - } - const {scrollX, scrollY} = window; - cm.setSelections(selections); - window.scrollTo(scrollX, scrollY); - cm.beautifyChange[cm.changeGeneration()] = true; - $('#help-popup button[role="close"]').disabled = false; - } - }); - }); - $('.beautify-options').onchange = ({target}) => { const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0; prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value})); diff --git a/edit/colorpicker-helper.js b/edit/colorpicker-helper.js index cae28510..9736ab00 100644 --- a/edit/colorpicker-helper.js +++ b/edit/colorpicker-helper.js @@ -1,4 +1,4 @@ -/* global CodeMirror showHelp cmFactory onDOMready $ $create prefs t */ +/* global CodeMirror showHelp cmFactory onDOMready $ prefs t createHotkeyInput */ 'use strict'; (() => { @@ -62,46 +62,8 @@ function configureColorpicker(event) { event.preventDefault(); - const input = $create('input', { - type: 'search', - spellcheck: false, - value: prefs.get('editor.colorpicker.hotkey'), - onkeydown(event) { - event.preventDefault(); - event.stopPropagation(); - const key = CodeMirror.keyName(event); - switch (key) { - case 'Enter': - if (this.checkValidity()) { - $('#help-popup .dismiss').onclick(); - } - return; - case 'Esc': - $('#help-popup .dismiss').onclick(); - return; - default: - // disallow: [Shift?] characters, modifiers-only, [modifiers?] + Esc, Tab, nav keys - if (!key || new RegExp('^(' + [ - '(Back)?Space', - '(Shift-)?.', // a single character - '(Shift-?|Ctrl-?|Alt-?|Cmd-?){0,2}(|Esc|Tab|(Page)?(Up|Down)|Left|Right|Home|End|Insert|Delete)', - ].join('|') + ')$', 'i').test(key)) { - this.value = key || this.value; - this.setCustomValidity('Not allowed'); - return; - } - } - this.value = key; - this.setCustomValidity(''); - prefs.set('editor.colorpicker.hotkey', key); - }, - oninput() { - // fired on pressing "x" to clear the field - prefs.set('editor.colorpicker.hotkey', ''); - }, - onpaste(event) { - event.preventDefault(); - } + const input = createHotkeyInput('editor.colorpicker.hotkey', () => { + $('#help-popup .dismiss').onclick(); }); const popup = showHelp(t('helpKeyMapHotkey'), input); if (this instanceof Element) { diff --git a/edit/edit.css b/edit/edit.css index 5a674532..3635fa61 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -779,6 +779,11 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high padding-left: 4px; margin-left: 4px; } +.beautify-hint { + width: 0; + min-width: 100%; + font-size: 90%; +} /************ single editor **************/ .usercss body { diff --git a/edit/edit.js b/edit/edit.js index 56685eb2..b7cf2846 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -1,7 +1,7 @@ /* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch closeCurrentTab messageBox debounce workerUtil - beautify ignoreChromeError + initBeautifyButton ignoreChromeError moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */ /* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */ 'use strict'; @@ -170,10 +170,8 @@ preinit(); $('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle'); $('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName'); $('#name').title = usercss ? t('usercssReplaceTemplateName') : ''; - $('#preview-label').classList.toggle('hidden', !style.id); - - $('#beautify').onclick = () => beautify(editor.getEditors()); + initBeautifyButton($('#beautify'), () => editor.getEditors()); window.addEventListener('resize', () => { debounce(rememberWindowSize, 100); detectLayout(); diff --git a/edit/reroute-hotkeys.js b/edit/reroute-hotkeys.js index 83f57720..8e148b73 100644 --- a/edit/reroute-hotkeys.js +++ b/edit/reroute-hotkeys.js @@ -12,6 +12,7 @@ const rerouteHotkeys = (() => { 'toggleEditorFocus', 'find', 'findNext', 'findPrev', 'replace', 'replaceAll', 'colorpicker', + 'beautify', ]); return rerouteHotkeys; diff --git a/edit/sections-editor-section.js b/edit/sections-editor-section.js index 67880a35..8885cfc6 100644 --- a/edit/sections-editor-section.js +++ b/edit/sections-editor-section.js @@ -1,5 +1,5 @@ /* global template cmFactory $ propertyToCss CssToProperty linter regExpTester - FIREFOX toggleContextMenuDelete beautify showHelp t tryRegExp */ + FIREFOX toggleContextMenuDelete initBeautifyButton showHelp t tryRegExp */ /* exported createSection */ 'use strict'; @@ -94,6 +94,7 @@ function createSection({ const cm = cmFactory.create(wrapper => { el.insertBefore(wrapper, $('.code-label', el).nextSibling); }, {value: originalSection.code}); + el.CodeMirror = cm; // used by getAssociatedEditor const changeListeners = new Set(); @@ -196,12 +197,12 @@ function createSection({ $('.clone-section', el).addEventListener('click', () => insertSectionAfter(getModel(), section)); $('.move-section-up', el).addEventListener('click', () => moveSectionUp(section)); $('.move-section-down', el).addEventListener('click', () => moveSectionDown(section)); - $('.beautify-section', el).addEventListener('click', () => beautify([cm])); $('.restore-section', el).addEventListener('click', () => restoreSection(section)); $('.test-regexp', el).addEventListener('click', () => { regExpTester.toggle(); updateRegexpTester(); }); + initBeautifyButton($('.beautify-section', el), () => [cm]); } function handleKeydown(cm, event) { diff --git a/edit/sections-editor.js b/edit/sections-editor.js index c4b7cab3..6cd09104 100644 --- a/edit/sections-editor.js +++ b/edit/sections-editor.js @@ -154,9 +154,7 @@ function createSectionsEditor({style, onTitleChanged}) { function closestVisible(nearbyElement) { const cm = nearbyElement instanceof CodeMirror ? nearbyElement : - nearbyElement instanceof Node && - (nearbyElement.closest('#sections > .section') || {}).CodeMirror || - getLastActivatedEditor(); + nearbyElement instanceof Node && getAssociatedEditor(nearbyElement) || getLastActivatedEditor(); if (nearbyElement instanceof Node && cm) { const {left, top} = nearbyElement.getBoundingClientRect(); const bounds = cm.display.wrapper.getBoundingClientRect(); @@ -228,6 +226,15 @@ function createSectionsEditor({style, onTitleChanged}) { } } + function getAssociatedEditor(nearbyElement) { + for (let el = nearbyElement; el; el = el.parentElement) { + // added by createSection + if (el.CodeMirror) { + return el.CodeMirror; + } + } + } + function getEditors() { return sections.filter(s => !s.isRemoved()).map(s => s.cm); } diff --git a/edit/util.js b/edit/util.js index cc18b515..772292fc 100644 --- a/edit/util.js +++ b/edit/util.js @@ -1,4 +1,5 @@ -/* exported dirtyReporter memoize clipString sectionsToMozFormat */ +/* global CodeMirror $create prefs */ +/* exported dirtyReporter memoize clipString sectionsToMozFormat createHotkeyInput */ 'use strict'; function dirtyReporter() { @@ -135,3 +136,52 @@ function memoize(fn) { return result; }; } + +/** + * @param {!string} prefId + * @param {?function(isEnter:boolean)} onDone + */ +function createHotkeyInput(prefId, onDone = () => {}) { + return $create('input', { + type: 'search', + spellcheck: false, + value: prefs.get(prefId), + onkeydown(event) { + const key = CodeMirror.keyName(event); + if (key === 'Tab' || key === 'Shift-Tab') { + return; + } + event.preventDefault(); + event.stopPropagation(); + switch (key) { + case 'Enter': + if (this.checkValidity()) onDone(true); + return; + case 'Esc': + onDone(false); + return; + default: + // disallow: [Shift?] characters, modifiers-only, [modifiers?] + Esc, Tab, nav keys + if (!key || new RegExp('^(' + [ + '(Back)?Space', + '(Shift-)?.', // a single character + '(Shift-?|Ctrl-?|Alt-?|Cmd-?){0,2}(|Esc|Tab|(Page)?(Up|Down)|Left|Right|Home|End|Insert|Delete)', + ].join('|') + ')$', 'i').test(key)) { + this.value = key || this.value; + this.setCustomValidity('Not allowed'); + return; + } + } + this.value = key; + this.setCustomValidity(''); + prefs.set(prefId, key); + }, + oninput() { + // fired on pressing "x" to clear the field + prefs.set(prefId, ''); + }, + onpaste(event) { + event.preventDefault(); + } + }); +} diff --git a/js/prefs.js b/js/prefs.js index b227d2c6..de4aed43 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -59,6 +59,7 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => { end_with_newline: false, indent_conditional: true, }, + 'editor.beautify.hotkey': '', 'editor.lintDelay': 300, // lint gutter marker update delay, ms 'editor.linter': 'csslint', // 'csslint' or 'stylelint' or '' 'editor.lintReportDelay': 500, // lint report update delay, ms