From 8ddeef221b1b8ab8fc506bf28ef8897733539c9e Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 13 Jan 2022 12:47:37 +0300 Subject: [PATCH] resizable header panel (#1378) --- _locales/en/messages.json | 4 + edit.html | 1 + edit/edit.css | 7 +- edit/sections-editor-section.js | 6 +- global.css | 48 ++++++++ install-usercss.html | 1 + install-usercss/install-usercss.css | 3 +- js/dom-on-load.js | 124 ++++++++++++++++++++ js/dom.js | 169 ++++++---------------------- js/header-resizer.js | 48 ++++++++ js/prefs.js | 6 + manage.html | 1 + manage/manage.css | 2 - 13 files changed, 274 insertions(+), 146 deletions(-) create mode 100644 js/dom-on-load.js create mode 100644 js/header-resizer.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 251f8dd1..29e2863c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -501,6 +501,10 @@ "gettingStyles": { "message": "Getting all styles..." }, + "headerResizerHint": { + "message": "Hold Shift to resize only in this type of UI, i.e. editor, manager, installer", + "description": "Tooltip for the header panel resizer" + }, "helpAlt": { "message": "Help", "description": "Alternate text for help buttons" diff --git a/edit.html b/edit.html index 60efff60..b862d77f 100644 --- a/edit.html +++ b/edit.html @@ -466,6 +466,7 @@ +
+
diff --git a/install-usercss/install-usercss.css b/install-usercss/install-usercss.css index 0b25f5a2..7b373866 100644 --- a/install-usercss/install-usercss.css +++ b/install-usercss/install-usercss.css @@ -29,10 +29,9 @@ input:disabled + span { #header, .warnings { - flex: 0 0 280px; + flex: 0 0 var(--header-width); box-sizing: border-box; padding: 1rem; - border-right: 1px dashed #aaa; box-shadow: 0 0 50px -18px black; overflow-wrap: break-word; overflow-y: auto; diff --git a/js/dom-on-load.js b/js/dom-on-load.js new file mode 100644 index 00000000..ba91a40b --- /dev/null +++ b/js/dom-on-load.js @@ -0,0 +1,124 @@ +/* global $ $$ focusAccessibility getEventKeyName */// dom.js +/* global debounce */// toolbox.js +/* global t */// localization.js +'use strict'; + +/** DOM housekeeping after a page finished loading */ + +(() => { + 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('resize', () => debounce(addTooltipsToEllipsized, 100)); + // Removing transition-suppressor rule + const {sheet} = $('link[href^="global.css"]'); + for (let i = 0, rule; (rule = sheet.cssRules[i]); i++) { + if (/#\\1\s?transition-suppressor/.test(rule.selectorText)) { + sheet.deleteRule(i); + break; + } + } + + function changeFocusedInputOnWheel(event) { + const el = document.activeElement; + if (!el || el !== event.target && !el.contains(event.target)) { + return; + } + const isSelect = el.tagName === 'SELECT'; + if (isSelect || el.tagName === 'INPUT' && el.type === 'range') { + const key = isSelect ? 'selectedIndex' : 'valueAsNumber'; + const old = el[key]; + const rawVal = old + Math.sign(event.deltaY) * (el.step || 1); + el[key] = Math.max(el.min || 0, Math.min(el.max || el.length - 1, rawVal)); + if (el[key] !== old) { + el.dispatchEvent(new Event('change', {bubbles: true})); + } + event.preventDefault(); + } + event.stopImmediatePropagation(); + } + + /** Displays a full text tooltip on buttons with ellipsis overflow and no inherent title */ + function addTooltipsToEllipsized() { + for (const btn of document.getElementsByTagName('button')) { + if (btn.title && !btn.titleIsForEllipsis) { + continue; + } + const width = btn.offsetWidth; + if (!width || btn.preresizeClientWidth === width) { + continue; + } + btn.preresizeClientWidth = width; + if (btn.scrollWidth > width) { + const text = btn.textContent; + btn.title = text.includes('\u00AD') ? text.replace(/\u00AD/g, '') : text; + btn.titleIsForEllipsis = true; + } else if (btn.title) { + btn.title = ''; + } + } + } + + function clickDummyLinkOnEnter(e) { + if (getEventKeyName(e) === 'Enter') { + const a = e.target.closest('a'); + const isDummy = a && !a.href && a.tabIndex === 0; + if (isDummy) a.dispatchEvent(new MouseEvent('click', {bubbles: true})); + } + } + + function keepFocusRingOnTabbing(event) { + if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) { + focusAccessibility.lastFocusedViaClick = false; + setTimeout(() => { + let el = document.activeElement; + if (el) { + el = el.closest('[data-focused-via-click]'); + if (el) delete el.dataset.focusedViaClick; + } + }); + } + } + + function suppressFocusRingOnClick({target}) { + const el = focusAccessibility.closest(target); + if (el) { + focusAccessibility.lastFocusedViaClick = true; + if (el.dataset.focusedViaClick === undefined) { + el.dataset.focusedViaClick = ''; + } + } + } + + function showTooltipNote(event) { + const el = event.target.closest('[data-cmd=note]'); + if (el) { + event.preventDefault(); + window.messageBoxProxy.show({ + className: 'note center-dialog', + contents: el.dataset.title || el.title, + buttons: [t('confirmClose')], + }); + } + } + + function splitLongTooltips() { + for (const el of $$('[title]')) { + el.dataset.title = el.title; + el.title = el.title.replace(/<\/?\w+>/g, ''); // strip html tags + if (el.title.length < 50) { + continue; + } + const newTitle = el.title + .split('\n') + .map(s => s.replace(/([^.][.。?!]|.{50,60},)\s+/g, '$1\n')) + .map(s => s.replace(/(.{50,80}(?=.{40,}))\s+/g, '$1\n')) + .join('\n'); + if (newTitle !== el.title) el.title = newTitle; + } + } +})(); diff --git a/js/dom.js b/js/dom.js index a2601fc1..612acf71 100644 --- a/js/dom.js +++ b/js/dom.js @@ -1,6 +1,5 @@ -/* global FIREFOX debounce */// toolbox.js +/* global FIREFOX */// toolbox.js /* global prefs */ -/* global t */// localization.js 'use strict'; /* exported @@ -9,9 +8,11 @@ $remove $$remove animateElement + focusAccessibility getEventKeyName messageBoxProxy moveFocus + onDOMready scrollElementIntoView setupLivePrefs showSpinner @@ -427,6 +428,8 @@ async function waitForSheet({ //#endregion //#region Internals +const dom = {}; + (() => { const Collapsible = { @@ -459,42 +462,33 @@ async function waitForSheet({ } }, }; - - window.on('mousedown', suppressFocusRingOnClick, {passive: true}); - window.on('keydown', keepFocusRingOnTabbing, {passive: true}); - + const lazyScripts = [ + '/js/dom-on-load', + ]; + const elHtml = document.documentElement; if (!/^Win\d+/.test(navigator.platform)) { - document.documentElement.classList.add('non-windows'); + elHtml.classList.add('non-windows'); } // set language for a) CSS :lang pseudo and b) hyphenation - document.documentElement.setAttribute('lang', chrome.i18n.getUILanguage()); - document.on('keypress', clickDummyLinkOnEnter); - document.on('wheel', changeFocusedInputOnWheel, {capture: true, passive: false}); - document.on('click', showTooltipNote); - - Promise.resolve().then(async () => { - if (!chrome.app) addFaviconFF(); - await prefs.ready; - waitForSelector('details[data-pref]', {recur: Collapsible.bindEvents}); - }); - - onDOMready().then(() => { - splitLongTooltips(); - debounce(addTooltipsToEllipsized, 500); - window.on('resize', () => debounce(addTooltipsToEllipsized, 100)); - }); - - window.on('load', () => { - const {sheet} = $('link[href^="global.css"]'); - for (let i = 0, rule; (rule = sheet.cssRules[i]); i++) { - if (/#\\1\s?transition-suppressor/.test(rule.selectorText)) { - sheet.deleteRule(i); - break; - } - } - }, {once: true}); - - function addFaviconFF() { + elHtml.setAttribute('lang', chrome.i18n.getUILanguage()); + // set up header width resizer + const HW = 'headerWidth.'; + const HWprefId = HW + location.pathname.match(/^.(\w*)/)[1]; + if (prefs.knownKeys.includes(HWprefId)) { + Object.assign(dom, { + HW, + HWprefId, + setHWProp(width) { + width = Math.round(Math.max(200, Math.min(innerWidth / 3, Number(width) || 0))); + elHtml.style.setProperty('--header-width', width + 'px'); + return width; + }, + }); + prefs.ready.then(() => dom.setHWProp(prefs.get(HWprefId))); + lazyScripts.push('/js/header-resizer'); + } + // add favicon in FF + if (FIREFOX) { const iconset = ['', 'light/'][prefs.get('iconset')] || ''; for (const size of [38, 32, 19, 16]) { document.head.appendChild($create('link', { @@ -504,105 +498,12 @@ async function waitForSheet({ })); } } - - function changeFocusedInputOnWheel(event) { - const el = document.activeElement; - if (!el || el !== event.target && !el.contains(event.target)) { - return; - } - const isSelect = el.tagName === 'SELECT'; - if (isSelect || el.tagName === 'INPUT' && el.type === 'range') { - const key = isSelect ? 'selectedIndex' : 'valueAsNumber'; - const old = el[key]; - const rawVal = old + Math.sign(event.deltaY) * (el.step || 1); - el[key] = Math.max(el.min || 0, Math.min(el.max || el.length - 1, rawVal)); - if (el[key] !== old) { - el.dispatchEvent(new Event('change', {bubbles: true})); - } - event.preventDefault(); - } - event.stopImmediatePropagation(); - } - - /** Displays a full text tooltip on buttons with ellipsis overflow and no inherent title */ - function addTooltipsToEllipsized() { - for (const btn of document.getElementsByTagName('button')) { - if (btn.title && !btn.titleIsForEllipsis) { - continue; - } - const width = btn.offsetWidth; - if (!width || btn.preresizeClientWidth === width) { - continue; - } - btn.preresizeClientWidth = width; - if (btn.scrollWidth > width) { - const text = btn.textContent; - btn.title = text.includes('\u00AD') ? text.replace(/\u00AD/g, '') : text; - btn.titleIsForEllipsis = true; - } else if (btn.title) { - btn.title = ''; - } - } - } - - function clickDummyLinkOnEnter(e) { - if (getEventKeyName(e) === 'Enter') { - const a = e.target.closest('a'); - const isDummy = a && !a.href && a.tabIndex === 0; - if (isDummy) a.dispatchEvent(new MouseEvent('click', {bubbles: true})); - } - } - - function keepFocusRingOnTabbing(event) { - if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) { - focusAccessibility.lastFocusedViaClick = false; - setTimeout(() => { - let el = document.activeElement; - if (el) { - el = el.closest('[data-focused-via-click]'); - if (el) delete el.dataset.focusedViaClick; - } - }); - } - } - - function suppressFocusRingOnClick({target}) { - const el = focusAccessibility.closest(target); - if (el) { - focusAccessibility.lastFocusedViaClick = true; - if (el.dataset.focusedViaClick === undefined) { - el.dataset.focusedViaClick = ''; - } - } - } - - function showTooltipNote(event) { - const el = event.target.closest('[data-cmd=note]'); - if (el) { - event.preventDefault(); - window.messageBoxProxy.show({ - className: 'note center-dialog', - contents: el.dataset.title || el.title, - buttons: [t('confirmClose')], - }); - } - } - - function splitLongTooltips() { - for (const el of $$('[title]')) { - el.dataset.title = el.title; - el.title = el.title.replace(/<\/?\w+>/g, ''); // strip html tags - if (el.title.length < 50) { - continue; - } - const newTitle = el.title - .split('\n') - .map(s => s.replace(/([^.][.。?!]|.{50,60},)\s+/g, '$1\n')) - .map(s => s.replace(/(.{50,80}(?=.{40,}))\s+/g, '$1\n')) - .join('\n'); - if (newTitle !== el.title) el.title = newTitle; - } - } + prefs.ready.then(() => { + waitForSelector('details[data-pref]', {recur: Collapsible.bindEvents}); + }); + window.on('load', () => { + require(lazyScripts); + }, {once: true}); })(); //#endregion diff --git a/js/header-resizer.js b/js/header-resizer.js new file mode 100644 index 00000000..0017b4d4 --- /dev/null +++ b/js/header-resizer.js @@ -0,0 +1,48 @@ +/* global $ $$ dom */// dom.js +/* global prefs */ +'use strict'; + +(() => { + let curW = $('#header').offsetWidth; + let offset, perPage; + prefs.subscribe(dom.HWprefId, (key, val) => setWidth(val)); + $('#header-resizer').onmousedown = e => { + if (e.button) return; + offset = curW - e.clientX; + perPage = e.shiftKey; + document.body.classList.add('resizing-h'); + document.on('mousemove', resize); + document.on('mouseup', resizeStop); + }; + + function resize(e) { + setWidth(offset + e.clientX); + } + + function resizeStop() { + document.off('mouseup', resizeStop); + document.off('mousemove', resize); + document.body.classList.remove('resizing-h'); + save(); + } + + function save() { + if (perPage) { + prefs.set(dom.HWprefId, curW); + } else { + for (const k of prefs.knownKeys) { + if (k.startsWith(dom.HW)) prefs.set(k, curW); + } + } + } + + function setWidth(w) { + const delta = (w = dom.setHWProp(w)) - curW; + if (delta) { + curW = w; + for (const el of $$('.CodeMirror-linewidget[style*="width:"]')) { + el.style.width = parseFloat(el.style.width) - delta + 'px'; + } + } + } +})(); diff --git a/js/prefs.js b/js/prefs.js index 7f461173..a4f638b5 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -123,6 +123,12 @@ 'badgeDisabled': '#8B0000', // badge background color when disabled 'badgeNormal': '#006666', // badge background color + /* Using separate values instead of a single {} to ensure type control. + * Sub-key is the first word in the html's file name. */ + 'headerWidth.edit': 280, + 'headerWidth.install': 280, + 'headerWidth.manage': 280, + 'popupWidth': 246, // popup width in pixels 'updateInterval': 24, // user-style automatic update interval, hours (0 = disable) diff --git a/manage.html b/manage.html index dc9e4dd1..e51d29fa 100644 --- a/manage.html +++ b/manage.html @@ -354,6 +354,7 @@
+
diff --git a/manage/manage.css b/manage/manage.css index 75b490da..f88b6f86 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -1,5 +1,4 @@ :root { - --header-width: 280px; --name-padding-left: 20px; --name-padding-right: 40px; --actions-width: 75px; @@ -52,7 +51,6 @@ a:hover { position: fixed; top: 0; padding: 1rem; - border-right: 1px dashed #AAA; box-shadow: 0 0 50px -18px black; overflow: auto; box-sizing: border-box;