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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
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);
}
}
}