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).",
"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"

View File

@ -143,7 +143,7 @@
<h1 id="heading">&nbsp;</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>

View File

@ -1,4 +1,4 @@
/* global regExpTester debounce messageBox */
/* global regExpTester debounce messageBox CodeMirror */
'use strict';
function createAppliesToLineWidget(cm) {
@ -56,14 +56,20 @@ 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);
toLine = Math.max(toLine === null ? lastChanged : toLine, to.line);
if (origin === 'setValue') {
update();
} else {
debounce(update, THROTTLE_DELAY);
}
}
function onOptionChange(cm, option) {
if (option === 'theme') {
@ -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) {

View File

@ -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 {

View File

@ -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 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() {
function initStyleData() {
const params = new URLSearchParams(location.search);
const style = {
const id = params.get('id');
const createEmptyStyle = () => ({
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;
}
sections: [
Object.assign({code: ''},
...Object.keys(CssToProperty)
.map(name => ({
[CssToProperty[name]]: params.get(name) && [params.get(name)] || []
}))
)
],
});
return !id ?
Promise.resolve(createEmptyStyle()) :
getStylesSafe({id}).then(([style]) => style || createEmptyStyle());
}
function setStyleMeta(style) {

View File

@ -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);
});

View File

@ -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;