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:
tophf 2017-11-26 16:04:03 +03:00
parent b63449f299
commit a58f42dee0
7 changed files with 157 additions and 138 deletions

View File

@ -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).", "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" "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": { "toggleStyle": {
"message": "Toggle style", "message": "Toggle style",
"description": "Label for the checkbox to enable/disable a style" "description": "Label for the checkbox to enable/disable a style"
@ -958,6 +962,17 @@
"message": "Updates installed:", "message": "Updates installed:",
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates." "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": { "versionInvalidOlder": {
"message": "The version is older than the installed style.", "message": "The version is older than the installed style.",
"description": "Displayed when the version of style is older than the installed one" "description": "Displayed when the version of style is older than the installed one"

View File

@ -143,7 +143,7 @@
<h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift --> <h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift -->
<section id="basic-info"> <section id="basic-info">
<div id="basic-info-name"> <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> <a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
</div> </div>
<div id="basic-info-enabled"> <div id="basic-info-enabled">
@ -160,7 +160,7 @@
<button id="beautify" i18n-text="styleBeautify"></button> <button id="beautify" i18n-text="styleBeautify"></button>
<a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a> <a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
</div> </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> <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="from-mozilla" i18n-text="importLabel"></button>
<button id="to-mozilla" i18n-text="exportLabel"></button> <button id="to-mozilla" i18n-text="exportLabel"></button>
@ -199,6 +199,12 @@
</svg> </svg>
</span> </span>
</div> </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"> <div class="option aligned">
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label> <label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
<input id="editor.tabSize" type="number" min="0"> <input id="editor.tabSize" type="number" min="0">
@ -246,6 +252,11 @@
</summary> </summary>
<div></div> <div></div>
</details> </details>
<div id="footer">
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
i18n-text="externalUsercssDocument"
target="_blank"></a>
</div>
</div> </div>
<section id="sections"> <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> <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>

View File

@ -1,4 +1,4 @@
/* global regExpTester debounce messageBox */ /* global regExpTester debounce messageBox CodeMirror */
'use strict'; 'use strict';
function createAppliesToLineWidget(cm) { function createAppliesToLineWidget(cm) {
@ -56,13 +56,19 @@ function createAppliesToLineWidget(cm) {
styleVariables.remove(); styleVariables.remove();
} }
function onChange(cm, {from, to, origin}) { function onChange(cm, event) {
const {from, to, origin} = event;
if (origin === 'appliesTo') { if (origin === 'appliesTo') {
return; return;
} }
const lastChanged = CodeMirror.changeEnd(event).line;
fromLine = Math.min(fromLine === null ? from.line : fromLine, from.line); fromLine = Math.min(fromLine === null ? from.line : fromLine, from.line);
toLine = Math.max(toLine === null ? to.line : toLine, to.line); toLine = Math.max(toLine === null ? lastChanged : toLine, to.line);
debounce(update, THROTTLE_DELAY); if (origin === 'setValue') {
update();
} else {
debounce(update, THROTTLE_DELAY);
}
} }
function onOptionChange(cm, option) { function onOptionChange(cm, option) {
@ -82,9 +88,9 @@ function createAppliesToLineWidget(cm) {
function update() { function update() {
const changed = {fromLine, toLine}; const changed = {fromLine, toLine};
fromLine = Math.max(fromLine || 0, cm.display.viewFrom); 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}; 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); cm.operation(doUpdate);
} }
if (changed.fromLine !== visible.fromLine || changed.toLine !== visible.toLine) { if (changed.fromLine !== visible.fromLine || changed.toLine !== visible.toLine) {

View File

@ -547,6 +547,12 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
justify-items: normal; 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 { #sections .single-editor {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -565,7 +571,6 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
color: #333; color: #333;
transition: color .5s; transition: color .5s;
text-decoration-skip: ink; text-decoration-skip: ink;
animation: fadein 10s;
} }
#footer a:hover { #footer a:hover {

View File

@ -8,14 +8,6 @@
/* global initColorpicker */ /* global initColorpicker */
'use strict'; 'use strict';
onDOMready()
.then(() => Promise.all([
initColorpicker(),
initCollapsibles(),
initHooksCommon(),
]))
.then(init);
let styleId = null; let styleId = null;
// only the actually dirty items here // only the actually dirty items here
let dirty = {}; let dirty = {};
@ -31,25 +23,50 @@ const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'do
let editor; let editor;
// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded Promise.all([
onBackgroundReady(); 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 // make querySelectorAll enumeration code readable
['forEach', 'some', 'indexOf', 'map'].forEach(method => { ['forEach', 'some', 'indexOf', 'map'].forEach(method => {
NodeList.prototype[method] = Array.prototype[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 // eslint-disable-next-line no-extend-native
Array.prototype.rotate = function (amount) { Array.prototype.rotate = function (amount) {
// negative amount == rotate left // negative amount == rotate left
@ -1317,54 +1334,25 @@ function beautify(event) {
} }
} }
function init() { function initStyleData() {
initCodeMirror(); const params = new URLSearchParams(location.search);
getStyle().then(style => { const id = params.get('id');
styleId = style.id; const createEmptyStyle = () => ({
sessionStorage.justEditedStyleId = styleId; id: null,
name: '',
if (!isUsercss(style)) { enabled: true,
initWithSectionStyle({style}); sections: [
} else { Object.assign({code: ''},
editor = createSourceEditor(style); ...Object.keys(CssToProperty)
} .map(name => ({
[CssToProperty[name]]: params.get(name) && [params.get(name)] || []
}))
)
],
}); });
return !id ?
function getStyle() { Promise.resolve(createEmptyStyle()) :
const id = new URLSearchParams(location.search).get('id'); getStylesSafe({id}).then(([style]) => style || createEmptyStyle());
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;
}
} }
function setStyleMeta(style) { function setStyleMeta(style) {

View File

@ -9,20 +9,13 @@ function createSourceEditor(style) {
// a flag for isTouched() // a flag for isTouched()
let hadBeenSaved = false; let hadBeenSaved = false;
document.documentElement.classList.add('usercss');
$('#sections').textContent = '';
$('#name').disabled = true; $('#name').disabled = true;
$('#mozilla-format-heading').parentNode.remove(); $('#mozilla-format-container').remove();
$('#sections').textContent = '';
$('#sections').appendChild( $('#sections').appendChild(
$element({className: 'single-editor'}) $element({className: 'single-editor'})
); );
$('#header').appendChild($element({
id: 'footer',
appendChild: makeLink('https://github.com/openstyles/stylus/wiki/Usercss', t('externalUsercssDocument'))
}));
const dirty = dirtyReporter(); const dirty = dirtyReporter();
dirty.onChange(() => { dirty.onChange(() => {
const DIRTY = dirty.isDirty(); const DIRTY = dirty.isDirty();
@ -59,34 +52,8 @@ function createSourceEditor(style) {
function initAppliesToLineWidget() { function initAppliesToLineWidget() {
const PREF_NAME = 'editor.appliesToLineWidget'; const PREF_NAME = 'editor.appliesToLineWidget';
const widget = createAppliesToLineWidget(cm); const widget = createAppliesToLineWidget(cm);
const optionEl = buildOption();
$('#options').insertBefore(optionEl, $('#options > .option.aligned'));
widget.toggle(prefs.get(PREF_NAME)); widget.toggle(prefs.get(PREF_NAME));
prefs.subscribe([PREF_NAME], (key, value) => { prefs.subscribe([PREF_NAME], (key, value) => widget.toggle(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')
})
]});
}
} }
function initLinterSwitch() { function initLinterSwitch() {
@ -123,18 +90,27 @@ function createSourceEditor(style) {
section = mozParser.format(style); section = mozParser.format(style);
} }
const sourceCode = `/* ==UserStyle== const DEFAULT_CODE = `
@name New Style - ${Date.now()} /* ==UserStyle==
@namespace github.com/openstyles/stylus @name ${t('usercssReplaceTemplateName') + ' - ' + new Date().toLocaleString()}
@version 0.1.0 @namespace github.com/openstyles/stylus
@description A new userstyle @version 0.1.0
@author Me @description A new userstyle
==/UserStyle== */ @author Me
==/UserStyle== */
${section} ${section}
`; `.replace(/^\s+/gm, '');
dirty.modify('source', '', sourceCode); dirty.clear('source');
style.sourceCode = sourceCode; 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() { function initHooks() {
@ -187,11 +163,10 @@ ${section}
} }
function updateTitle() { function updateTitle() {
// title depends on dirty and style meta const newTitle = (dirty.isDirty() ? '* ' : '') +
if (!style.id) { (style.id ? t('editStyleTitle', [style.name]) : t('addStyleTitle'));
document.title = t('addStyleTitle'); if (document.title !== newTitle) {
} else { document.title = newTitle;
document.title = (dirty.isDirty() ? '* ' : '') + t('editStyleTitle', [style.name]);
} }
} }
@ -241,6 +216,17 @@ ${section}
hadBeenSaved = true; hadBeenSaved = true;
}) })
.catch(err => { .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)]; const contents = [String(err)];
if (Number.isInteger(err.index)) { if (Number.isInteger(err.index)) {
const pos = cm.posFromIndex(err.index); const pos = cm.posFromIndex(err.index);
@ -250,7 +236,6 @@ ${section}
textContent: drawLinePointer(pos) textContent: drawLinePointer(pos)
})); }));
} }
console.error(err);
messageBox.alert(contents); messageBox.alert(contents);
}); });

View File

@ -96,6 +96,9 @@ var usercss = (() => {
} }
}; };
const RX_NUMBER = /^-?\d+(\.\d+)?\s*/y;
const RX_WHITESPACE = /\s*/y;
function getMetaSource(source) { function getMetaSource(source) {
const commentRe = /\/\*[\s\S]*?\*\//g; const commentRe = /\/\*[\s\S]*?\*\//g;
const metaRe = /==userstyle==[\s\S]*?==\/userstyle==/i; const metaRe = /==userstyle==[\s\S]*?==\/userstyle==/i;
@ -307,7 +310,8 @@ var usercss = (() => {
} }
function parseNumber(state) { 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) { if (!match) {
throw new Error('invalid number'); throw new Error('invalid number');
} }
@ -316,19 +320,20 @@ var usercss = (() => {
} }
function eatWhitespace(state) { function eatWhitespace(state) {
const match = state.text.slice(state.re.lastIndex).match(/\s*/); RX_WHITESPACE.lastIndex = state.re.lastIndex;
state.re.lastIndex += match[0].length; state.re.lastIndex += RX_WHITESPACE.exec(state.text)[0].length;
} }
function parseStringToEnd(state) { function parseStringToEnd(state) {
const match = state.text.slice(state.re.lastIndex).match(/.+/); const EOL = state.text.indexOf('\n', state.re.lastIndex);
state.value = unquote(match[0].trim()); const match = state.text.slice(state.re.lastIndex, EOL >= 0 ? EOL : undefined);
state.re.lastIndex += match[0].length; state.value = unquote(match.trim());
state.re.lastIndex += match.length;
} }
function unquote(s) { function unquote(s) {
const q = s[0]; const q = s[0];
if (q === s[s.length - 1] && /['"`]/.test(q)) { if (q === s[s.length - 1] && (q === '"' || q === "'")) {
// http://www.json.org/ // http://www.json.org/
return s.slice(1, -1).replace( return s.slice(1, -1).replace(
new RegExp(`\\\\([${q}\\\\/bfnrt]|u[0-9a-fA-F]{4})`, 'g'), new RegExp(`\\\\([${q}\\\\/bfnrt]|u[0-9a-fA-F]{4})`, 'g'),
@ -368,6 +373,10 @@ var usercss = (() => {
if (!(state.key in METAS)) { if (!(state.key in METAS)) {
continue; 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 === 'var' || state.key === 'advanced') {
if (state.key === 'advanced') { if (state.key === 'advanced') {
state.maybeUSO = true; state.maybeUSO = true;