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