usercss editor: save as template when @name is empty
* reduced the flickering on page open * show * in title for new styles * align the values in the default template * don't ask to save an untouched template * don't spam the console with errors * trivial code refactor and cosmetics
This commit is contained in:
parent
b63449f299
commit
a58f42dee0
|
@ -889,6 +889,10 @@
|
|||
"message": "As a security precaution, the browser prohibits extensions from affecting its built-in pages (like chrome://version, the standard new tab page as of Chrome 61, about:addons, and so on) as well as other extensions' pages. Each browser also restricts access to its own extensions gallery (like Chrome Web Store or AMO).",
|
||||
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"syncStorageErrorSaving": {
|
||||
"message": "The value cannot be saved. Try reducing the amount of text.",
|
||||
"description": "Displayed when trying to save an excessively big value via storage.sync API"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"message": "Toggle style",
|
||||
"description": "Label for the checkbox to enable/disable a style"
|
||||
|
@ -958,6 +962,17 @@
|
|||
"message": "Updates installed:",
|
||||
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
|
||||
},
|
||||
"usercssEditorNamePlaceholder": {
|
||||
"message": "Specify @name in the code",
|
||||
"description": "Placeholder text for the empty name input field when creating a new Usercss style"
|
||||
},
|
||||
"usercssReplaceTemplateName": {
|
||||
"message": "Empty @name replaces the default template",
|
||||
"description": "The text shown after @name when creating a new Usercss style"
|
||||
},
|
||||
"usercssReplaceTemplateConfirmation": {
|
||||
"message": "Replace the default template for new Usercss styles with the current code?"
|
||||
},
|
||||
"versionInvalidOlder": {
|
||||
"message": "The version is older than the installed style.",
|
||||
"description": "Displayed when the version of style is older than the installed one"
|
||||
|
|
15
edit.html
15
edit.html
|
@ -143,7 +143,7 @@
|
|||
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
||||
<section id="basic-info">
|
||||
<div id="basic-info-name">
|
||||
<input id="name" class="style-contributor" i18n-placeholder="styleMissingName" spellcheck="false">
|
||||
<input id="name" class="style-contributor" spellcheck="false">
|
||||
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
||||
</div>
|
||||
<div id="basic-info-enabled">
|
||||
|
@ -160,7 +160,7 @@
|
|||
<button id="beautify" i18n-text="styleBeautify"></button>
|
||||
<a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
||||
</div>
|
||||
<div>
|
||||
<div id="mozilla-format-container">
|
||||
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading"><svg id="to-mozilla-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
|
||||
<button id="from-mozilla" i18n-text="importLabel"></button>
|
||||
<button id="to-mozilla" i18n-text="exportLabel"></button>
|
||||
|
@ -199,6 +199,12 @@
|
|||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="option usercss-only">
|
||||
<input id="editor.appliesToLineWidget" type="checkbox">
|
||||
<label for="editor.appliesToLineWidget"
|
||||
i18n-text="appliesLineWidgetLabel"
|
||||
i18n-title="appliesLineWidgetWarning"></label>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
||||
<input id="editor.tabSize" type="number" min="0">
|
||||
|
@ -246,6 +252,11 @@
|
|||
</summary>
|
||||
<div></div>
|
||||
</details>
|
||||
<div id="footer">
|
||||
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
|
||||
i18n-text="externalUsercssDocument"
|
||||
target="_blank"></a>
|
||||
</div>
|
||||
</div>
|
||||
<section id="sections">
|
||||
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span><svg id="sections-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global regExpTester debounce messageBox */
|
||||
/* global regExpTester debounce messageBox CodeMirror */
|
||||
'use strict';
|
||||
|
||||
function createAppliesToLineWidget(cm) {
|
||||
|
@ -56,13 +56,19 @@ function createAppliesToLineWidget(cm) {
|
|||
styleVariables.remove();
|
||||
}
|
||||
|
||||
function onChange(cm, {from, to, origin}) {
|
||||
function onChange(cm, event) {
|
||||
const {from, to, origin} = event;
|
||||
if (origin === 'appliesTo') {
|
||||
return;
|
||||
}
|
||||
const lastChanged = CodeMirror.changeEnd(event).line;
|
||||
fromLine = Math.min(fromLine === null ? from.line : fromLine, from.line);
|
||||
toLine = Math.max(toLine === null ? to.line : toLine, to.line);
|
||||
debounce(update, THROTTLE_DELAY);
|
||||
toLine = Math.max(toLine === null ? lastChanged : toLine, to.line);
|
||||
if (origin === 'setValue') {
|
||||
update();
|
||||
} else {
|
||||
debounce(update, THROTTLE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
function onOptionChange(cm, option) {
|
||||
|
@ -82,9 +88,9 @@ function createAppliesToLineWidget(cm) {
|
|||
function update() {
|
||||
const changed = {fromLine, toLine};
|
||||
fromLine = Math.max(fromLine || 0, cm.display.viewFrom);
|
||||
toLine = Math.min(toLine === null ? cm.doc.size : toLine, cm.display.viewTo);
|
||||
toLine = Math.min(toLine === null ? cm.doc.size : toLine, cm.display.viewTo || toLine);
|
||||
const visible = {fromLine, toLine};
|
||||
if (fromLine >= cm.display.viewFrom && toLine <= cm.display.viewTo) {
|
||||
if (fromLine >= cm.display.viewFrom && toLine <= (cm.display.viewTo || toLine)) {
|
||||
cm.operation(doUpdate);
|
||||
}
|
||||
if (changed.fromLine !== visible.fromLine || changed.toLine !== visible.toLine) {
|
||||
|
|
|
@ -547,6 +547,12 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
|
|||
justify-items: normal;
|
||||
}
|
||||
|
||||
html:not(.usercss) .usercss-only,
|
||||
.usercss #mozilla-format-container,
|
||||
.usercss #sections > h2 {
|
||||
display: none !important; /* hide during page init */
|
||||
}
|
||||
|
||||
#sections .single-editor {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -565,7 +571,6 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
|
|||
color: #333;
|
||||
transition: color .5s;
|
||||
text-decoration-skip: ink;
|
||||
animation: fadein 10s;
|
||||
}
|
||||
|
||||
#footer a:hover {
|
||||
|
|
124
edit/edit.js
124
edit/edit.js
|
@ -8,14 +8,6 @@
|
|||
/* global initColorpicker */
|
||||
'use strict';
|
||||
|
||||
onDOMready()
|
||||
.then(() => Promise.all([
|
||||
initColorpicker(),
|
||||
initCollapsibles(),
|
||||
initHooksCommon(),
|
||||
]))
|
||||
.then(init);
|
||||
|
||||
let styleId = null;
|
||||
// only the actually dirty items here
|
||||
let dirty = {};
|
||||
|
@ -31,25 +23,50 @@ const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'do
|
|||
|
||||
let editor;
|
||||
|
||||
// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded
|
||||
onBackgroundReady();
|
||||
Promise.all([
|
||||
initStyleData().then(style => {
|
||||
styleId = style.id;
|
||||
sessionStorage.justEditedStyleId = styleId;
|
||||
// we set "usercss" class on <html> when <body> is empty
|
||||
// so there'll be no flickering of the elements that depend on it
|
||||
if (isUsercss(style)) {
|
||||
document.documentElement.classList.add('usercss');
|
||||
}
|
||||
// strip URL parameters when invoked for a non-existent id
|
||||
if (!styleId) {
|
||||
history.replaceState({}, document.title, location.pathname);
|
||||
}
|
||||
return style;
|
||||
}),
|
||||
onDOMready(),
|
||||
onBackgroundReady(),
|
||||
])
|
||||
.then(([style]) => Promise.all([
|
||||
style,
|
||||
initColorpicker(),
|
||||
initCollapsibles(),
|
||||
initHooksCommon(),
|
||||
]))
|
||||
.then(([style]) => {
|
||||
initCodeMirror();
|
||||
|
||||
const usercss = isUsercss(style);
|
||||
$('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle');
|
||||
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
||||
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
|
||||
|
||||
if (usercss) {
|
||||
editor = createSourceEditor(style);
|
||||
} else {
|
||||
initWithSectionStyle({style});
|
||||
}
|
||||
});
|
||||
|
||||
// make querySelectorAll enumeration code readable
|
||||
['forEach', 'some', 'indexOf', 'map'].forEach(method => {
|
||||
NodeList.prototype[method] = Array.prototype[method];
|
||||
});
|
||||
|
||||
// Chrome pre-34
|
||||
Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector;
|
||||
|
||||
// Chrome pre-41 polyfill
|
||||
Element.prototype.closest = Element.prototype.closest || function (selector) {
|
||||
let e;
|
||||
// eslint-disable-next-line no-empty
|
||||
for (e = this; e && !e.matches(selector); e = e.parentElement) {}
|
||||
return e;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Array.prototype.rotate = function (amount) {
|
||||
// negative amount == rotate left
|
||||
|
@ -1317,54 +1334,25 @@ function beautify(event) {
|
|||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
initCodeMirror();
|
||||
getStyle().then(style => {
|
||||
styleId = style.id;
|
||||
sessionStorage.justEditedStyleId = styleId;
|
||||
|
||||
if (!isUsercss(style)) {
|
||||
initWithSectionStyle({style});
|
||||
} else {
|
||||
editor = createSourceEditor(style);
|
||||
}
|
||||
function initStyleData() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = params.get('id');
|
||||
const createEmptyStyle = () => ({
|
||||
id: null,
|
||||
name: '',
|
||||
enabled: true,
|
||||
sections: [
|
||||
Object.assign({code: ''},
|
||||
...Object.keys(CssToProperty)
|
||||
.map(name => ({
|
||||
[CssToProperty[name]]: params.get(name) && [params.get(name)] || []
|
||||
}))
|
||||
)
|
||||
],
|
||||
});
|
||||
|
||||
function getStyle() {
|
||||
const id = new URLSearchParams(location.search).get('id');
|
||||
if (!id) {
|
||||
// match should be 2 - one for the whole thing, one for the parentheses
|
||||
// This is an add
|
||||
$('#heading').textContent = t('addStyleTitle');
|
||||
return Promise.resolve(createEmptyStyle());
|
||||
}
|
||||
$('#heading').textContent = t('editStyleHeading');
|
||||
// This is an edit
|
||||
return getStylesSafe({id}).then(styles => {
|
||||
let style = styles[0];
|
||||
if (!style) {
|
||||
style = createEmptyStyle();
|
||||
history.replaceState({}, document.title, location.pathname);
|
||||
}
|
||||
return style;
|
||||
});
|
||||
}
|
||||
|
||||
function createEmptyStyle() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const style = {
|
||||
id: null,
|
||||
name: '',
|
||||
enabled: true,
|
||||
sections: [{code: ''}]
|
||||
};
|
||||
for (const i in CssToProperty) {
|
||||
if (params.get(i)) {
|
||||
style.sections[0][CssToProperty[i]] = [params.get(i)];
|
||||
}
|
||||
}
|
||||
return style;
|
||||
}
|
||||
return !id ?
|
||||
Promise.resolve(createEmptyStyle()) :
|
||||
getStylesSafe({id}).then(([style]) => style || createEmptyStyle());
|
||||
}
|
||||
|
||||
function setStyleMeta(style) {
|
||||
|
|
|
@ -9,20 +9,13 @@ function createSourceEditor(style) {
|
|||
// a flag for isTouched()
|
||||
let hadBeenSaved = false;
|
||||
|
||||
document.documentElement.classList.add('usercss');
|
||||
$('#sections').textContent = '';
|
||||
$('#name').disabled = true;
|
||||
$('#mozilla-format-heading').parentNode.remove();
|
||||
|
||||
$('#mozilla-format-container').remove();
|
||||
$('#sections').textContent = '';
|
||||
$('#sections').appendChild(
|
||||
$element({className: 'single-editor'})
|
||||
);
|
||||
|
||||
$('#header').appendChild($element({
|
||||
id: 'footer',
|
||||
appendChild: makeLink('https://github.com/openstyles/stylus/wiki/Usercss', t('externalUsercssDocument'))
|
||||
}));
|
||||
|
||||
const dirty = dirtyReporter();
|
||||
dirty.onChange(() => {
|
||||
const DIRTY = dirty.isDirty();
|
||||
|
@ -59,34 +52,8 @@ function createSourceEditor(style) {
|
|||
function initAppliesToLineWidget() {
|
||||
const PREF_NAME = 'editor.appliesToLineWidget';
|
||||
const widget = createAppliesToLineWidget(cm);
|
||||
const optionEl = buildOption();
|
||||
|
||||
$('#options').insertBefore(optionEl, $('#options > .option.aligned'));
|
||||
widget.toggle(prefs.get(PREF_NAME));
|
||||
prefs.subscribe([PREF_NAME], (key, value) => {
|
||||
widget.toggle(value);
|
||||
optionEl.checked = value;
|
||||
});
|
||||
optionEl.addEventListener('change', e => {
|
||||
prefs.set(PREF_NAME, e.target.checked);
|
||||
});
|
||||
|
||||
function buildOption() {
|
||||
return $element({className: 'option', appendChild: [
|
||||
$element({
|
||||
tag: 'input',
|
||||
type: 'checkbox',
|
||||
id: PREF_NAME,
|
||||
checked: prefs.get(PREF_NAME)
|
||||
}),
|
||||
$element({
|
||||
tag: 'label',
|
||||
htmlFor: PREF_NAME,
|
||||
textContent: ' ' + t('appliesLineWidgetLabel'),
|
||||
title: t('appliesLineWidgetWarning')
|
||||
})
|
||||
]});
|
||||
}
|
||||
prefs.subscribe([PREF_NAME], (key, value) => widget.toggle(value));
|
||||
}
|
||||
|
||||
function initLinterSwitch() {
|
||||
|
@ -123,18 +90,27 @@ function createSourceEditor(style) {
|
|||
section = mozParser.format(style);
|
||||
}
|
||||
|
||||
const sourceCode = `/* ==UserStyle==
|
||||
@name New Style - ${Date.now()}
|
||||
@namespace github.com/openstyles/stylus
|
||||
@version 0.1.0
|
||||
@description A new userstyle
|
||||
@author Me
|
||||
==/UserStyle== */
|
||||
const DEFAULT_CODE = `
|
||||
/* ==UserStyle==
|
||||
@name ${t('usercssReplaceTemplateName') + ' - ' + new Date().toLocaleString()}
|
||||
@namespace github.com/openstyles/stylus
|
||||
@version 0.1.0
|
||||
@description A new userstyle
|
||||
@author Me
|
||||
==/UserStyle== */
|
||||
|
||||
${section}
|
||||
`;
|
||||
dirty.modify('source', '', sourceCode);
|
||||
style.sourceCode = sourceCode;
|
||||
${section}
|
||||
`.replace(/^\s+/gm, '');
|
||||
dirty.clear('source');
|
||||
style.sourceCode = '';
|
||||
BG.chromeSync.getLZValue('usercssTemplate').then(code => {
|
||||
style.sourceCode = code || DEFAULT_CODE;
|
||||
cm.startOperation();
|
||||
cm.setValue(style.sourceCode);
|
||||
cm.clearHistory();
|
||||
cm.markClean();
|
||||
cm.endOperation();
|
||||
});
|
||||
}
|
||||
|
||||
function initHooks() {
|
||||
|
@ -187,11 +163,10 @@ ${section}
|
|||
}
|
||||
|
||||
function updateTitle() {
|
||||
// title depends on dirty and style meta
|
||||
if (!style.id) {
|
||||
document.title = t('addStyleTitle');
|
||||
} else {
|
||||
document.title = (dirty.isDirty() ? '* ' : '') + t('editStyleTitle', [style.name]);
|
||||
const newTitle = (dirty.isDirty() ? '* ' : '') +
|
||||
(style.id ? t('editStyleTitle', [style.name]) : t('addStyleTitle'));
|
||||
if (document.title !== newTitle) {
|
||||
document.title = newTitle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,6 +216,17 @@ ${section}
|
|||
hadBeenSaved = true;
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.message === t('styleMissingMeta', 'name')) {
|
||||
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
|
||||
BG.chromeSync.setLZValue('usercssTemplate', style.sourceCode)
|
||||
.then(() => BG.chromeSync.getLZValue('usercssTemplate'))
|
||||
.then(saved => {
|
||||
if (saved !== style.sourceCode) {
|
||||
messageBox.alert(t('syncStorageErrorSaving'));
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const contents = [String(err)];
|
||||
if (Number.isInteger(err.index)) {
|
||||
const pos = cm.posFromIndex(err.index);
|
||||
|
@ -250,7 +236,6 @@ ${section}
|
|||
textContent: drawLinePointer(pos)
|
||||
}));
|
||||
}
|
||||
console.error(err);
|
||||
messageBox.alert(contents);
|
||||
});
|
||||
|
||||
|
|
|
@ -96,6 +96,9 @@ var usercss = (() => {
|
|||
}
|
||||
};
|
||||
|
||||
const RX_NUMBER = /^-?\d+(\.\d+)?\s*/y;
|
||||
const RX_WHITESPACE = /\s*/y;
|
||||
|
||||
function getMetaSource(source) {
|
||||
const commentRe = /\/\*[\s\S]*?\*\//g;
|
||||
const metaRe = /==userstyle==[\s\S]*?==\/userstyle==/i;
|
||||
|
@ -307,7 +310,8 @@ var usercss = (() => {
|
|||
}
|
||||
|
||||
function parseNumber(state) {
|
||||
const match = state.slice(state.re.lastIndex).match(/^-?\d+(\.\d+)?\s*/);
|
||||
RX_NUMBER.lastIndex = state.re.lastIndex;
|
||||
const match = RX_NUMBER.exec(state.text);
|
||||
if (!match) {
|
||||
throw new Error('invalid number');
|
||||
}
|
||||
|
@ -316,19 +320,20 @@ var usercss = (() => {
|
|||
}
|
||||
|
||||
function eatWhitespace(state) {
|
||||
const match = state.text.slice(state.re.lastIndex).match(/\s*/);
|
||||
state.re.lastIndex += match[0].length;
|
||||
RX_WHITESPACE.lastIndex = state.re.lastIndex;
|
||||
state.re.lastIndex += RX_WHITESPACE.exec(state.text)[0].length;
|
||||
}
|
||||
|
||||
function parseStringToEnd(state) {
|
||||
const match = state.text.slice(state.re.lastIndex).match(/.+/);
|
||||
state.value = unquote(match[0].trim());
|
||||
state.re.lastIndex += match[0].length;
|
||||
const EOL = state.text.indexOf('\n', state.re.lastIndex);
|
||||
const match = state.text.slice(state.re.lastIndex, EOL >= 0 ? EOL : undefined);
|
||||
state.value = unquote(match.trim());
|
||||
state.re.lastIndex += match.length;
|
||||
}
|
||||
|
||||
function unquote(s) {
|
||||
const q = s[0];
|
||||
if (q === s[s.length - 1] && /['"`]/.test(q)) {
|
||||
if (q === s[s.length - 1] && (q === '"' || q === "'")) {
|
||||
// http://www.json.org/
|
||||
return s.slice(1, -1).replace(
|
||||
new RegExp(`\\\\([${q}\\\\/bfnrt]|u[0-9a-fA-F]{4})`, 'g'),
|
||||
|
@ -368,6 +373,10 @@ var usercss = (() => {
|
|||
if (!(state.key in METAS)) {
|
||||
continue;
|
||||
}
|
||||
if (text[re.lastIndex - 1] === '\n') {
|
||||
// an empty value should point to EOL
|
||||
re.lastIndex--;
|
||||
}
|
||||
if (state.key === 'var' || state.key === 'advanced') {
|
||||
if (state.key === 'advanced') {
|
||||
state.maybeUSO = true;
|
||||
|
|
Loading…
Reference in New Issue
Block a user