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 @@
+