From a58f42dee00ad3c7d0e3cd38b6a8e4c3e82522e3 Mon Sep 17 00:00:00 2001 From: tophf Date: Sun, 26 Nov 2017 16:04:03 +0300 Subject: [PATCH] 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 --- _locales/en/messages.json | 15 ++++ edit.html | 15 +++- edit/applies-to-line-widget.js | 18 +++-- edit/edit.css | 7 +- edit/edit.js | 124 +++++++++++++++------------------ edit/source-editor.js | 93 +++++++++++-------------- js/usercss.js | 23 ++++-- 7 files changed, 157 insertions(+), 138 deletions(-) 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;