From 8d3e01e05a6207f21cea2a87722c6528c0604468 Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 18 Feb 2022 03:47:22 +0300 Subject: [PATCH] shuffle and tidy up options (#1406) * move updates/sync to the top, theme to the bottom * remove font override * replace 'Back to manage' with 'Close' * add a note for the built-in shortcuts UI in FF - update button + confirm reset * one button to connect/disconnect * shorten ids * simplify/extract sync js * reuse :invalid style --- _locales/en/messages.json | 5 +- edit/edit.css | 4 - global.css | 5 + js/localization.js | 20 +-- options.html | 151 ++++++++++++----------- options/options-sync.js | 96 +++++++++++++++ options/options.css | 85 +++---------- options/options.js | 250 +++++--------------------------------- 8 files changed, 245 insertions(+), 371 deletions(-) create mode 100644 options/options-sync.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 57bf1b02..8ca3a34e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1163,7 +1163,7 @@ "message": "Reset options" }, "optionsStylusThemes": { - "message": "Find a Stylus UI theme" + "message": "Click Stylus icon in the browser toolbar on any Stylus page including this one, then click 'Find styles'" }, "optionsSubheading": { "message": "More Options", @@ -1500,6 +1500,9 @@ "shortcutsNote": { "message": "Define keyboard shortcuts" }, + "shortcutsNoteFF": { + "message": "In Firefox 66+ you can open the built-in shortcuts UI manually:\n1) right-click Stylus icon in the toolbar and choose 'Manage'\n(alternatively, open about:addons via the main menu or Ctrl-Shift-A),\n2) in the page that opens click the cog wheel icon in the top right corner,\n3) choose 'Manage extension shortcuts'.\n\nYou can also customize the shortcuts here." + }, "sortDateNewestFirst": { "message": "newest first", "description": "Text added to indicate that sorting a date would add the newest entries at the top" diff --git a/edit/edit.css b/edit/edit.css index 8ee54035..434cea6f 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -215,10 +215,6 @@ label { #options span .svg-icon { margin-top: -3px; /* inline info and config icons */ } -input:invalid { - background-color: rgba(255, 0, 0, 0.1); - color: darkred; -} #enabled { margin-left: 0; } diff --git a/global.css b/global.css index 0dfa6721..304a88b7 100644 --- a/global.css +++ b/global.css @@ -116,6 +116,11 @@ input[type=search] { border: 1px solid var(--c65); } +input:invalid { + background-color: rgba(255, 0, 0, 0.1); + color: darkred; +} + .svg-icon { cursor: pointer; vertical-align: middle; diff --git a/js/localization.js b/js/localization.js index 9bc63be6..e99776c6 100644 --- a/js/localization.js +++ b/js/localization.js @@ -43,6 +43,7 @@ Object.assign(t, { continue; } if (node.localName === 'template') { + node.remove(); t.createTemplate(node); continue; } @@ -87,17 +88,20 @@ Object.assign(t, { text.replace(t.RX_WORD_BREAK, '$&\u00AD'); }, - createTemplate(node) { - const el = node.content.firstElementChild.cloneNode(true); - t.NodeList(el); - t.template[node.dataset.id] = el; - // compress inter-tag whitespace to reduce number of DOM nodes by 25% - const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); + createTemplate(el) { + const {content} = el; + const toRemove = []; + // Compress inter-tag whitespace to reduce DOM tree and avoid space between elements without flex + const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT); for (let n; (n = walker.nextNode());) { - if (!/[\xA0\S]/.test(n.textContent)) { // allow \xA0 to keep   - n.remove(); + if (!/[\xA0\S]/.test(n.textContent) || // allowing \xA0 so as to preserve   + n.nodeType === Node.COMMENT_NODE) { + toRemove.push(n); } } + toRemove.forEach(n => n.remove()); + t.NodeList(content.querySelectorAll('*')); + t.template[el.dataset.id] = content.childNodes.length > 1 ? content : content.childNodes[0]; }, createText(str) { diff --git a/options.html b/options.html index 34f8f2d6..0ebedb35 100644 --- a/options.html +++ b/options.html @@ -5,7 +5,7 @@ Stylus - + @@ -19,6 +19,30 @@ + + + + @@ -38,12 +62,55 @@
-
-

+
+

-
- + +
+
+ +
+

+
+ + + + + + + + + + + + + + +
+
+ + +
@@ -207,62 +274,6 @@
-
-

-
- -
-
- -
-

-
-
- -
- - -
-
-
-
- - - -
-
-
- - - - -
-
-
-

@@ -325,18 +336,16 @@
+
+

+
+
- - -
- -
-
- + + +
diff --git a/options/options-sync.js b/options/options-sync.js new file mode 100644 index 00000000..c62c54e0 --- /dev/null +++ b/options/options-sync.js @@ -0,0 +1,96 @@ +/* global API msg */// msg.js +/* global t */// localization.js +/* global $ $$ toggleDataset waitForSelector */// dom.js +/* global capitalize */// toolbox.js +'use strict'; + +Promise.all([ + API.sync.getStatus(), + waitForSelector('.sync-options'), +]).then(([status, elSync]) => { + const elCloud = $('.cloud-name', elSync); + const elToggle = $('.connect', elSync); + const elSyncNow = $('.sync-now', elSync); + const elStatus = $('.sync-status', elSync); + const elLogin = $('.sync-login', elSync); + const elDriveOptions = $$('.drive-options', elSync); + updateButtons(); + msg.onExtension(e => { + if (e.method === 'syncStatusUpdate') { + setStatus(e.status); + } + }); + elCloud.on('change', updateButtons); + elToggle.onclick = async () => { + if (elToggle.dataset.cmd === 'start') { + await API.sync.setDriveOptions(elCloud.value, getDriveOptions()); + await API.sync.start(elCloud.value); + } else { + await API.sync.stop(); + } + }; + elSyncNow.onclick = API.sync.syncNow; + elLogin.onclick = async () => { + await API.sync.login(); + await API.sync.syncNow(); + }; + + function getDriveOptions() { + const result = {}; + for (const el of $$(`[data-drive=${elCloud.value}] [data-option]`)) { + result[el.dataset.option] = el.value; + } + return result; + } + + function setDriveOptions(options) { + for (const el of $$(`[data-drive=${elCloud.value}] [data-option]`)) { + el.value = options[el.dataset.option] || ''; + } + } + + function setStatus(newStatus) { + status = newStatus; + updateButtons(); + } + + async function updateButtons() { + const {state, STATES} = status; + const isConnected = state === STATES.connected; + const off = state === STATES.disconnected; + if (status.currentDriveName) { + elCloud.value = status.currentDriveName; + } + elCloud.disabled = !off; + elToggle.disabled = status.syncing; + elToggle.textContent = t(`optionsSync${off ? 'Connect' : 'Disconnect'}`); + elToggle.dataset.cmd = off ? 'start' : 'stop'; + elSyncNow.disabled = !isConnected || status.syncing || !status.login; + elStatus.textContent = getStatusText(); + elLogin.hidden = !isConnected || status.login; + for (const el of elDriveOptions) { + el.hidden = el.dataset.drive !== elCloud.value; + el.disabled = !off; + } + toggleDataset(elSync, 'enabled', elCloud.value !== 'none'); + setDriveOptions(await API.sync.getDriveOptions(elCloud.value)); + } + + function getStatusText() { + if (status.syncing) { + const {phase, loaded, total} = status.progress || {}; + return phase + ? t(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total], false) || + `${phase} ${loaded} / ${total}` + : t('optionsSyncStatusSyncing'); + } + const {state, errorMessage, STATES} = status; + if (errorMessage && (state === STATES.connected || state === STATES.disconnected)) { + return errorMessage; + } + if (state === STATES.connected && !status.login) { + return t('optionsSyncStatusRelogin'); + } + return t(`optionsSyncStatus${capitalize(state)}`, null, false) || state; + } +}); diff --git a/options/options.css b/options/options.css index 32b52c8d..07925b87 100644 --- a/options/options.css +++ b/options/options.css @@ -8,8 +8,6 @@ html { body { background: none; - font-family: "Helvetica Neue", Helvetica, sans-serif; - font-size: 12px; display: flex; flex-direction: column; width: auto; @@ -184,12 +182,6 @@ input[type=number] { text-align: right; } -input[type=number]:invalid, -input[type=text]:invalid { - background-color: rgba(255, 0, 0, 0.1); - color: darkred; -} - input[type="color"] { box-sizing: border-box; height: 2em; @@ -201,25 +193,16 @@ input[type=time] { } #actions { - justify-content: space-around; - align-items: stretch; - flex-wrap: wrap; + justify-content: center; padding: .5em 1em 1em; - white-space: nowrap; background-color: rgba(0, 0, 0, .05); margin: 0; border-top: 1px solid var(--c60); border-bottom: none; - min-height: min-content; /* workaround for old Chrome ~70 bug when the window height is small */ } #actions button { - width: auto; - margin-top: .5em; -} - -#actions button:not(:last-child) { - margin-right: 4px; + margin: .5em 1em 0 0; } [data-cmd="check-updates"] button { @@ -229,41 +212,6 @@ input[type=time] { padding: .5em 0 .5em 0; cursor: pointer; } -.update-in-progress [data-cmd="check-updates"] { - opacity: .5; - pointer-events: none; -} - -.update-in-progress #update-progress { - position: absolute; - top: 0; - left: 0; - bottom: 0; - background-color: currentColor; - content: ""; - opacity: .35; -} - -#updates-installed { - position: absolute; - font-size: 85%; - margin-top: 1px; -} - -#updates-installed::after { - content: attr(data-value); - margin-left: .5ex; - font-weight: bold; -} - -#updates-installed:not([data-value]), -#updates-installed[data-value=""] { - display: none; -} - -html:not(.firefox):not(.opera) #updates { - margin-bottom: 0; -} .svg-inline-wrapper .svg-icon { width: 16px; @@ -298,24 +246,27 @@ html:not(.firefox):not(.opera) #updates { .sync-status::first-letter { text-transform: uppercase; } -.sync-options .drive-options { - margin: 0; - padding: 0; - border: 0; +[data-drive="webdav"] { + width: 100%; + border-spacing: 0; + border-collapse: collapse; } -.drive-options > :not([hidden]) { - display: table; +[data-drive="webdav"] td:nth-child(1) { + padding: 1px .5em 1px 0; + max-width: 10em; + overflow-wrap: break-word; +} +[data-drive="webdav"] td:nth-child(2) { + padding: 1px 0; width: 100%; } -.drive-options > * > label { - display: table-row; -} -.drive-options > * > label > * { - display: table-cell; -} -.drive-options > * input { +[data-drive="webdav"] input { width: 100%; box-sizing: border-box; + line-height: 1.5; +} +.sync-options:not([data-enabled]) .actions { + display: none; } .sync-options .actions button { margin-top: .5em; diff --git a/options/options.js b/options/options.js index db0fe3df..a1a6a9ce 100644 --- a/options/options.js +++ b/options/options.js @@ -1,20 +1,11 @@ -/* global API msg */// msg.js +/* global API */// msg.js /* global prefs */ /* global t */// localization.js -/* global - $ - $$ - $create - $createLink - getEventKeyName - messageBoxProxy - setupLivePrefs -*/// dom.js +/* global $ $$ getEventKeyName messageBoxProxy setupLivePrefs */// dom.js /* global CHROME_POPUP_BORDER_BUG FIREFOX URLS - capitalize clamp ignoreChromeError openURL @@ -23,242 +14,61 @@ setupLivePrefs(); $$('input[min], input[max]').forEach(enforceInputRange); - if (CHROME_POPUP_BORDER_BUG) { $('.chrome-no-popup-border').classList.remove('chrome-no-popup-border'); } - if (FIREFOX && 'update' in (chrome.commands || {})) { - $('[data-cmd="open-keyboard"]').classList.remove('chromium-only'); + $('#shortcuts').classList.remove('chromium-only'); } - // actions $('#options-close-icon').onclick = () => { top.dispatchEvent(new CustomEvent('closeOptions')); }; - -document.onclick = e => { - const target = e.target.closest('[data-cmd]'); - if (!target) { - return; +$('#manage').onclick = () => { + API.openManage(); +}; +$('#shortcuts').onclick = () => { + if (FIREFOX) { + customizeHotkeys(); + } else { + openURL({url: URLS.configureCommands}); } - // prevent double-triggering in case a sub-element was clicked - e.stopPropagation(); - - switch (target.dataset.cmd) { - case 'open-manage': - API.openManage(); - break; - - case 'check-updates': - checkUpdates(); - break; - - case 'open-keyboard': - if (FIREFOX) { - customizeHotkeys(); - } else { - openURL({url: URLS.configureCommands}); +}; +$('#reset').onclick = async () => { + if (await messageBoxProxy.confirm(t('confirmDiscardChanges'))) { + for (const el of $$('input')) { + const id = el.id || el.name; + if (prefs.knownKeys.includes(id)) { + prefs.reset(id); } - e.preventDefault(); - break; - - case 'reset': - $$('input') - .filter(input => prefs.knownKeys.includes(input.id)) - .forEach(input => prefs.reset(input.id)); - break; + } } }; -// sync to cloud -(() => { - const elCloud = $('.sync-options .cloud-name'); - const elStart = $('.sync-options .connect'); - const elStop = $('.sync-options .disconnect'); - const elSyncNow = $('.sync-options .sync-now'); - const elStatus = $('.sync-options .sync-status'); - const elLogin = $('.sync-options .sync-login'); - const elDriveOptions = $('.sync-options .drive-options'); - /** @type {Sync.Status} */ - let status = {}; - msg.onExtension(e => { - if (e.method === 'syncStatusUpdate') { - setStatus(e.status); - } - }); - API.sync.getStatus() - .then(setStatus); - - elCloud.on('change', updateButtons); - for (const [btn, fn] of [ - [elStart, async () => { - await API.sync.setDriveOptions(elCloud.value, getDriveOptions()); - await API.sync.start(elCloud.value); - }], - [elStop, API.sync.stop], - [elSyncNow, API.sync.syncNow], - [elLogin, async () => { - await API.sync.login(); - await API.sync.syncNow(); - }], - ]) { - btn.on('click', e => { - if (getEventKeyName(e) === 'MouseL') { - fn(); - } - }); - } - - function getDriveOptions() { - const result = {}; - for (const el of $$(`[data-drive=${elCloud.value}] [data-option]`)) { - result[el.dataset.option] = el.value; - } - return result; - } - - function setDriveOptions(options) { - for (const el of $$(`[data-drive=${elCloud.value}] [data-option]`)) { - el.value = options[el.dataset.option] || ''; - } - } - - function setStatus(newStatus) { - status = newStatus; - updateButtons(); - } - - async function updateButtons() { - const {state, STATES} = status; - const isConnected = state === STATES.connected; - const isDisconnected = state === STATES.disconnected; - if (status.currentDriveName) { - elCloud.value = status.currentDriveName; - } - for (const [el, enable] of [ - [elCloud, isDisconnected], - [elDriveOptions, isDisconnected], - [elStart, isDisconnected && elCloud.value !== 'none'], - [elStop, isConnected && !status.syncing], - [elSyncNow, isConnected && !status.syncing && status.login], - ]) { - el.disabled = !enable; - } - elStatus.textContent = getStatusText(); - elLogin.hidden = !isConnected || status.login; - for (const el of elDriveOptions.children) { - el.hidden = el.dataset.drive !== elCloud.value; - } - setDriveOptions(await API.sync.getDriveOptions(elCloud.value)); - } - - function getStatusText() { - if (status.syncing) { - const {phase, loaded, total} = status.progress || {}; - return phase - ? t(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total], false) || - `${phase} ${loaded} / ${total}` - : t('optionsSyncStatusSyncing'); - } - - const {state, errorMessage, STATES} = status; - if (errorMessage && (state === STATES.connected || state === STATES.disconnected)) { - return errorMessage; - } - if (state === STATES.connected && !status.login) { - return t('optionsSyncStatusRelogin'); - } - return t(`optionsSyncStatus${capitalize(state)}`, null, false) || state; - } -})(); - -function checkUpdates() { - let total = 0; - let checked = 0; - let updated = 0; - const maxWidth = $('#update-progress').parentElement.clientWidth; - - chrome.runtime.onConnect.addListener(function onConnect(port) { - if (port.name !== 'updater') return; - port.onMessage.addListener(observer); - chrome.runtime.onConnect.removeListener(onConnect); - }); - - API.updater.checkAllStyles({observe: true}); - - function observer(info) { - if ('count' in info) { - total = info.count; - document.body.classList.add('update-in-progress'); - } else if (info.updated) { - updated++; - checked++; - } else if (info.error) { - checked++; - } else if (info.done) { - document.body.classList.remove('update-in-progress'); - } - $('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px'; - $('#updates-installed').dataset.value = updated || ''; - } -} - function customizeHotkeys() { - // command name -> i18n id - const hotkeys = new Map([ - ['_execute_browser_action', 'optionsCustomizePopup'], - ['openManage', 'openManage'], - ['styleDisableAll', 'disableAllStyles'], - ]); - messageBoxProxy.show({ title: t('shortcutsNote'), - contents: [ - $create('table', - [...hotkeys.entries()].map(([cmd, i18n]) => - $create('tr', [ - $create('td', t(i18n)), - $create('td', - $create('input', { - id: 'hotkey.' + cmd, - type: 'search', - //placeholder: t('helpKeyMapHotkey'), - })), - ]))), - ], - className: 'center', + contents: t.template.shortcutsFF.cloneNode(true), + className: 'center-dialog pre-line', buttons: [t('confirmClose')], onshow(box) { - const ids = []; - for (const cmd of hotkeys.keys()) { - const id = 'hotkey.' + cmd; - ids.push(id); - $('#' + id).oninput = onInput; - } - setupLivePrefs(ids); - $('button', box).insertAdjacentElement('beforebegin', - $createLink( - 'https://developer.mozilla.org/Add-ons/WebExtensions/manifest.json/commands#Key_combinations', - t('helpAlt'))); + box.oninput = onInput; + setupLivePrefs($$('input', box).map(el => el.id)); }, }); - - function onInput() { - const name = this.id.split('.')[1]; - const shortcut = this.value.trim(); + async function onInput({target: el}) { + const name = el.id.split('.')[1]; + const shortcut = el.value.trim(); if (!shortcut) { browser.commands.reset(name).catch(ignoreChromeError); - this.setCustomValidity(''); + el.setCustomValidity(''); return; } try { - browser.commands.update({name, shortcut}).then( - () => this.setCustomValidity(''), - err => this.setCustomValidity(err) - ); + await browser.commands.update({name, shortcut}); + el.setCustomValidity(''); } catch (err) { - this.setCustomValidity(err); + el.setCustomValidity(err); } } }