420733b93a
* add Patch CSP option * show style version, size, and update age in manager * add scope selector to style search in manager * keep scroll position and selections in tab's session * directly install usercss from raw github links * ditch localStorage, use on-demand SessionStore proxy * simplify localization * allow <code> tag in i18n-html * keep nodes in HTML templates * API.getAllStyles is actually faster with code untouched * fix fitToContent when applies-to is taller than window * dedupe linter.enableForEditor calls * prioritize visible CMs in refreshOnViewListener * don't scroll to last style on editing a new one * delay colorview for invisible CMs * eslint comma-dangle error + autofix files * styleViaXhr: also toggle for disableAll pref * styleViaXhr: allow cookies for sandbox CSP * simplify notes in options * simplify getStylesViaXhr * oldUI fixups: * remove separator before 1st applies-to * center name bubbles * fix updateToc focus on a newly added section * fix fitToContent when cloning section * remove CSS `contain` as it makes no difference * replace overrides with declarative CSS + code cosmetics * simplify adjustWidth and make it work in FF
189 lines
6.6 KiB
JavaScript
189 lines
6.6 KiB
JavaScript
/* global loadScript css_beautify showHelp prefs t $ $create */
|
|
/* global editor createHotkeyInput moveFocus CodeMirror */
|
|
/* exported initBeautifyButton */
|
|
'use strict';
|
|
|
|
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) {
|
|
window.css_beautify = window.exports.css_beautify;
|
|
}
|
|
})
|
|
.then(doBeautify);
|
|
|
|
function doBeautify() {
|
|
const tabs = prefs.get('editor.indentWithTabs');
|
|
const options = Object.assign({}, prefs.get('editor.beautify'));
|
|
for (const k of Object.keys(prefs.defaults['editor.beautify'])) {
|
|
if (!(k in options)) options[k] = prefs.defaults['editor.beautify'][k];
|
|
}
|
|
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', [
|
|
$createOption('.selector1,', 'selector_separator_newline'),
|
|
$createOption('.selector2', 'newline_before_open_brace'),
|
|
$createOption('{', 'newline_after_open_brace'),
|
|
$createOption('border: none;', 'newline_between_properties', true),
|
|
$createOption('display: block;', 'newline_before_close_brace', true),
|
|
$createOption('}', 'newline_between_rules'),
|
|
$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'},
|
|
// showHelp.close will be defined after showHelp() is invoked
|
|
onclick: () => showHelp.close(),
|
|
}, t('confirmClose')),
|
|
$create('button', {
|
|
attributes: {role: 'undo'},
|
|
onclick() {
|
|
let undoable = false;
|
|
for (const cm of scope) {
|
|
const data = cm.beautifyChange;
|
|
if (!data || !data[cm.changeGeneration()]) continue;
|
|
delete data[cm.changeGeneration()];
|
|
const {scrollX, scrollY} = window;
|
|
cm.undo();
|
|
cm.scrollIntoView(cm.getCursor());
|
|
window.scrollTo(scrollX, scrollY);
|
|
undoable |= data[cm.changeGeneration()];
|
|
}
|
|
this.disabled = !undoable;
|
|
},
|
|
}, t(scope.length === 1 ? 'undo' : 'undoGlobal')),
|
|
]),
|
|
]));
|
|
|
|
$('#help-popup').className = 'wide';
|
|
|
|
$('.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}));
|
|
if (target.parentNode.hasAttribute('newline')) {
|
|
target.parentNode.setAttribute('newline', value.toString());
|
|
}
|
|
doBeautify();
|
|
};
|
|
|
|
function $createOption(label, optionName, indent) {
|
|
const value = options[optionName];
|
|
return (
|
|
$create('div', {attributes: {newline: value}}, [
|
|
$create('span', indent ? {attributes: {indent: ''}} : {}, label),
|
|
$create('div.select-resizer', [
|
|
$create('select', {dataset: {option: optionName}}, [
|
|
$create('option', {selected: !value}, '\xA0'),
|
|
$create('option', {selected: value}, '\\n'),
|
|
]),
|
|
$create('SVG:svg.svg-icon.select-arrow', {viewBox: '0 0 1792 1792'}, [
|
|
$create('SVG:path', {
|
|
'fill-rule': 'evenodd',
|
|
'd': 'M1408 704q0 26-19 45l-448 448q-19 19-45 ' +
|
|
'19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z',
|
|
}),
|
|
]),
|
|
]),
|
|
])
|
|
);
|
|
}
|
|
|
|
function $createLabeledCheckbox(optionName, i18nKey) {
|
|
return (
|
|
$create('label', {style: 'display: block; clear: both;'}, [
|
|
$create('input', {
|
|
type: 'checkbox',
|
|
dataset: {option: optionName},
|
|
checked: options[optionName] !== false,
|
|
}),
|
|
$create('SVG:svg.svg-icon.checked',
|
|
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})),
|
|
t(i18nKey),
|
|
])
|
|
);
|
|
}
|
|
}
|
|
}
|