save-as-template button in editor (#1385)
+ keep i18n attributes to use them as CSS selectors + reduce flicker when creating a new style + split button
This commit is contained in:
		
							parent
							
								
									594ca3520c
								
							
						
					
					
						commit
						cc7eba979e
					
				|  | @ -1352,6 +1352,9 @@ | |||
|   "retrieveDropboxSync": { | ||||
|     "message": "Dropbox Import" | ||||
|   }, | ||||
|   "saveAsTemplate": { | ||||
|     "message": "Save as template" | ||||
|   }, | ||||
|   "search": { | ||||
|     "message": "Search", | ||||
|     "description": "Label before the search input field in the editor shown on Ctrl-F" | ||||
|  | @ -1812,10 +1815,6 @@ | |||
|   "usercssReplaceTemplateConfirmation": { | ||||
|     "message": "Replace the default template for new Usercss styles with the current code?" | ||||
|   }, | ||||
|   "usercssReplaceTemplateName": { | ||||
|     "message": "Empty @name replaces the default template", | ||||
|     "description": "The text shown after @name when creating a new Usercss style" | ||||
|   }, | ||||
|   "usercssReplaceTemplateSectionBody": { | ||||
|     "message": "Insert code here...", | ||||
|     "description": "The code placeholder comment in a new style created by clicking 'Write style' in the popup" | ||||
|  |  | |||
|  | @ -271,13 +271,16 @@ | |||
|         </div> | ||||
|       </section> | ||||
|       <section id="actions"> | ||||
|         <div> | ||||
|           <button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button> | ||||
|         <div class="buttons"> | ||||
|           <div class="split-btn"> | ||||
|             <button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button | ||||
|             ><button class="split-btn-pedal usercss-only" i18n-menu-tpl="saveAsTemplate"></button> | ||||
|           </div> | ||||
|           <button id="beautify" i18n-text="styleBeautify"></button> | ||||
|           <button id="style-settings-btn" i18n-text="settings"></button> | ||||
|           <button id="cancel-button" i18n-title="styleCancelEditLabel">↩</button> | ||||
|         </div> | ||||
|         <div id="mozilla-format-buttons" class="sectioned-only"> | ||||
|         <div id="mozilla-format-buttons" class="buttons sectioned-only"> | ||||
|           <button id="from-mozilla" i18n-text="importLabel"></button> | ||||
|           <button id="to-mozilla" i18n-text="exportLabel"></button> | ||||
|           <a id="to-mozilla-help" class="svg-inline-wrapper" tabindex="0" | ||||
|  |  | |||
|  | @ -260,23 +260,13 @@ input:invalid { | |||
|   margin-top: .5rem; | ||||
| } | ||||
| 
 | ||||
| #actions > * { | ||||
| #actions .buttons { | ||||
|   display: inline-flex; | ||||
|   flex-wrap: wrap; | ||||
| } | ||||
| 
 | ||||
| #mozilla-format-buttons { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   align-items: center; | ||||
| } | ||||
| 
 | ||||
| #actions > div > a { | ||||
|   height: min-content; | ||||
| } | ||||
| 
 | ||||
| #actions button, | ||||
| #actions > div > a { | ||||
| #actions .buttons > * { | ||||
|   margin: 0 .2rem .5rem 0; | ||||
| } | ||||
| 
 | ||||
|  | @ -1064,13 +1054,6 @@ body.linter-disabled .hidden-unless-compact { | |||
|     margin-left: 1rem; | ||||
|     padding: .25rem 0 .5rem; | ||||
|   } | ||||
|   #actions { | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|     white-space: nowrap; | ||||
|     margin: 0; | ||||
|     box-sizing: border-box; | ||||
|   } | ||||
|   #header input[type="checkbox"] { | ||||
|     vertical-align: middle; | ||||
|   } | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ | |||
| /* global SectionsEditor */ | ||||
| /* global SourceEditor */ | ||||
| /* global baseInit */ | ||||
| /* global chromeSync */// storage-util.js
 | ||||
| /* global clipString createHotkeyInput helpPopup */// util.js
 | ||||
| /* global closeCurrentTab deepEqual sessionStore tryJSONparse */// toolbox.js
 | ||||
| /* global cmFactory */ | ||||
|  | @ -16,7 +17,10 @@ | |||
| //#region init
 | ||||
| 
 | ||||
| baseInit.ready.then(async () => { | ||||
|   await waitForSheet(); | ||||
|   [editor.template] = await Promise.all([ | ||||
|     editor.isUsercss && !editor.style.id && chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate), | ||||
|     waitForSheet(), | ||||
|   ]); | ||||
|   (editor.isUsercss ? SourceEditor : SectionsEditor)(); | ||||
|   await editor.ready; | ||||
|   editor.ready = true; | ||||
|  |  | |||
|  | @ -17,20 +17,19 @@ | |||
| function SourceEditor() { | ||||
|   const {style, /** @type DirtyReporter */dirty} = editor; | ||||
|   let savedGeneration; | ||||
|   let placeholderName = ''; | ||||
|   let prevMode = NaN; | ||||
| 
 | ||||
|   $$remove('.sectioned-only'); | ||||
|   $('#header').on('wheel', headerOnScroll); | ||||
|   $('#sections').textContent = ''; | ||||
|   $('#sections').appendChild($create('.single-editor')); | ||||
| 
 | ||||
|   if (!style.id) setupNewStyle(style); | ||||
|   $('#save-button').onauxclick = e => e.detail === 'tpl' && saveTemplate(); | ||||
| 
 | ||||
|   const cm = cmFactory.create($('.single-editor')); | ||||
|   const sectionFinder = MozSectionFinder(cm); | ||||
|   const sectionWidget = MozSectionWidget(cm, sectionFinder); | ||||
|   editor.livePreview.init(preprocess); | ||||
|   if (!style.id) setupNewStyle(); | ||||
|   createMetaCompiler(meta => { | ||||
|     style.usercssData = meta; | ||||
|     style.name = meta.name; | ||||
|  | @ -75,13 +74,7 @@ function SourceEditor() { | |||
|         } | ||||
|         showLog(res); | ||||
|       } catch (err) { | ||||
|         const i = err.index; | ||||
|         const isNameEmpty = i > 0 && | ||||
|           err.code === 'missingValue' && | ||||
|           sourceCode.slice(sourceCode.lastIndexOf('\n', i - 1), i).trim().endsWith('@name'); | ||||
|         return isNameEmpty | ||||
|           ? saveTemplate(sourceCode) | ||||
|           : showSaveError(err); | ||||
|         showSaveError(err); | ||||
|       } | ||||
|     }, | ||||
|     scrollToEditor: () => {}, | ||||
|  | @ -160,7 +153,7 @@ function SourceEditor() { | |||
|     return name; | ||||
|   } | ||||
| 
 | ||||
|   async function setupNewStyle(style) { | ||||
|   function setupNewStyle() { | ||||
|     style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) + | ||||
|       `/* ${t('usercssReplaceTemplateSectionBody')} */`; | ||||
|     let section = MozDocMapper.styleToCss(style); | ||||
|  | @ -177,17 +170,11 @@ function SourceEditor() { | |||
|       @author         Me | ||||
|       ==/UserStyle== */ | ||||
|     `.replace(/^\s+/gm, '');
 | ||||
| 
 | ||||
|     dirty.clear('sourceGeneration'); | ||||
|     style.sourceCode = ''; | ||||
| 
 | ||||
|     placeholderName = `${style.name || t('usercssReplaceTemplateName')} - ${new Date().toLocaleString()}`; | ||||
|     let code = await chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate); | ||||
|     code = code || DEFAULT_CODE; | ||||
|     code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) => | ||||
|       `${str}${space ? '' : ' '}${placeholderName}`); | ||||
|     style.name = [style.name, new Date().toLocaleString()].filter(Boolean).join(' - '); | ||||
|     // strip the last dummy section if any, add an empty line followed by the section
 | ||||
|     style.sourceCode = code.replace(/\s*@-moz-document[^{]*{[^}]*}\s*$|\s+$/g, '') + '\n\n' + section; | ||||
|     style.sourceCode = (editor.template || DEFAULT_CODE) | ||||
|       .replace(/(@name)(?:([\t\x20]+).*|\n)/, (_, k, space) => `${k}${space || ' '}${style.name}`) | ||||
|       .replace(/\s*@-moz-document[^{]*{[^}]*}\s*$|\s+$/g, '') + '\n\n' + section; | ||||
|     cm.startOperation(); | ||||
|     cm.setValue(style.sourceCode); | ||||
|     cm.clearHistory(); | ||||
|  | @ -199,9 +186,7 @@ function SourceEditor() { | |||
| 
 | ||||
|   function updateMeta() { | ||||
|     const name = style.customName || style.name; | ||||
|     if (name !== placeholderName) { | ||||
|       $('#name').value = name; | ||||
|     } | ||||
|     $('#name').value = name; | ||||
|     $('#enabled').checked = style.enabled; | ||||
|     $('#url').href = style.url; | ||||
|     editor.updateName(); | ||||
|  | @ -236,9 +221,10 @@ function SourceEditor() { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async function saveTemplate(code) { | ||||
|   async function saveTemplate() { | ||||
|     if (await messageBoxProxy.confirm(t('usercssReplaceTemplateConfirmation'))) { | ||||
|       const key = chromeSync.LZ_KEY.usercssTemplate; | ||||
|       const code = cm.getValue(); | ||||
|       await chromeSync.setLZValue(key, code); | ||||
|       if (await chromeSync.getLZValue(key) !== code) { | ||||
|         messageBoxProxy.alert(t('syncStorageErrorSaving')); | ||||
|  |  | |||
							
								
								
									
										34
									
								
								global.css
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								global.css
									
									
									
									
									
								
							|  | @ -33,6 +33,7 @@ button { | |||
|   border: 1px solid hsl(0, 0%, 62%); | ||||
|   font: inherit; | ||||
|   font-size: 13px; | ||||
|   line-height: 1.2; | ||||
|   color: #000; | ||||
|   background-color: hsl(0, 0%, 100%); | ||||
|   background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAeCAYAAADtlXTHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QwGBBwIHvKt6QAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAL0lEQVQI12NoaGgQZ2JgYGBkYmBgYGZiYGBggrMY4VxsYsyoskQQCB2MWAxAMhkADVECDhlW9CoAAAAASUVORK5CYII='); | ||||
|  | @ -307,6 +308,39 @@ body.resizing-v > * { | |||
| } | ||||
| /* header resizer - end */ | ||||
| 
 | ||||
| .split-btn { | ||||
|   position: relative; | ||||
| } | ||||
| .split-btn-pedal { | ||||
|   margin-left: -1px !important; | ||||
|   padding-left: .2em !important; | ||||
|   padding-right: .2em !important; | ||||
| } | ||||
| .split-btn-pedal::after { | ||||
|   content: '\25BC'; /* down triangle */ | ||||
|   font-size: 90%; | ||||
| } | ||||
| .split-btn-pedal.active { | ||||
|   box-shadow: inset 0 0 100px rgba(0, 0, 0, .2); | ||||
| } | ||||
| .split-btn-menu { | ||||
|   background: #fff; | ||||
|   position: absolute; | ||||
|   box-shadow: 2px 3px 7px rgba(0, 0, 0, .5); | ||||
|   border: 1px solid hsl(180deg, 50%, 50%); | ||||
|   white-space: nowrap; | ||||
|   cursor: pointer; | ||||
|   padding: .25em 0; | ||||
| } | ||||
| .split-btn-menu > * { | ||||
|   padding: .5em 1em; | ||||
|   display: block; | ||||
| } | ||||
| .split-btn-menu > :hover { | ||||
|   background-color: hsla(180deg, 50%, 50%, .25); | ||||
|   color: #000; | ||||
| } | ||||
| 
 | ||||
| @supports (-moz-appearance: none) { | ||||
|   .moz-appearance-bug .svg-icon.checked, | ||||
|   .moz-appearance-bug .onoffswitch input, | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| /* global $ $$ focusAccessibility getEventKeyName */// dom.js
 | ||||
| /* global $$ $ $create focusAccessibility getEventKeyName moveFocus */// dom.js
 | ||||
| /* global debounce */// toolbox.js
 | ||||
| /* global t */// localization.js
 | ||||
| 'use strict'; | ||||
|  | @ -6,13 +6,14 @@ | |||
| /** DOM housekeeping after a page finished loading */ | ||||
| 
 | ||||
| (() => { | ||||
|   const SPLIT_BTN_MENU = '.split-btn-menu'; | ||||
|   splitLongTooltips(); | ||||
|   addTooltipsToEllipsized(); | ||||
|   window.on('mousedown', suppressFocusRingOnClick, {passive: true}); | ||||
|   window.on('keydown', keepFocusRingOnTabbing, {passive: true}); | ||||
|   window.on('keypress', clickDummyLinkOnEnter); | ||||
|   window.on('wheel', changeFocusedInputOnWheel, {capture: true, passive: false}); | ||||
|   window.on('click', showTooltipNote); | ||||
|   window.on('click', e => splitMenu(e) || showTooltipNote(e)); | ||||
|   window.on('resize', () => debounce(addTooltipsToEllipsized, 100)); | ||||
|   // Removing transition-suppressor rule
 | ||||
|   const {sheet} = $('link[href$="global.css"]'); | ||||
|  | @ -78,19 +79,44 @@ | |||
|         let el = document.activeElement; | ||||
|         if (el) { | ||||
|           el = el.closest('[data-focused-via-click]'); | ||||
|           if (el) delete el.dataset.focusedViaClick; | ||||
|           focusAccessibility.toggle(el, false); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function splitMenu(event) { | ||||
|     const prevMenu = $(SPLIT_BTN_MENU); | ||||
|     const prevPedal = (prevMenu || {}).previousElementSibling; | ||||
|     const pedal = event.target.closest('.split-btn-pedal'); | ||||
|     const entry = prevMenu && event.target.closest(SPLIT_BTN_MENU + '>*'); | ||||
|     if (prevMenu) prevMenu.remove(); | ||||
|     if (prevPedal) prevPedal.classList.remove('active'); | ||||
|     if (pedal && pedal !== prevPedal) { | ||||
|       const menu = $create(SPLIT_BTN_MENU, | ||||
|         Array.from(pedal.attributes, ({name, value}) => | ||||
|           name.startsWith('menu-') && | ||||
|           $create('a', {tabIndex: 0, __cmd: name.split('-').pop()}, value) | ||||
|         )); | ||||
|       menu.on('focusout', e => e.target === menu && splitMenu(e)); | ||||
|       pedal.classList.toggle('active'); | ||||
|       pedal.after(menu); | ||||
|       moveFocus(menu, 0); | ||||
|       focusAccessibility.toggle(menu.firstChild, focusAccessibility.get(pedal)); | ||||
|     } | ||||
|     if (entry) { | ||||
|       prevPedal.previousElementSibling.dispatchEvent(new CustomEvent('auxclick', { | ||||
|         detail: entry.__cmd, | ||||
|         bubbles: true, | ||||
|       })); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function suppressFocusRingOnClick({target}) { | ||||
|     const el = focusAccessibility.closest(target); | ||||
|     if (el) { | ||||
|       focusAccessibility.lastFocusedViaClick = true; | ||||
|       if (el.dataset.focusedViaClick === undefined) { | ||||
|         el.dataset.focusedViaClick = ''; | ||||
|       } | ||||
|       focusAccessibility.toggle(el, true); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -31,6 +31,8 @@ Object.assign(EventTarget.prototype, { | |||
| const focusAccessibility = { | ||||
|   // last event's focusedViaClick
 | ||||
|   lastFocusedViaClick: false, | ||||
|   get: el => el && el.dataset.focusedViaClick != null, | ||||
|   toggle: (el, state) => el && toggleDataset(el, 'focusedViaClick', state), | ||||
|   // to avoid a full layout recalc due to changes on body/root
 | ||||
|   // we modify the closest focusable element (like input or button or anything with tabindex=0)
 | ||||
|   closest(el) { | ||||
|  |  | |||
|  | @ -73,7 +73,6 @@ Object.assign(t, { | |||
|         if (toInsert) { | ||||
|           node.insertBefore(toInsert, before || null); | ||||
|         } | ||||
|         node.removeAttribute(name); | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user