Merge pull request #1054 from tophf/custom-name
fix local name customization for usercss/legacy
This commit is contained in:
		
						commit
						7f15ae324d
					
				|  | @ -250,6 +250,13 @@ | |||
|     "message": "Copy to clipboard", | ||||
|     "description": "Tooltip for elements which can be copied" | ||||
|   }, | ||||
|   "customNameHint": { | ||||
|     "message": "Enter a custom name here to rename the style in UI without breaking its updates" | ||||
|   }, | ||||
|   "customNameResetHint": { | ||||
|     "message": "Stop using customized name, switch to the style's own name", | ||||
|     "description": "Tooltip of 'x' button shown in editor when changing the name input of a) styles updated from a URL i.e. not locally created, b) UserCSS styles" | ||||
|   }, | ||||
|   "dateInstalled": { | ||||
|     "message": "Date installed", | ||||
|     "description": "Option text for the user to sort the style by install date" | ||||
|  |  | |||
|  | @ -57,7 +57,7 @@ | |||
|           continue; | ||||
|         } | ||||
|         for (const part in PARTS) { | ||||
|           const text = style[part]; | ||||
|           const text = part === 'name' ? style.customName || style.name : style[part]; | ||||
|           if (text && PARTS[part](text, rx, words, icase)) { | ||||
|             results.push(id); | ||||
|             break; | ||||
|  |  | |||
|  | @ -61,6 +61,8 @@ const styleManager = (() => { | |||
|     username: '' | ||||
|   }; | ||||
| 
 | ||||
|   const DELETE_IF_NULL = ['id', 'customName']; | ||||
| 
 | ||||
|   handleLivePreviewConnections(); | ||||
| 
 | ||||
|   return Object.assign({ | ||||
|  | @ -387,8 +389,10 @@ const styleManager = (() => { | |||
|     if (!style.name) { | ||||
|       throw new Error('style name is empty'); | ||||
|     } | ||||
|     if (style.id == null) { | ||||
|       delete style.id; | ||||
|     for (const key of DELETE_IF_NULL) { | ||||
|       if (style[key] == null) { | ||||
|         delete style[key]; | ||||
|       } | ||||
|     } | ||||
|     if (!style._id) { | ||||
|       style._id = uuidv4(); | ||||
|  | @ -569,6 +573,16 @@ const styleManager = (() => { | |||
|           touched = true; | ||||
|         } | ||||
|       } | ||||
|       // upgrade the old way of customizing local names
 | ||||
|       const {originalName} = style; | ||||
|       if (originalName) { | ||||
|         touched = true; | ||||
|         if (originalName !== style.name) { | ||||
|           style.customName = style.name; | ||||
|           style.name = originalName; | ||||
|         } | ||||
|         delete style.originalName; | ||||
|       } | ||||
|       return touched; | ||||
|     } | ||||
|   } | ||||
|  |  | |||
|  | @ -116,7 +116,7 @@ | |||
|     } | ||||
| 
 | ||||
|     function reportSuccess(saved) { | ||||
|       log(STATES.UPDATED + ` #${style.id} ${style.name}`); | ||||
|       log(STATES.UPDATED + ` #${style.id} ${style.customName || style.name}`); | ||||
|       const info = {updated: true, style: saved}; | ||||
|       if (port) port.postMessage(info); | ||||
|       return info; | ||||
|  | @ -139,7 +139,7 @@ | |||
|       if (typeof error === 'object' && error.message) { | ||||
|         error = error.message; | ||||
|       } | ||||
|       log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.name}`); | ||||
|       log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.customName || style.name}`); | ||||
|       const info = {error, STATES, style: getStyleWithNoCode(style)}; | ||||
|       if (port) port.postMessage(info); | ||||
|       return info; | ||||
|  | @ -207,13 +207,6 @@ | |||
|       // keep current state
 | ||||
|       delete json.enabled; | ||||
| 
 | ||||
|       // keep local name customizations
 | ||||
|       if (style.originalName !== style.name && style.name !== json.name) { | ||||
|         delete json.name; | ||||
|       } else { | ||||
|         json.originalName = json.name; | ||||
|       } | ||||
| 
 | ||||
|       const newStyle = Object.assign({}, style, json); | ||||
|       if (styleSectionsEqual(json, style, {checkSource: true})) { | ||||
|         // update digest even if save === false as there might be just a space added etc.
 | ||||
|  |  | |||
							
								
								
									
										53
									
								
								edit.html
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								edit.html
									
									
									
									
									
								
							|  | @ -18,6 +18,22 @@ | |||
|       } | ||||
|     </style> | ||||
| 
 | ||||
|     <link id="cm-theme" rel="stylesheet"> | ||||
| 
 | ||||
|     <script src="js/polyfill.js"></script> | ||||
|     <script src="js/dom.js"></script> | ||||
|     <script src="js/messaging.js"></script> | ||||
|     <script src="js/msg.js"></script> | ||||
|     <script src="js/prefs.js"></script> | ||||
|     <script src="js/localization.js"></script> | ||||
|     <script src="js/script-loader.js"></script> | ||||
|     <script src="js/storage-util.js"></script> | ||||
| 
 | ||||
|     <script src="content/style-injector.js"></script> | ||||
|     <script src="content/apply.js"></script> | ||||
| 
 | ||||
|     <script src="edit/edit.js"></script> <!-- run it ASAP to send a request for the style --> | ||||
| 
 | ||||
|     <link  href="vendor/codemirror/lib/codemirror.css" rel="stylesheet"> | ||||
|     <script src="vendor/codemirror/lib/codemirror.js"></script> | ||||
| 
 | ||||
|  | @ -63,44 +79,27 @@ | |||
| 
 | ||||
|     <script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script> | ||||
| 
 | ||||
|     <script src="js/polyfill.js"></script> | ||||
|     <script src="js/dom.js"></script> | ||||
|     <script src="js/messaging.js"></script> | ||||
|     <script src="js/prefs.js"></script> | ||||
|     <script src="js/localization.js"></script> | ||||
|     <script src="js/script-loader.js"></script> | ||||
|     <script src="js/storage-util.js"></script> | ||||
|     <script src="js/msg.js"></script> | ||||
|     <script src="js/worker-util.js"></script> | ||||
| 
 | ||||
|     <script src="content/style-injector.js"></script> | ||||
|     <script src="content/apply.js"></script> | ||||
| 
 | ||||
|     <link  href="edit/global-search.css" rel="stylesheet"> | ||||
|     <script src="edit/global-search.js"></script> | ||||
|     <script src="msgbox/msgbox.js" async></script> | ||||
| 
 | ||||
|     <link  href="edit/codemirror-default.css" rel="stylesheet"> | ||||
|     <script src="edit/codemirror-default.js"></script> | ||||
| 
 | ||||
|     <script src="edit/codemirror-factory.js"></script> | ||||
|     <script src="edit/util.js"></script> | ||||
|     <script src="edit/regexp-tester.js"></script> | ||||
|     <script src="edit/live-preview.js"></script> | ||||
|     <script src="edit/applies-to-line-widget.js"></script> | ||||
|     <script src="edit/reroute-hotkeys.js"></script> | ||||
|     <script src="edit/codemirror-factory.js"></script> | ||||
|     <link  href="edit/global-search.css" rel="stylesheet"> | ||||
|     <script src="edit/global-search.js"></script> | ||||
|     <script src="edit/colorpicker-helper.js"></script> | ||||
|     <script src="edit/beautify.js"></script> | ||||
|     <script src="edit/show-keymap-help.js"></script> | ||||
|     <script src="edit/codemirror-themes.js"></script> | ||||
| 
 | ||||
|     <script src="edit/source-editor.js"></script> | ||||
|     <script src="edit/sections-editor-section.js"></script> | ||||
|     <script src="edit/sections-editor.js"></script> | ||||
| 
 | ||||
|     <script src="edit/edit.js"></script> | ||||
| 
 | ||||
|     <script src="msgbox/msgbox.js" async></script> | ||||
| 
 | ||||
|     <script src="js/worker-util.js"></script> | ||||
|     <script src="edit/linter.js"></script> | ||||
|     <script src="edit/linter-defaults.js"></script> | ||||
|     <script src="edit/linter-engines.js"></script> | ||||
|  | @ -109,8 +108,6 @@ | |||
|     <script src="edit/linter-report.js"></script> | ||||
|     <script src="edit/linter-config-dialog.js"></script> | ||||
| 
 | ||||
|     <link id="cm-theme" rel="stylesheet"> | ||||
| 
 | ||||
|     <template data-id="appliesTo"> | ||||
|       <li class="applies-to-item"> | ||||
|         <div class="select-resizer"> | ||||
|  | @ -284,7 +281,13 @@ | |||
|       <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" spellcheck="false" required> | ||||
|           <input id="name" class="style-contributor" spellcheck="false"> | ||||
|           <a id="reset-name" href="#" i18n-title="customNameResetHint" tabindex="0" hidden> | ||||
|             <svg class="svg-icon" viewBox="0 0 20 20"> | ||||
|               <polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5 | ||||
|                                5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/> | ||||
|             </svg> | ||||
|           </a> | ||||
|           <a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a> | ||||
|         </div> | ||||
|         <div id="basic-info-enabled"> | ||||
|  |  | |||
|  | @ -315,7 +315,7 @@ CodeMirror.hint && (() => { | |||
|     } | ||||
| 
 | ||||
|     // USO vars in usercss mode editor
 | ||||
|     const vars = editor.getStyle().usercssData.vars; | ||||
|     const vars = editor.style.usercssData.vars; | ||||
|     const list = vars ? | ||||
|       Object.keys(vars).filter(name => name.startsWith(leftPart)) : []; | ||||
|     return { | ||||
|  | @ -343,7 +343,7 @@ CodeMirror.hint && (() => { | |||
|         string[start + 3] === '[' && | ||||
|         string[pos - 3] === ']' && | ||||
|         string[pos - 4] === ']') { | ||||
|       const vars = typeof editor !== 'undefined' && (editor.getStyle().usercssData || {}).vars; | ||||
|       const vars = typeof editor !== 'undefined' && (editor.style.usercssData || {}).vars; | ||||
|       const name = vars && string.slice(start + 4, pos - 4); | ||||
|       if (vars && Object.hasOwnProperty.call(vars, name.endsWith('-rgb') ? name.slice(0, -4) : name)) { | ||||
|         token[0] = USO_VALID_VAR; | ||||
|  |  | |||
|  | @ -88,6 +88,9 @@ label { | |||
|   display: flex; | ||||
|   align-items: center; | ||||
| } | ||||
| #reset-name { | ||||
|   margin: 0 .25em 0 .5em; | ||||
| } | ||||
| #url { | ||||
|   margin-left: 0.25rem; | ||||
| } | ||||
|  | @ -610,7 +613,8 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high | |||
|   right: 4px; | ||||
|   top: .5em; | ||||
| } | ||||
| #help-popup input[type="search"] { | ||||
| #help-popup input[type="search"], | ||||
| #help-popup .CodeMirror { | ||||
|   margin: 3px; | ||||
| } | ||||
| 
 | ||||
|  | @ -813,11 +817,6 @@ body.linter-disabled .hidden-unless-compact { | |||
|   margin-top: .75rem; | ||||
| } | ||||
| 
 | ||||
| .usercss #name { | ||||
|   background-color: #eee; | ||||
|   color: #888; | ||||
| } | ||||
| 
 | ||||
| .single-editor { | ||||
|   height: 100%; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										365
									
								
								edit/edit.js
									
									
									
									
									
								
							
							
						
						
									
										365
									
								
								edit/edit.js
									
									
									
									
									
								
							|  | @ -1,15 +1,11 @@ | |||
| /* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML | ||||
|   createSourceEditor sessionStorageHash getOwnTab FIREFOX API tryCatch | ||||
|   closeCurrentTab messageBox debounce workerUtil | ||||
|   initBeautifyButton ignoreChromeError | ||||
|   closeCurrentTab messageBox debounce | ||||
|   initBeautifyButton ignoreChromeError dirtyReporter linter | ||||
|   moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */ | ||||
| /* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const editorWorker = workerUtil.createWorker({ | ||||
|   url: '/edit/editor-worker.js' | ||||
| }); | ||||
| 
 | ||||
| let saveSizeOnClose; | ||||
| 
 | ||||
| // direct & reverse mapping of @-moz-document keywords and internal property names
 | ||||
|  | @ -28,48 +24,84 @@ document.addEventListener('visibilitychange', beforeUnload); | |||
| window.addEventListener('beforeunload', beforeUnload); | ||||
| msg.onExtension(onRuntimeMessage); | ||||
| 
 | ||||
| preinit(); | ||||
| lazyInit(); | ||||
| 
 | ||||
| (() => { | ||||
|   onDOMready().then(() => { | ||||
|     prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip); | ||||
|     addEventListener('showHotkeyInTooltip', showHotkeyInTooltip); | ||||
|     showHotkeyInTooltip(); | ||||
| (async function init() { | ||||
|   const [style] = await Promise.all([ | ||||
|     initStyleData(), | ||||
|     onDOMready(), | ||||
|     prefs.initializing.then(() => new Promise(resolve => { | ||||
|       const theme = prefs.get('editor.theme'); | ||||
|       const el = $('#cm-theme'); | ||||
|       if (theme === 'default') { | ||||
|         resolve(); | ||||
|       } else { | ||||
|         // preload the theme so CodeMirror can use the correct metrics
 | ||||
|         el.href = `vendor/codemirror/theme/${theme}.css`; | ||||
|         el.addEventListener('load', resolve, {once: true}); | ||||
|       } | ||||
|     })), | ||||
|   ]); | ||||
|   const usercss = isUsercss(style); | ||||
|   const dirty = dirtyReporter(); | ||||
|   let wasDirty = false; | ||||
|   let nameTarget; | ||||
| 
 | ||||
|     buildThemeElement(); | ||||
|     buildKeymapElement(); | ||||
|   prefs.subscribe(['editor.linter'], updateLinter); | ||||
|   prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip); | ||||
|   addEventListener('showHotkeyInTooltip', showHotkeyInTooltip); | ||||
|   showHotkeyInTooltip(); | ||||
|   buildThemeElement(); | ||||
|   buildKeymapElement(); | ||||
|   setupLivePrefs(); | ||||
|   initNameArea(); | ||||
|   initBeautifyButton($('#beautify'), () => editor.getEditors()); | ||||
|   initResizeListener(); | ||||
|   detectLayout(); | ||||
|   updateTitle(); | ||||
| 
 | ||||
|     setupLivePrefs(); | ||||
|   $('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle'); | ||||
|   $('#preview-label').classList.toggle('hidden', !style.id); | ||||
| 
 | ||||
|   editor = (usercss ? createSourceEditor : createSectionsEditor)({ | ||||
|     style, | ||||
|     dirty, | ||||
|     updateName, | ||||
|     toggleStyle, | ||||
|   }); | ||||
|   dirty.onChange(updateDirty); | ||||
|   await editor.ready; | ||||
| 
 | ||||
|   initEditor(); | ||||
|   // enabling after init to prevent flash of validation failure on an empty name
 | ||||
|   $('#name').required = !usercss; | ||||
|   $('#save-button').onclick = editor.save; | ||||
| 
 | ||||
|   function getCodeMirrorThemes() { | ||||
|     if (!chrome.runtime.getPackageDirectoryEntry) { | ||||
|       const themes = [ | ||||
|         chrome.i18n.getMessage('defaultTheme'), | ||||
|         ...CODEMIRROR_THEMES | ||||
|       ]; | ||||
|       localStorage.codeMirrorThemes = themes.join(' '); | ||||
|       return Promise.resolve(themes); | ||||
|     } | ||||
|     return new Promise(resolve => { | ||||
|       chrome.runtime.getPackageDirectoryEntry(rootDir => { | ||||
|         rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => { | ||||
|           themeDir.createReader().readEntries(entries => { | ||||
|             const themes = [ | ||||
|               chrome.i18n.getMessage('defaultTheme') | ||||
|             ].concat( | ||||
|               entries.filter(entry => entry.isFile) | ||||
|                 .sort((a, b) => (a.name < b.name ? -1 : 1)) | ||||
|                 .map(entry => entry.name.replace(/\.css$/, '')) | ||||
|             ); | ||||
|             localStorage.codeMirrorThemes = themes.join(' '); | ||||
|             resolve(themes); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|   function initNameArea() { | ||||
|     const nameEl = $('#name'); | ||||
|     const resetEl = $('#reset-name'); | ||||
|     const isCustomName = style.updateUrl || usercss; | ||||
|     nameTarget = isCustomName ? 'customName' : 'name'; | ||||
|     nameEl.placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName'); | ||||
|     nameEl.title = isCustomName ? t('customNameHint') : ''; | ||||
|     nameEl.addEventListener('input', () => { | ||||
|       updateName(); | ||||
|       resetEl.hidden = false; | ||||
|     }); | ||||
|     resetEl.hidden = !style.customName; | ||||
|     resetEl.onclick = () => { | ||||
|       const style = editor.style; | ||||
|       nameEl.focus(); | ||||
|       nameEl.select(); | ||||
|       // trying to make it undoable via Ctrl-Z
 | ||||
|       if (!document.execCommand('insertText', false, style.name)) { | ||||
|         nameEl.value = style.name; | ||||
|         updateName(); | ||||
|       } | ||||
|       style.customName = null; // to delete it from db
 | ||||
|       resetEl.hidden = true; | ||||
|     }; | ||||
|     const enabledEl = $('#enabled'); | ||||
|     enabledEl.onchange = () => updateEnabledness(enabledEl.checked); | ||||
|   } | ||||
| 
 | ||||
|   function findKeyForCommand(command, map) { | ||||
|  | @ -88,27 +120,10 @@ preinit(); | |||
|   } | ||||
| 
 | ||||
|   function buildThemeElement() { | ||||
|     const themeElement = $('#editor.theme'); | ||||
|     const themeList = localStorage.codeMirrorThemes; | ||||
| 
 | ||||
|     const optionsFromArray = options => { | ||||
|       const fragment = document.createDocumentFragment(); | ||||
|       options.forEach(opt => fragment.appendChild($create('option', opt))); | ||||
|       themeElement.appendChild(fragment); | ||||
|     }; | ||||
| 
 | ||||
|     if (themeList) { | ||||
|       optionsFromArray(themeList.split(/\s+/)); | ||||
|     } else { | ||||
|       // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
 | ||||
|       const theme = prefs.get('editor.theme'); | ||||
|       optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]); | ||||
|       getCodeMirrorThemes().then(() => { | ||||
|         const themes = (localStorage.codeMirrorThemes || '').split(/\s+/); | ||||
|         optionsFromArray(themes); | ||||
|         themeElement.selectedIndex = Math.max(0, themes.indexOf(theme)); | ||||
|       }); | ||||
|     } | ||||
|     CODEMIRROR_THEMES.unshift(chrome.i18n.getMessage('defaultTheme')); | ||||
|     $('#editor.theme').append(...CODEMIRROR_THEMES.map(s => $create('option', s))); | ||||
|     // move the theme after built-in CSS so that its same-specificity selectors win
 | ||||
|     document.head.appendChild($('#cm-theme')); | ||||
|   } | ||||
| 
 | ||||
|   function buildKeymapElement() { | ||||
|  | @ -159,134 +174,118 @@ preinit(); | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function initEditor() { | ||||
|     return Promise.all([ | ||||
|       initStyleData(), | ||||
|       onDOMready(), | ||||
|       prefs.initializing, | ||||
|     ]) | ||||
|       .then(([style]) => { | ||||
|         const usercss = isUsercss(style); | ||||
|         $('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle'); | ||||
|         $('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName'); | ||||
|         $('#name').title = usercss ? t('usercssReplaceTemplateName') : ''; | ||||
|         $('#preview-label').classList.toggle('hidden', !style.id); | ||||
|         initBeautifyButton($('#beautify'), () => editor.getEditors()); | ||||
|         const {onBoundsChanged} = chrome.windows || {}; | ||||
|         if (onBoundsChanged) { | ||||
|           // * movement is reported even if the window wasn't resized
 | ||||
|           // * fired just once when done so debounce is not needed
 | ||||
|           onBoundsChanged.addListener(wnd => { | ||||
|             // getting the current window id as it may change if the user attached/detached the tab
 | ||||
|             chrome.windows.getCurrent(ownWnd => { | ||||
|               if (wnd.id === ownWnd.id) rememberWindowSize(); | ||||
|             }); | ||||
|           }); | ||||
|         } | ||||
|         window.addEventListener('resize', () => { | ||||
|           if (!onBoundsChanged) debounce(rememberWindowSize, 100); | ||||
|           detectLayout(); | ||||
|   function initResizeListener() { | ||||
|     const {onBoundsChanged} = chrome.windows || {}; | ||||
|     if (onBoundsChanged) { | ||||
|       // * movement is reported even if the window wasn't resized
 | ||||
|       // * fired just once when done so debounce is not needed
 | ||||
|       onBoundsChanged.addListener(wnd => { | ||||
|         // getting the current window id as it may change if the user attached/detached the tab
 | ||||
|         chrome.windows.getCurrent(ownWnd => { | ||||
|           if (wnd.id === ownWnd.id) rememberWindowSize(); | ||||
|         }); | ||||
|         detectLayout(); | ||||
|         editor = (usercss ? createSourceEditor : createSectionsEditor)({ | ||||
|           style, | ||||
|           onTitleChanged: updateTitle | ||||
|         }); | ||||
|         editor.dirty.onChange(updateDirty); | ||||
|         return Promise.resolve(editor.ready && editor.ready()) | ||||
|           .then(updateDirty); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   function updateTitle() { | ||||
|     if (editor) { | ||||
|       const styleName = editor.getStyle().name; | ||||
|       const isDirty = editor.dirty.isDirty(); | ||||
|       document.title = (isDirty ? '* ' : '') + styleName; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function updateDirty() { | ||||
|     const isDirty = editor.dirty.isDirty(); | ||||
|     document.body.classList.toggle('dirty', isDirty); | ||||
|     $('#save-button').disabled = !isDirty; | ||||
|     updateTitle(); | ||||
|   } | ||||
| })(); | ||||
| 
 | ||||
| function preinit() { | ||||
|   // preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
 | ||||
|   new MutationObserver((mutations, observer) => { | ||||
|     const themeElement = $('#cm-theme'); | ||||
|     if (themeElement) { | ||||
|       themeElement.href = prefs.get('editor.theme') === 'default' ? '' | ||||
|         : 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css'; | ||||
|       observer.disconnect(); | ||||
|     } | ||||
|   }).observe(document, {subtree: true, childList: true}); | ||||
| 
 | ||||
|   if (chrome.windows) { | ||||
|     browser.tabs.query({currentWindow: true}).then(tabs => { | ||||
|       const windowId = tabs[0].windowId; | ||||
|       if (prefs.get('openEditInWindow')) { | ||||
|         if ( | ||||
|           /true/.test(sessionStorage.saveSizeOnClose) && | ||||
|           'left' in prefs.get('windowPosition', {}) && | ||||
|           !isWindowMaximized() | ||||
|         ) { | ||||
|           // window was reopened via Ctrl-Shift-T etc.
 | ||||
|           chrome.windows.update(windowId, prefs.get('windowPosition')); | ||||
|         } | ||||
|         if (tabs.length === 1 && window.history.length === 1) { | ||||
|           chrome.windows.getAll(windows => { | ||||
|             if (windows.length > 1) { | ||||
|               sessionStorageHash('saveSizeOnClose').set(windowId, true); | ||||
|               saveSizeOnClose = true; | ||||
|             } | ||||
|           }); | ||||
|         } else { | ||||
|           saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId]; | ||||
|         } | ||||
|       } | ||||
|     window.addEventListener('resize', () => { | ||||
|       if (!onBoundsChanged) debounce(rememberWindowSize, 100); | ||||
|       detectLayout(); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   getOwnTab().then(tab => { | ||||
|     const ownTabId = tab.id; | ||||
|   function toggleStyle() { | ||||
|     $('#enabled').checked = !style.enabled; | ||||
|     updateEnabledness(!style.enabled); | ||||
|   } | ||||
| 
 | ||||
|     // use browser history back when 'back to manage' is clicked
 | ||||
|     if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) { | ||||
|       onDOMready().then(() => { | ||||
|         $('#cancel-button').onclick = event => { | ||||
|           event.stopPropagation(); | ||||
|           event.preventDefault(); | ||||
|           history.back(); | ||||
|         }; | ||||
|       }); | ||||
|   function updateDirty() { | ||||
|     const isDirty = dirty.isDirty(); | ||||
|     if (wasDirty !== isDirty) { | ||||
|       wasDirty = isDirty; | ||||
|       document.body.classList.toggle('dirty', isDirty); | ||||
|       $('#save-button').disabled = !isDirty; | ||||
|     } | ||||
|     // no windows on android
 | ||||
|     if (!chrome.windows) { | ||||
|     updateTitle(); | ||||
|   } | ||||
| 
 | ||||
|   function updateEnabledness(enabled) { | ||||
|     dirty.modify('enabled', style.enabled, enabled); | ||||
|     style.enabled = enabled; | ||||
|     editor.updateLivePreview(); | ||||
|   } | ||||
| 
 | ||||
|   function updateName() { | ||||
|     if (!editor) return; | ||||
|     const {value} = $('#name'); | ||||
|     dirty.modify('name', style[nameTarget] || style.name, value); | ||||
|     style[nameTarget] = value; | ||||
|     updateTitle({}); | ||||
|   } | ||||
| 
 | ||||
|   function updateTitle() { | ||||
|     document.title = `${dirty.isDirty() ? '* ' : ''}${style.customName || style.name}`; | ||||
|   } | ||||
| 
 | ||||
|   function updateLinter(key, value) { | ||||
|     $('body').classList.toggle('linter-disabled', value === ''); | ||||
|     linter.run(); | ||||
|   } | ||||
| })(); | ||||
| 
 | ||||
| /* Stuff not needed for the main init so we can let it run at its own tempo */ | ||||
| async function lazyInit() { | ||||
|   const ownTabId = (await getOwnTab()).id; | ||||
|   // use browser history back when 'back to manage' is clicked
 | ||||
|   if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) { | ||||
|     onDOMready().then(() => { | ||||
|       $('#cancel-button').onclick = event => { | ||||
|         event.stopPropagation(); | ||||
|         event.preventDefault(); | ||||
|         history.back(); | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
|   // no windows on android
 | ||||
|   if (!chrome.windows) { | ||||
|     return; | ||||
|   } | ||||
|   const tabs = await browser.tabs.query({currentWindow: true}); | ||||
|   const windowId = tabs[0].windowId; | ||||
|   if (prefs.get('openEditInWindow')) { | ||||
|     if ( | ||||
|       /true/.test(sessionStorage.saveSizeOnClose) && | ||||
|       'left' in prefs.get('windowPosition', {}) && | ||||
|       !isWindowMaximized() | ||||
|     ) { | ||||
|       // window was reopened via Ctrl-Shift-T etc.
 | ||||
|       chrome.windows.update(windowId, prefs.get('windowPosition')); | ||||
|     } | ||||
|     if (tabs.length === 1 && window.history.length === 1) { | ||||
|       chrome.windows.getAll(windows => { | ||||
|         if (windows.length > 1) { | ||||
|           sessionStorageHash('saveSizeOnClose').set(windowId, true); | ||||
|           saveSizeOnClose = true; | ||||
|         } | ||||
|       }); | ||||
|     } else { | ||||
|       saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId]; | ||||
|     } | ||||
|   } | ||||
|   chrome.tabs.onAttached.addListener((tabId, info) => { | ||||
|     if (tabId !== ownTabId) { | ||||
|       return; | ||||
|     } | ||||
|     // When an edit page gets attached or detached, remember its state
 | ||||
|     // so we can do the same to the next one to open.
 | ||||
|     chrome.tabs.onAttached.addListener((tabId, info) => { | ||||
|       if (tabId !== ownTabId) { | ||||
|         return; | ||||
|     if (info.newPosition !== 0) { | ||||
|       prefs.set('openEditInWindow', false); | ||||
|       return; | ||||
|     } | ||||
|     chrome.windows.get(info.newWindowId, {populate: true}, win => { | ||||
|       // If there's only one tab in this window, it's been dragged to new window
 | ||||
|       const openEditInWindow = win.tabs.length === 1; | ||||
|       if (openEditInWindow && FIREFOX) { | ||||
|         // FF-only because Chrome retardedly resets the size during dragging
 | ||||
|         chrome.windows.update(info.newWindowId, prefs.get('windowPosition')); | ||||
|       } | ||||
|       if (info.newPosition !== 0) { | ||||
|         prefs.set('openEditInWindow', false); | ||||
|         return; | ||||
|       } | ||||
|       chrome.windows.get(info.newWindowId, {populate: true}, win => { | ||||
|         // If there's only one tab in this window, it's been dragged to new window
 | ||||
|         const openEditInWindow = win.tabs.length === 1; | ||||
|         if (openEditInWindow && FIREFOX) { | ||||
|           // FF-only because Chrome retardedly resets the size during dragging
 | ||||
|           chrome.windows.update(info.newWindowId, prefs.get('windowPosition')); | ||||
|         } | ||||
|         prefs.set('openEditInWindow', openEditInWindow); | ||||
|       }); | ||||
|       prefs.set('openEditInWindow', openEditInWindow); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | @ -295,7 +294,7 @@ function onRuntimeMessage(request) { | |||
|   switch (request.method) { | ||||
|     case 'styleUpdated': | ||||
|       if ( | ||||
|         editor.getStyleId() === request.style.id && | ||||
|         editor.style.id === request.style.id && | ||||
|         !['editPreview', 'editPreviewEnd', 'editSave', 'config'] | ||||
|           .includes(request.reason) | ||||
|       ) { | ||||
|  | @ -309,7 +308,7 @@ function onRuntimeMessage(request) { | |||
|       } | ||||
|       break; | ||||
|     case 'styleDeleted': | ||||
|       if (editor.getStyleId() === request.style.id) { | ||||
|       if (editor.style.id === request.style.id) { | ||||
|         document.removeEventListener('visibilitychange', beforeUnload); | ||||
|         document.removeEventListener('beforeunload', beforeUnload); | ||||
|         closeCurrentTab(); | ||||
|  | @ -503,10 +502,6 @@ function rememberWindowSize() { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| prefs.subscribe(['editor.linter'], (key, value) => { | ||||
|   $('body').classList.toggle('linter-disabled', value === ''); | ||||
| }); | ||||
| 
 | ||||
| function fixedHeader() { | ||||
|   const scrollPoint = $('#header').clientHeight - 40; | ||||
|   const linterEnabled = prefs.get('editor.linter') !== ''; | ||||
|  |  | |||
|  | @ -1,6 +1,11 @@ | |||
| /* global prefs */ | ||||
| /* global workerUtil */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| /* exported editorWorker */ | ||||
| const editorWorker = workerUtil.createWorker({ | ||||
|   url: '/edit/editor-worker.js' | ||||
| }); | ||||
| 
 | ||||
| /* exported linter */ | ||||
| const linter = (() => { | ||||
|   const lintingUpdatedListeners = []; | ||||
|  | @ -59,8 +64,3 @@ const linter = (() => { | |||
|       .then(results => [].concat(...results.filter(Boolean))); | ||||
|   } | ||||
| })(); | ||||
| 
 | ||||
| // FIXME: this should be put inside edit.js
 | ||||
| prefs.subscribe(['editor.linter'], () => { | ||||
|   linter.run(); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,41 +1,24 @@ | |||
| /* global dirtyReporter showHelp toggleContextMenuDelete createSection | ||||
| /* global showHelp toggleContextMenuDelete createSection | ||||
|   CodeMirror linter createLivePreview showCodeMirrorPopup | ||||
|   sectionsToMozFormat messageBox clipString | ||||
|   rerouteHotkeys $ $$ $create t FIREFOX API | ||||
|   $ $$ $create t FIREFOX API | ||||
|   debounce */ | ||||
| /* exported createSectionsEditor */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| function createSectionsEditor({style, onTitleChanged}) { | ||||
| function createSectionsEditor(editorBase) { | ||||
|   const {style, dirty} = editorBase; | ||||
| 
 | ||||
|   let INC_ID = 0; // an increment id that is used by various object to track the order
 | ||||
|   const dirty = dirtyReporter(); | ||||
| 
 | ||||
|   const container = $('#sections'); | ||||
|   const sections = []; | ||||
| 
 | ||||
|   container.classList.add('section-editor'); | ||||
| 
 | ||||
|   const nameEl = $('#name'); | ||||
|   nameEl.addEventListener('input', () => { | ||||
|     dirty.modify('name', style.name, nameEl.value); | ||||
|     style.name = nameEl.value; | ||||
|     onTitleChanged(); | ||||
|   }); | ||||
| 
 | ||||
|   const enabledEl = $('#enabled'); | ||||
|   enabledEl.addEventListener('change', () => { | ||||
|     dirty.modify('enabled', style.enabled, enabledEl.checked); | ||||
|     style.enabled = enabledEl.checked; | ||||
|     updateLivePreview(); | ||||
|   }); | ||||
| 
 | ||||
|   updateHeader(); | ||||
|   rerouteHotkeys(true); | ||||
| 
 | ||||
|   $('#to-mozilla').addEventListener('click', showMozillaFormat); | ||||
|   $('#to-mozilla-help').addEventListener('click', showToMozillaHelp); | ||||
|   $('#from-mozilla').addEventListener('click', () => showMozillaFormatImport()); | ||||
|   $('#save-button').addEventListener('click', saveStyle); | ||||
| 
 | ||||
|   document.addEventListener('wheel', scrollEntirePageOnCtrlShift, {passive: false}); | ||||
|   CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow'; | ||||
|  | @ -65,33 +48,30 @@ function createSectionsEditor({style, onTitleChanged}) { | |||
| 
 | ||||
|   let sectionOrder = ''; | ||||
|   let headerOffset; // in compact mode the header is at the top so it reduces the available height
 | ||||
|   const initializing = initSections(style.sections.slice()); | ||||
|   const ready = initSections(style.sections.slice()); | ||||
| 
 | ||||
|   const livePreview = createLivePreview(); | ||||
|   livePreview.show(Boolean(style.id)); | ||||
| 
 | ||||
|   return { | ||||
|     ready: () => initializing, | ||||
|   return Object.assign({}, editorBase, { | ||||
|     ready, | ||||
|     replaceStyle, | ||||
|     dirty, | ||||
|     getStyle: () => style, | ||||
|     getEditors, | ||||
|     scrollToEditor, | ||||
|     getStyleId: () => style.id, | ||||
|     getEditorTitle: cm => { | ||||
|       const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm); | ||||
|       return `${t('sectionCode')} ${index + 1}`; | ||||
|     }, | ||||
|     save: saveStyle, | ||||
|     toggleStyle, | ||||
|     save, | ||||
|     nextEditor, | ||||
|     prevEditor, | ||||
|     closestVisible, | ||||
|     getSearchableInputs, | ||||
|   }; | ||||
|     updateLivePreview, | ||||
|   }); | ||||
| 
 | ||||
|   function fitToContent(section) { | ||||
|     const {cm, cm: {display: {wrapper, sizer}}} = section; | ||||
|     const {el, cm, cm: {display: {wrapper, sizer}}} = section; | ||||
|     if (cm.display.renderedView) { | ||||
|       resize(); | ||||
|     } else { | ||||
|  | @ -104,7 +84,7 @@ function createSectionsEditor({style, onTitleChanged}) { | |||
|         return; | ||||
|       } | ||||
|       if (headerOffset == null) { | ||||
|         headerOffset = wrapper.getBoundingClientRect().top; | ||||
|         headerOffset = el.getBoundingClientRect().top; | ||||
|       } | ||||
|       contentHeight += 9; // border & resize grip
 | ||||
|       cm.off('update', resize); | ||||
|  | @ -115,16 +95,15 @@ function createSectionsEditor({style, onTitleChanged}) { | |||
|   } | ||||
| 
 | ||||
|   function fitToAvailableSpace() { | ||||
|     const available = | ||||
|       Math.floor(container.offsetHeight - sections.reduce((h, s) => h + s.el.offsetHeight, 0)) || | ||||
|       window.innerHeight - container.offsetHeight; | ||||
|     if (available <= 0) { | ||||
|       return; | ||||
|     const ch = container.offsetHeight; | ||||
|     let available = ch - sections[sections.length - 1].el.getBoundingClientRect().bottom + headerOffset; | ||||
|     if (available <= 1) available = window.innerHeight - ch - headerOffset; | ||||
|     const delta = Math.floor(available / sections.length); | ||||
|     if (delta > 1) { | ||||
|       sections.forEach(({cm}) => { | ||||
|         cm.setSize(null, cm.display.wrapper.offsetHeight + delta); | ||||
|       }); | ||||
|     } | ||||
|     const cmHeights = sections.map(s => s.cm.getWrapperElement().offsetHeight); | ||||
|     sections.forEach((section, i) => { | ||||
|       section.cm.setSize(null, cmHeights[i] + Math.floor(available / sections.length)); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function genId() { | ||||
|  | @ -246,14 +225,6 @@ function createSectionsEditor({style, onTitleChanged}) { | |||
|     return sections.filter(s => !s.isRemoved()).map(s => s.cm); | ||||
|   } | ||||
| 
 | ||||
|   function toggleStyle() { | ||||
|     const newValue = !style.enabled; | ||||
|     dirty.modify('enabled', style.enabled, newValue); | ||||
|     style.enabled = newValue; | ||||
|     enabledEl.checked = newValue; | ||||
|     updateLivePreview(); | ||||
|   } | ||||
| 
 | ||||
|   function nextEditor(cm, cycle = true) { | ||||
|     if (!cycle && findLast(sections, s => !s.isRemoved()).cm === cm) { | ||||
|       return; | ||||
|  | @ -417,7 +388,7 @@ function createSectionsEditor({style, onTitleChanged}) { | |||
|   } | ||||
| 
 | ||||
|   function validate() { | ||||
|     if (!nameEl.reportValidity()) { | ||||
|     if (!$('#name').reportValidity()) { | ||||
|       messageBox.alert(t('styleMissingName')); | ||||
|       return false; | ||||
|     } | ||||
|  | @ -435,7 +406,7 @@ function createSectionsEditor({style, onTitleChanged}) { | |||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   function saveStyle() { | ||||
|   function save() { | ||||
|     if (!dirty.isDirty()) { | ||||
|       return; | ||||
|     } | ||||
|  | @ -464,10 +435,10 @@ function createSectionsEditor({style, onTitleChanged}) { | |||
|   } | ||||
| 
 | ||||
|   function updateHeader() { | ||||
|     nameEl.value = style.name || ''; | ||||
|     enabledEl.checked = style.enabled !== false; | ||||
|     $('#name').value = style.customName || style.name || ''; | ||||
|     $('#enabled').checked = style.enabled !== false; | ||||
|     $('#url').href = style.url || ''; | ||||
|     onTitleChanged(); | ||||
|     editorBase.updateName(); | ||||
|   } | ||||
| 
 | ||||
|   function updateLivePreview() { | ||||
|  | @ -609,6 +580,7 @@ function createSectionsEditor({style, onTitleChanged}) { | |||
|   } | ||||
| 
 | ||||
|   function replaceStyle(newStyle, codeIsUpdated) { | ||||
|     dirty.clear('name'); | ||||
|     // FIXME: avoid recreating all editors?
 | ||||
|     reinit().then(() => { | ||||
|       Object.assign(style, newStyle); | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| /* global dirtyReporter | ||||
| /* global | ||||
|   createAppliesToLineWidget messageBox | ||||
|   sectionsToMozFormat | ||||
|   createMetaCompiler linter createLivePreview cmFactory $ $create API prefs t | ||||
|  | @ -6,17 +6,14 @@ | |||
| /* exported createSourceEditor */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| function createSourceEditor({style, onTitleChanged}) { | ||||
|   $('#name').disabled = true; | ||||
|   $('#save-button').disabled = true; | ||||
| function createSourceEditor(editorBase) { | ||||
|   const {style, dirty} = editorBase; | ||||
| 
 | ||||
|   $('#mozilla-format-container').remove(); | ||||
|   $('#save-button').onclick = save; | ||||
|   $('#header').addEventListener('wheel', headerOnScroll); | ||||
|   $('#sections').textContent = ''; | ||||
|   $('#sections').appendChild($create('.single-editor')); | ||||
| 
 | ||||
|   const dirty = dirtyReporter(); | ||||
| 
 | ||||
|   // normalize style
 | ||||
|   if (!style.id) setupNewStyle(style); | ||||
| 
 | ||||
|  | @ -28,13 +25,6 @@ function createSourceEditor({style, onTitleChanged}) { | |||
|   const livePreview = createLivePreview(preprocess); | ||||
|   livePreview.show(Boolean(style.id)); | ||||
| 
 | ||||
|   $('#enabled').onchange = function () { | ||||
|     const value = this.checked; | ||||
|     dirty.modify('enabled', style.enabled, value); | ||||
|     style.enabled = value; | ||||
|     updateLivePreview(); | ||||
|   }; | ||||
| 
 | ||||
|   cm.on('changes', () => { | ||||
|     dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration()); | ||||
|     updateLivePreview(); | ||||
|  | @ -162,14 +152,15 @@ function createSourceEditor({style, onTitleChanged}) { | |||
|   } | ||||
| 
 | ||||
|   function updateMeta() { | ||||
|     $('#name').value = style.name; | ||||
|     $('#name').value = style.customName || style.name; | ||||
|     $('#enabled').checked = style.enabled; | ||||
|     $('#url').href = style.url; | ||||
|     onTitleChanged(); | ||||
|     editorBase.updateName(); | ||||
|     return cm.setPreprocessor((style.usercssData || {}).preprocessor); | ||||
|   } | ||||
| 
 | ||||
|   function replaceStyle(newStyle, codeIsUpdated) { | ||||
|     dirty.clear('name'); | ||||
|     const sameCode = newStyle.sourceCode === cm.getValue(); | ||||
|     if (sameCode) { | ||||
|       savedGeneration = cm.changeGeneration(); | ||||
|  | @ -210,14 +201,6 @@ function createSourceEditor({style, onTitleChanged}) { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function toggleStyle() { | ||||
|     const value = !style.enabled; | ||||
|     dirty.modify('enabled', style.enabled, value); | ||||
|     style.enabled = value; | ||||
|     updateMeta(); | ||||
|     $('#enabled').dispatchEvent(new Event('change', {bubbles: true})); | ||||
|   } | ||||
| 
 | ||||
|   function save() { | ||||
|     if (!dirty.isDirty()) return; | ||||
|     const code = cm.getValue(); | ||||
|  | @ -226,6 +209,7 @@ function createSourceEditor({style, onTitleChanged}) { | |||
|         id: style.id, | ||||
|         enabled: style.enabled, | ||||
|         sourceCode: code, | ||||
|         customName: style.customName, | ||||
|       })) | ||||
|       .then(replaceStyle) | ||||
|       .catch(err => { | ||||
|  | @ -372,19 +356,17 @@ function createSourceEditor({style, onTitleChanged}) { | |||
|            (mode.helperType || ''); | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|   return Object.assign({}, editorBase, { | ||||
|     ready: Promise.resolve(), | ||||
|     replaceStyle, | ||||
|     dirty, | ||||
|     getStyle: () => style, | ||||
|     getEditors: () => [cm], | ||||
|     scrollToEditor: () => {}, | ||||
|     getStyleId: () => style.id, | ||||
|     getEditorTitle: () => '', | ||||
|     save, | ||||
|     toggleStyle, | ||||
|     prevEditor: cm => nextPrevMozDocument(cm, -1), | ||||
|     nextEditor: cm => nextPrevMozDocument(cm, 1), | ||||
|     closestVisible: () => cm, | ||||
|     getSearchableInputs: () => [] | ||||
|   }; | ||||
|     getSearchableInputs: () => [], | ||||
|     updateLivePreview, | ||||
|   }); | ||||
| } | ||||
|  |  | |||
|  | @ -243,7 +243,7 @@ | |||
|       (!dup ? | ||||
|         Promise.resolve(true) : | ||||
|         messageBox.confirm(t('styleInstallOverwrite', [ | ||||
|           data.name, | ||||
|           data.name + (dup.customName ? ` (${dup.customName})` : ''), | ||||
|           dupData.version, | ||||
|           data.version, | ||||
|         ])) | ||||
|  |  | |||
|  | @ -33,7 +33,8 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => { | |||
|     onExtension, | ||||
|     off, | ||||
|     RX_NO_RECEIVER, | ||||
|     RX_PORT_CLOSED | ||||
|     RX_PORT_CLOSED, | ||||
|     isBg, | ||||
|   }; | ||||
| 
 | ||||
|   function getBg() { | ||||
|  |  | |||
							
								
								
									
										15
									
								
								js/prefs.js
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								js/prefs.js
									
									
									
									
									
								
							|  | @ -1,6 +1,8 @@ | |||
| /* global promisifyChrome */ | ||||
| /* global promisifyChrome msg API */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| // Needs msg.js loaded first
 | ||||
| 
 | ||||
| self.prefs = self.INJECTED === 1 ? self.prefs : (() => { | ||||
|   const defaults = { | ||||
|     'openEditInWindow': false,      // new editor opens in a own browser window
 | ||||
|  | @ -112,12 +114,11 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => { | |||
|     'storage.sync': ['get', 'set'], | ||||
|   }); | ||||
| 
 | ||||
|   const initializing = browser.storage.sync.get('settings') | ||||
|     .then(result => { | ||||
|       if (result.settings) { | ||||
|         setAll(result.settings, true); | ||||
|       } | ||||
|     }); | ||||
|   const initializing = ( | ||||
|     msg.isBg | ||||
|       ? browser.storage.sync.get('settings').then(res => res.settings) | ||||
|       : API.getPrefs() | ||||
|   ).then(res => res && setAll(res, true)); | ||||
| 
 | ||||
|   chrome.storage.onChanged.addListener((changes, area) => { | ||||
|     if (area !== 'sync' || !changes.settings || !changes.settings.newValue) { | ||||
|  |  | |||
|  | @ -48,73 +48,3 @@ const loadScript = (() => { | |||
|     )); | ||||
|   }; | ||||
| })(); | ||||
| 
 | ||||
| 
 | ||||
| (() => { | ||||
|   let subscribers, observer; | ||||
|   // natively declared <script> elements in html can't have onload= attribute
 | ||||
|   // due to the default extension CSP that forbids inline code (and we don't want to relax it),
 | ||||
|   // so we're using MutationObserver to add onload event listener to the script element to be loaded
 | ||||
|   window.onDOMscriptReady = (srcSuffix, timeout = 1000) => { | ||||
|     if (!subscribers) { | ||||
|       subscribers = new Map(); | ||||
|       observer = new MutationObserver(observe); | ||||
|       observer.observe(document.head, {childList: true}); | ||||
|     } | ||||
|     return new Promise((resolve, reject) => { | ||||
|       const listeners = subscribers.get(srcSuffix); | ||||
|       if (listeners) { | ||||
|         listeners.push(resolve); | ||||
|       } else { | ||||
|         subscribers.set(srcSuffix, [resolve]); | ||||
|       } | ||||
|       // a resolved Promise won't reject anymore
 | ||||
|       setTimeout(() => { | ||||
|         emptyAfterCleanup(srcSuffix); | ||||
|         reject(new Error('Timeout')); | ||||
|       }, timeout); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   return; | ||||
| 
 | ||||
|   function observe(mutations) { | ||||
|     for (const {addedNodes} of mutations) { | ||||
|       for (const n of addedNodes) { | ||||
|         if (n.src && getSubscribersForSrc(n.src)) { | ||||
|           n.addEventListener('load', notifySubscribers, {once: true}); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function getSubscribersForSrc(src) { | ||||
|     for (const [suffix, listeners] of subscribers.entries()) { | ||||
|       if (src.endsWith(suffix)) { | ||||
|         return {suffix, listeners}; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function notifySubscribers(event) { | ||||
|     for (let data; (data = getSubscribersForSrc(this.src));) { | ||||
|       data.listeners.forEach(fn => fn(event)); | ||||
|       if (emptyAfterCleanup(data.suffix)) { | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function emptyAfterCleanup(suffix) { | ||||
|     if (!subscribers) { | ||||
|       return true; | ||||
|     } | ||||
|     subscribers.delete(suffix); | ||||
|     if (!subscribers.size) { | ||||
|       observer.disconnect(); | ||||
|       observer = null; | ||||
|       subscribers = null; | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
| })(); | ||||
|  |  | |||
|  | @ -149,8 +149,8 @@ | |||
|   <script src="js/polyfill.js"></script> | ||||
|   <script src="js/dom.js"></script> | ||||
|   <script src="js/messaging.js"></script> | ||||
|   <script src="js/prefs.js"></script> | ||||
|   <script src="js/msg.js"></script> | ||||
|   <script src="js/prefs.js"></script> | ||||
|   <script src="js/router.js"></script> | ||||
|   <script src="content/style-injector.js"></script> | ||||
|   <script src="content/apply.js"></script> | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ function configDialog(style) { | |||
|   vars.forEach(renderValueState); | ||||
| 
 | ||||
|   return messageBox({ | ||||
|     title: `${style.name} v${data.version}`, | ||||
|     title: `${style.customName || style.name} v${data.version}`, | ||||
|     className: 'config-dialog' + (isPopup ? ' stylus-popup' : ''), | ||||
|     contents: [ | ||||
|       $create('.config-heading', data.supportURL && | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ let initialized = false; | |||
| router.watch({search: ['search']}, ([search]) => { | ||||
|   $('#search').value = search || ''; | ||||
|   if (!initialized) { | ||||
|     init(); | ||||
|     initFilters(); | ||||
|     initialized = true; | ||||
|   } else { | ||||
|     searchStyles(); | ||||
|  | @ -36,7 +36,7 @@ HTMLSelectElement.prototype.adjustWidth = function () { | |||
|   parent.replaceChild(this, singleSelect); | ||||
| }; | ||||
| 
 | ||||
| function init() { | ||||
| function initFilters() { | ||||
|   $('#search').oninput = e => { | ||||
|     router.updateSearch('search', e.target.value); | ||||
|   }; | ||||
|  |  | |||
|  | @ -1,15 +1,11 @@ | |||
| /* global messageBox styleSectionsEqual API onDOMready | ||||
|   tryJSONparse scrollElementIntoView $ $$ API $create t animateElement | ||||
|   styleJSONseemsValid */ | ||||
| /* exported bulkChangeQueue bulkChangeTime */ | ||||
|   styleJSONseemsValid bulkChangeQueue */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const STYLISH_DUMP_FILE_EXT = '.txt'; | ||||
| const STYLUS_BACKUP_FILE_EXT = '.json'; | ||||
| 
 | ||||
| let bulkChangeQueue = []; | ||||
| let bulkChangeTime = 0; | ||||
| 
 | ||||
| onDOMready().then(() => { | ||||
|   $('#file-all-styles').onclick = () => exportToFile(); | ||||
|   $('#unfile-all-styles').onclick = () => importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); | ||||
|  | @ -135,7 +131,7 @@ function importFromString(jsonString) { | |||
|       } | ||||
|     }); | ||||
|     bulkChangeQueue.length = 0; | ||||
|     bulkChangeTime = performance.now(); | ||||
|     bulkChangeQueue.time = performance.now(); | ||||
|     return API.importManyStyles(items.map(i => i.item)) | ||||
|       .then(styles => { | ||||
|         for (let i = 0; i < styles.length; i++) { | ||||
|  |  | |||
							
								
								
									
										146
									
								
								manage/manage.js
									
									
									
									
									
								
							
							
						
						
									
										146
									
								
								manage/manage.js
									
									
									
									
									
								
							|  | @ -4,11 +4,10 @@ global messageBox getStyleWithNoCode | |||
|   checkUpdate handleUpdateInstalled | ||||
|   objectDiff | ||||
|   configDialog | ||||
|   sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs | ||||
|   sorter msg prefs API $ $$ $create template setupLivePrefs | ||||
|   t tWordBreak formatDate | ||||
|   getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce | ||||
|   scrollElementIntoView CHROME VIVALDI router | ||||
|   bulkChangeTime:true bulkChangeQueue | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
|  | @ -18,6 +17,8 @@ const ENTRY_ID_PREFIX_RAW = 'style-'; | |||
| const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW; | ||||
| 
 | ||||
| const BULK_THROTTLE_MS = 100; | ||||
| const bulkChangeQueue = []; | ||||
| bulkChangeQueue.time = 0; | ||||
| 
 | ||||
| // define pref-mapped ids separately
 | ||||
| const newUI = { | ||||
|  | @ -49,18 +50,45 @@ Promise.all([ | |||
|   API.getAllStyles(true), | ||||
|   // FIXME: integrate this into filter.js
 | ||||
|   router.getSearch('search') && API.searchDB({query: router.getSearch('search')}), | ||||
|   Promise.all([ | ||||
|     onDOMready(), | ||||
|     prefs.initializing, | ||||
|   ]) | ||||
|     .then(() => { | ||||
|       initGlobalEvents(); | ||||
|       if (!VIVALDI) { | ||||
|         $$('#header select').forEach(el => el.adjustWidth()); | ||||
|       } | ||||
|     }), | ||||
| ]).then(args => { | ||||
|   showStyles(...args); | ||||
|   waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
 | ||||
|   prefs.initializing | ||||
| ]).then(([styles, ids, el]) => { | ||||
|   installed = el; | ||||
|   installed.onclick = handleEvent.entryClicked; | ||||
|   $('#manage-options-button').onclick = () => router.updateHash('#stylus-options'); | ||||
|   $('#sync-styles').onclick = () => router.updateHash('#stylus-options'); | ||||
|   $$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external)); | ||||
|   // show date installed & last update on hover
 | ||||
|   installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle); | ||||
|   installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle); | ||||
|   document.addEventListener('visibilitychange', onVisibilityChange); | ||||
|   // N.B. triggers existing onchange listeners
 | ||||
|   setupLivePrefs(); | ||||
|   sorter.init(); | ||||
|   prefs.subscribe(newUI.ids.map(newUI.prefKeyForId), () => switchUI()); | ||||
|   switchUI({styleOnly: true}); | ||||
|   // translate CSS manually
 | ||||
|   document.head.appendChild($create('style', ` | ||||
|     .disabled h2::after { | ||||
|       content: "${t('genericDisabledLabel')}"; | ||||
|     } | ||||
|     #update-all-no-updates[data-skipped-edited="true"]::after { | ||||
|       content: " ${t('updateAllCheckSucceededSomeEdited')}"; | ||||
|     } | ||||
|     body.all-styles-hidden-by-filters::after { | ||||
|       content: "${t('filteredStylesAllHidden')}"; | ||||
|     } | ||||
|   `));
 | ||||
|   if (!VIVALDI) { | ||||
|     $$('#header select').forEach(el => el.adjustWidth()); | ||||
|   } | ||||
|   if (CHROME >= 80 && CHROME <= 88) { | ||||
|     // Wrong checkboxes are randomly checked after going back in history, https://crbug.com/1138598
 | ||||
|     addEventListener('pagehide', () => { | ||||
|       $$('input[type=checkbox]').forEach((el, i) => (el.name = `bug${i}`)); | ||||
|     }); | ||||
|   } | ||||
|   showStyles(styles, ids); | ||||
| }); | ||||
| 
 | ||||
| msg.onExtension(onRuntimeMessage); | ||||
|  | @ -71,7 +99,7 @@ function onRuntimeMessage(msg) { | |||
|     case 'styleAdded': | ||||
|     case 'styleDeleted': | ||||
|       bulkChangeQueue.push(msg); | ||||
|       if (performance.now() - bulkChangeTime < BULK_THROTTLE_MS) { | ||||
|       if (performance.now() - bulkChangeQueue.time < BULK_THROTTLE_MS) { | ||||
|         debounce(handleBulkChange, BULK_THROTTLE_MS); | ||||
|       } else { | ||||
|         handleBulkChange(); | ||||
|  | @ -86,61 +114,16 @@ function onRuntimeMessage(msg) { | |||
|   setTimeout(sorter.updateStripes, 0, {onlyWhenColumnsChanged: true}); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function initGlobalEvents() { | ||||
|   installed = $('#installed'); | ||||
|   installed.onclick = handleEvent.entryClicked; | ||||
|   $('#manage-options-button').onclick = () => router.updateHash('#stylus-options'); | ||||
|   $('#sync-styles').onclick = () => router.updateHash('#stylus-options'); | ||||
|   $$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external)); | ||||
|   // show date installed & last update on hover
 | ||||
|   installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle); | ||||
|   installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle); | ||||
| 
 | ||||
|   document.addEventListener('visibilitychange', onVisibilityChange); | ||||
| 
 | ||||
|   $$('[data-toggle-on-click]').forEach(el => { | ||||
|     // dataset on SVG doesn't work in Chrome 49-??, works in 57+
 | ||||
|     const target = $(el.getAttribute('data-toggle-on-click')); | ||||
|     el.onclick = event => { | ||||
|       event.preventDefault(); | ||||
|       target.classList.toggle('hidden'); | ||||
|       if (target.classList.contains('hidden')) { | ||||
|         el.removeAttribute('open'); | ||||
|       } else { | ||||
|         el.setAttribute('open', ''); | ||||
|       } | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   // N.B. triggers existing onchange listeners
 | ||||
|   setupLivePrefs(); | ||||
|   sorter.init(); | ||||
| 
 | ||||
|   prefs.subscribe(newUI.ids.map(newUI.prefKeyForId), () => switchUI()); | ||||
| 
 | ||||
|   switchUI({styleOnly: true}); | ||||
| 
 | ||||
|   // translate CSS manually
 | ||||
|   document.head.appendChild($create('style', ` | ||||
|     .disabled h2::after { | ||||
|       content: "${t('genericDisabledLabel')}"; | ||||
|     } | ||||
|     #update-all-no-updates[data-skipped-edited="true"]::after { | ||||
|       content: " ${t('updateAllCheckSucceededSomeEdited')}"; | ||||
|     } | ||||
|     body.all-styles-hidden-by-filters::after { | ||||
|       content: "${t('filteredStylesAllHidden')}"; | ||||
|     } | ||||
|   `));
 | ||||
| } | ||||
| 
 | ||||
| function showStyles(styles = [], matchUrlIds) { | ||||
|   const sorted = sorter.sort({ | ||||
|     styles: styles.map(style => ({ | ||||
|       style, | ||||
|       name: (style.name || '').toLocaleLowerCase() + '\n' + style.name, | ||||
|     })), | ||||
|     styles: styles.map(style => { | ||||
|       const name = style.customName || style.name || ''; | ||||
|       return { | ||||
|         style, | ||||
|         // sort case-insensitively the whole list then sort dupes like `Foo` and `foo` case-sensitively
 | ||||
|         name: name.toLocaleLowerCase() + '\n' + name, | ||||
|       }; | ||||
|     }), | ||||
|   }); | ||||
|   let index = 0; | ||||
|   let firstRun = true; | ||||
|  | @ -187,7 +170,7 @@ function showStyles(styles = [], matchUrlIds) { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| function createStyleElement({style, name}) { | ||||
| function createStyleElement({style, name: nameLC}) { | ||||
|   // query the sub-elements just once, then reuse the references
 | ||||
|   if ((createStyleElement.parts || {}).newUI !== newUI.enabled) { | ||||
|     const entry = template[`style${newUI.enabled ? 'Compact' : ''}`]; | ||||
|  | @ -216,8 +199,9 @@ function createStyleElement({style, name}) { | |||
|   } | ||||
|   const parts = createStyleElement.parts; | ||||
|   const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0; | ||||
|   const name = style.customName || style.name; | ||||
|   parts.checker.checked = style.enabled; | ||||
|   parts.nameLink.textContent = tWordBreak(style.name); | ||||
|   parts.nameLink.textContent = tWordBreak(name); | ||||
|   parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id; | ||||
|   parts.homepage.href = parts.homepage.title = style.url || ''; | ||||
|   if (!newUI.enabled) { | ||||
|  | @ -234,7 +218,7 @@ function createStyleElement({style, name}) { | |||
|   const entry = parts.entry.cloneNode(true); | ||||
|   entry.id = ENTRY_ID_PREFIX_RAW + style.id; | ||||
|   entry.styleId = style.id; | ||||
|   entry.styleNameLowerCase = name || style.name.toLocaleLowerCase(); | ||||
|   entry.styleNameLowerCase = nameLC || name.toLocaleLowerCase() + '\n' + name; | ||||
|   entry.styleMeta = style; | ||||
|   entry.className = parts.entryClassBase + ' ' + | ||||
|     (style.enabled ? 'enabled' : 'disabled') + | ||||
|  | @ -437,7 +421,7 @@ Object.assign(handleEvent, { | |||
|     animateElement(entry); | ||||
|     messageBox({ | ||||
|       title: t('deleteStyleConfirm'), | ||||
|       contents: entry.styleMeta.name, | ||||
|       contents: entry.styleMeta.customName || entry.styleMeta.name, | ||||
|       className: 'danger center', | ||||
|       buttons: [t('confirmDelete'), t('confirmCancel')], | ||||
|     }) | ||||
|  | @ -533,7 +517,7 @@ function handleBulkChange() { | |||
|     const {id} = msg.style; | ||||
|     if (msg.method === 'styleDeleted') { | ||||
|       handleDelete(id); | ||||
|       bulkChangeTime = performance.now(); | ||||
|       bulkChangeQueue.time = performance.now(); | ||||
|     } else { | ||||
|       handleUpdateForId(id, msg); | ||||
|     } | ||||
|  | @ -544,7 +528,7 @@ function handleBulkChange() { | |||
| function handleUpdateForId(id, opts) { | ||||
|   return API.getStyle(id, true).then(style => { | ||||
|     handleUpdate(style, opts); | ||||
|     bulkChangeTime = performance.now(); | ||||
|     bulkChangeQueue.time = performance.now(); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
|  | @ -718,6 +702,20 @@ function highlightEditedStyle() { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| function waitForSelector(selector) { | ||||
|   // TODO: if used in other places, move to dom.js
 | ||||
|   // TODO: if used concurrently, rework to use just one observer internally
 | ||||
|   return new Promise(resolve => { | ||||
|     const mo = new MutationObserver(() => { | ||||
|       const el = $(selector); | ||||
|       if (el) { | ||||
|         mo.disconnect(); | ||||
|         resolve(el); | ||||
|       } | ||||
|     }); | ||||
|     mo.observe(document, {childList: true, subtree: true}); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function embedOptions() { | ||||
|   let options = $('#stylus-embedded-options'); | ||||
|  |  | |||
|  | @ -130,7 +130,7 @@ const sorter = (() => { | |||
|     const sorted = sort({ | ||||
|       styles: current.map(entry => ({ | ||||
|         entry, | ||||
|         name: entry.styleNameLowerCase + '\n' + entry.styleMeta.name, | ||||
|         name: entry.styleNameLowerCase, | ||||
|         style: entry.styleMeta, | ||||
|       })) | ||||
|     }); | ||||
|  |  | |||
|  | @ -180,8 +180,8 @@ | |||
|   <script src="js/dom.js"></script> | ||||
|   <script src="js/messaging.js"></script> | ||||
|   <script src="js/localization.js"></script> | ||||
|   <script src="js/prefs.js"></script> | ||||
|   <script src="js/msg.js"></script> | ||||
|   <script src="js/prefs.js"></script> | ||||
|   <script src="content/style-injector.js"></script> | ||||
|   <script src="content/apply.js"></script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -310,7 +310,7 @@ function sortStyles(entries) { | |||
|   return entries.sort(({styleMeta: a}, {styleMeta: b}) => | ||||
|     Boolean(a.frameUrl) - Boolean(b.frameUrl) || | ||||
|     enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) || | ||||
|     a.name.localeCompare(b.name)); | ||||
|     (a.customName || a.name).localeCompare(b.customName || b.name)); | ||||
| } | ||||
| 
 | ||||
| function showStyles(frameResults) { | ||||
|  | @ -408,7 +408,7 @@ function createStyleElement(style) { | |||
|   $('.checker', entry).checked = style.enabled; | ||||
| 
 | ||||
|   const styleName = $('.style-name', entry); | ||||
|   styleName.lastChild.textContent = style.name; | ||||
|   styleName.lastChild.textContent = style.customName || style.name; | ||||
|   setTimeout(() => { | ||||
|     styleName.title = entry.styleMeta.sloppy ? | ||||
|       t('styleNotAppliedRegexpProblemTooltip') : | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user