diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 0dce610e..2a43da21 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -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"
diff --git a/edit.html b/edit.html
index b5297fbb..c4948ce0 100644
--- a/edit.html
+++ b/edit.html
@@ -143,7 +143,7 @@
-
+
@@ -160,7 +160,7 @@
-
+
@@ -199,6 +199,12 @@
+
+
+
+
@@ -246,6 +252,11 @@
+
diff --git a/edit/applies-to-line-widget.js b/edit/applies-to-line-widget.js
index c2cddaae..36dbf8cb 100644
--- a/edit/applies-to-line-widget.js
+++ b/edit/applies-to-line-widget.js
@@ -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) {
diff --git a/edit/edit.css b/edit/edit.css
index 7ae7002c..16b7c00e 100644
--- a/edit/edit.css
+++ b/edit/edit.css
@@ -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 {
diff --git a/edit/edit.js b/edit/edit.js
index 2e18a96d..75ae8855 100644
--- a/edit/edit.js
+++ b/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 when 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) {
diff --git a/edit/source-editor.js b/edit/source-editor.js
index fe38e30b..b6ef422e 100644
--- a/edit/source-editor.js
+++ b/edit/source-editor.js
@@ -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== */
-
-${section}
-`;
- dirty.modify('source', '', sourceCode);
- style.sourceCode = sourceCode;
+ 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}
+ `.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);
});
diff --git a/js/usercss.js b/js/usercss.js
index 2f3298a6..63c7d4bd 100644
--- a/js/usercss.js
+++ b/js/usercss.js
@@ -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;