From 9e487b03e514c053ead813971f519305348981ca Mon Sep 17 00:00:00 2001 From: tophf Date: Tue, 13 Oct 2020 21:14:54 +0300 Subject: [PATCH] tweak editor (#1063) * also apply live-preview if an unsaved style was disabled * use box-shadow instead of outline for focus everywhere * allow focus outline on click in text/search input or textarea * search inputs should use the same style as text inputs * also use box-shadow focus on delete buttons * remove URLSearchParams workaround, not needed since Chrome 55 * use `once` in addEventListener, available since Chrome 55 * update USO bug workarounds, remove obsolete ones * ping/pong to fix openURL with `message` in FF * use unprefixed CSS filter, available since Chrome 53 * use unprefixed CSS user-select, available since Chrome 54 * focus tweaks * also use text query in inline search for Stylus category * use event.key, available since Chrome 51 Co-authored-by: narcolepticinsomniac --- background/background.js | 21 +- background/token-manager.js | 18 +- content/install-hook-userstyles.js | 235 +++++------------- edit/codemirror-default.css | 9 +- edit/edit.css | 7 + edit/edit.js | 12 +- edit/linter-config-dialog.js | 5 +- edit/live-preview.js | 2 +- edit/sections-editor-section.js | 29 +-- global.css | 18 +- install-usercss/install-usercss.css | 4 - install-usercss/install-usercss.js | 3 +- js/dom.js | 31 ++- js/polyfill.js | 15 ++ js/router.js | 20 +- js/script-loader.js | 3 +- manage/incremental-search.js | 33 +-- manage/manage.css | 8 +- manage/manage.js | 2 - msgbox/msgbox.css | 7 +- msgbox/msgbox.js | 14 +- options/onoffswitch.css | 4 +- options/options.js | 2 +- popup/hotkeys.js | 42 ++-- popup/popup.css | 6 +- popup/popup.js | 17 +- popup/search-results.css | 2 - popup/search-results.js | 20 +- vendor-overwrites/colorpicker/colorpicker.css | 7 - vendor-overwrites/colorpicker/colorpicker.js | 32 +-- 30 files changed, 240 insertions(+), 388 deletions(-) diff --git a/background/background.js b/background/background.js index d03250b6..fa54e208 100644 --- a/background/background.js +++ b/background/background.js @@ -66,11 +66,13 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { /* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent when the tab is ready, which is needed in the popup, otherwise another extension could force the tab to open in foreground thus auto-closing the popup (in Chrome at least) and preventing the sendMessage code from running */ - openURL(opts) { - const {message} = opts; - return openURL(opts) // will pass the resolved value untouched when `message` is absent or falsy - .then(message && (tab => tab.status === 'complete' ? tab : onTabReady(tab))) - .then(message && (tab => msg.sendTab(tab.id, opts.message))); + async openURL(opts) { + const tab = await openURL(opts); + if (opts.message) { + await onTabReady(tab); + await msg.sendTab(tab.id, opts.message); + } + return tab; function onTabReady(tab) { return new Promise((resolve, reject) => setTimeout(function ping(numTries = 10, delay = 100) { @@ -297,13 +299,10 @@ function openEditor(params) { 'url-prefix'?: String } */ - const searchParams = new URLSearchParams(); - for (const key in params) { - searchParams.set(key, params[key]); - } - const search = searchParams.toString(); + const u = new URL(chrome.runtime.getURL('edit.html')); + u.search = new URLSearchParams(params); return openURL({ - url: 'edit.html' + (search && `?${search}`), + url: `${u}`, newWindow: prefs.get('openEditInWindow'), windowPosition: prefs.get('windowPosition'), currentWindow: null diff --git a/background/token-manager.js b/background/token-manager.js index 63f69652..755fd9fa 100644 --- a/background/token-manager.js +++ b/background/token-manager.js @@ -37,7 +37,7 @@ const tokenManager = (() => { scopes: ['https://www.googleapis.com/auth/drive.appdata'], revoke: token => { const params = {token}; - return postQuery(`https://accounts.google.com/o/oauth2/revoke?${stringifyQuery(params)}`); + return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`); } }, onedrive: { @@ -137,14 +137,6 @@ const tokenManager = (() => { }); } - function stringifyQuery(obj) { - const search = new URLSearchParams(); - for (const key of Object.keys(obj)) { - search.set(key, obj[key]); - } - return search.toString(); - } - function authUser(name, k, interactive = false) { const provider = AUTH[name]; const state = Math.random().toFixed(8).slice(2); @@ -160,7 +152,7 @@ const tokenManager = (() => { if (provider.authQuery) { Object.assign(query, provider.authQuery); } - const url = `${provider.authURL}?${stringifyQuery(query)}`; + const url = `${provider.authURL}?${new URLSearchParams(query)}`; return webextLaunchWebAuthFlow({ url, interactive, @@ -211,11 +203,9 @@ const tokenManager = (() => { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' - } + }, + body: body ? new URLSearchParams(body) : null, }; - if (body) { - options.body = stringifyQuery(body); - } return fetch(url, options) .then(r => { if (r.ok) { diff --git a/content/install-hook-userstyles.js b/content/install-hook-userstyles.js index c0fd8f70..1adc4e89 100644 --- a/content/install-hook-userstyles.js +++ b/content/install-hook-userstyles.js @@ -1,7 +1,11 @@ /* global cloneInto msg API */ 'use strict'; -(() => { +// eslint-disable-next-line no-unused-expressions +/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => { + const styleId = RegExp.$1; + const pageEventId = `${performance.now()}${Math.random()}`; + window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install')); window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true); @@ -17,35 +21,18 @@ }, '*'); }); - let gotBody = false; let currentMd5; - new MutationObserver(observeDOM).observe(document.documentElement, { - childList: true, - subtree: true, - }); - observeDOM(); + const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`; + Promise.all([ + API.findStyle({md5Url}), + getResource(md5Url), + onDOMready(), + ]).then(checkUpdatability); - function observeDOM() { - if (!gotBody) { - if (!document.body) return; - gotBody = true; - // TODO: remove the following statement when USO pagination title is fixed - document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: '); - const md5Url = getMeta('stylish-md5-url') || location.href; - Promise.all([ - API.findStyle({md5Url}), - getResource(md5Url) - ]) - .then(checkUpdatability); - } - if (document.getElementById('install_button')) { - onDOMready().then(() => { - requestAnimationFrame(() => { - sendEvent(sendEvent.lastEvent); - }); - }); - } - } + document.documentElement.appendChild( + Object.assign(document.createElement('script'), { + textContent: `(${inPageContext})('${pageEventId}')`, + })); function onMessage(msg) { switch (msg.method) { @@ -72,7 +59,7 @@ function checkUpdatability([installedStyle, md5]) { // TODO: remove the following statement when USO is fixed - document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', { + document.dispatchEvent(new CustomEvent(pageEventId, { detail: installedStyle && installedStyle.updateUrl, })); currentMd5 = md5; @@ -141,7 +128,6 @@ }); } - function onClick(event) { if (onClick.processing || !orphanCheck()) { return; @@ -227,13 +213,11 @@ } } - function getMeta(name) { const e = document.querySelector(`link[rel="${name}"]`); return e ? e.getAttribute('href') : null; } - function getResource(url, options) { if (url.startsWith('#')) { return Promise.resolve(document.getElementById(url.slice(1)).textContent); @@ -280,7 +264,6 @@ .catch(() => null); } - function styleSectionsEqual({sections: a}, {sections: b}) { if (!a || !b) { return undefined; @@ -318,20 +301,12 @@ } } - function onDOMready() { - if (document.readyState !== 'loading') { - return Promise.resolve(); - } - return new Promise(resolve => { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - resolve(); - }); - }); + return document.readyState !== 'loading' + ? Promise.resolve() + : new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true})); } - function openSettings(countdown = 10e3) { const button = document.querySelector('.customize_button'); if (button) { @@ -349,12 +324,12 @@ } } - function orphanCheck() { - // TODO: switch to install-hook-usercss.js impl, and remove explicit orphanCheck() calls - if (chrome.i18n && chrome.i18n.getUILanguage()) { - return true; - } + try { + if (chrome.i18n.getUILanguage()) { + return true; + } + } catch (e) {} // In Chrome content script is orphaned on an extension update/reload // so we need to detach event listeners window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true); @@ -366,132 +341,56 @@ } })(); -// run in page context -document.documentElement.appendChild(document.createElement('script')).text = '(' + ( - () => { - document.currentScript.remove(); - - // spoof Stylish extension presence in Chrome - if (window.chrome && chrome.app) { - const realImage = window.Image; - window.Image = function Image(...args) { - return new Proxy(new realImage(...args), { - get(obj, key) { - return obj[key]; - }, - set(obj, key, value) { - if (key === 'src' && /^chrome-extension:/i.test(value)) { - setTimeout(() => typeof obj.onload === 'function' && obj.onload()); - } else { - obj[key] = value; - } - return true; - }, - }); - }; - } - - // USO bug workaround: use the actual style settings in API response - let settings; - const originalResponseJson = Response.prototype.json; - document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) { - document.removeEventListener('stylusFixBuggyUSOsettings', _); - // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) - settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search.replace(/^\?/, '')); - if (!settings) { - Response.prototype.json = originalResponseJson; +function inPageContext(eventId) { + document.currentScript.remove(); + const origMethods = { + json: Response.prototype.json, + byId: document.getElementById, + }; + let vars; + // USO bug workaround: prevent errors in console after install and busy cursor + document.getElementById = id => + origMethods.byId.call(document, id) || + (/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id) + ? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'}) + : null); + // USO bug workaround: use the actual image data in customized settings + document.addEventListener(eventId, ({detail}) => { + vars = /\?/.test(detail) && new URL(detail).searchParams; + if (!vars) Response.prototype.json = origMethods.json; + }, {once: true}); + Response.prototype.json = async function () { + const json = await origMethods.json.apply(this, arguments); + if (vars && json && Array.isArray(json.style_settings)) { + Response.prototype.json = origMethods.json; + const images = new Map(); + for (const ss of json.style_settings) { + const value = vars.get('ik-' + ss.install_key); + if (value && ss.setting_type === 'image' && ss.style_setting_options) { + let isListed; + for (const opt of ss.style_setting_options) { + isListed |= opt.default = (opt.value === value); + } + images.set(ss.install_key, {url: value, isListed}); + } } - }); - Response.prototype.json = function (...args) { - return originalResponseJson.call(this, ...args).then(json => { - if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') { - return json; - } - Response.prototype.json = originalResponseJson; - const images = new Map(); - for (const jsonSetting of json.style_settings) { - let value = settings.get('ik-' + jsonSetting.install_key); - if (!value - || !jsonSetting.style_setting_options - || !jsonSetting.style_setting_options[0]) { - continue; - } - if (value.startsWith('ik-')) { - value = value.replace(/^ik-/, ''); - const defaultItem = jsonSetting.style_setting_options.find(item => item.default); - if (!defaultItem || defaultItem.install_key !== value) { - if (defaultItem) { - defaultItem.default = false; - } - jsonSetting.style_setting_options.some(item => { - if (item.install_key === value) { - item.default = true; - return true; - } - }); - } - } else if (jsonSetting.setting_type === 'image') { - jsonSetting.style_setting_options.some(item => { - if (item.default) { - item.default = false; - return true; - } - }); - images.set(jsonSetting.install_key, value); - } else { - const item = jsonSetting.style_setting_options[0]; - if (item.value !== value && item.install_key === 'placeholder') { - item.value = value; - } - } - } - if (images.size) { - new MutationObserver((_, observer) => { - if (!document.getElementById('style-settings')) { - return; - } + if (images.size) { + new MutationObserver((_, observer) => { + if (document.getElementById('style-settings')) { observer.disconnect(); - for (const [name, url] of images.entries()) { + for (const [name, {url, isListed}] of images) { const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`); - const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url')); + const elUrl = elRadio && + document.getElementById(elRadio.id.replace('url-choice', 'user-url')); if (elUrl) { + elRadio.checked = !isListed; elUrl.value = url; } } - }).observe(document, {childList: true, subtree: true}); - } - return json; - }); - }; - } -) + `)('${chrome.runtime.getURL('').slice(0, -1)}')`; - -// TODO: remove the following statement when USO pagination is fixed -if (location.search.includes('category=')) { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - new MutationObserver((_, observer) => { - if (!document.getElementById('pagination')) { - return; + } + }).observe(document, {childList: true, subtree: true}); } - observer.disconnect(); - const category = '&' + location.search.match(/category=[^&]+/)[0]; - const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])'); - for (let i = 0; i < links.length; i++) { - links[i].href += category; - } - }).observe(document, {childList: true, subtree: true}); - }); -} - -if (/^https?:\/\/userstyles\.org\/styles\/\d{3,}/.test(location.href)) { - new MutationObserver((_, observer) => { - const cssButton = document.getElementsByClassName('css_button'); - if (cssButton.length) { - // Click on the "Show CSS Code" button to workaround the JS error - cssButton[0].click(); - cssButton[0].click(); - observer.disconnect(); } - }).observe(document, {childList: true, subtree: true}); + return json; + }; } diff --git a/edit/codemirror-default.css b/edit/codemirror-default.css index 0db190f4..4ae58f6c 100644 --- a/edit/codemirror-default.css +++ b/edit/codemirror-default.css @@ -7,6 +7,12 @@ } .CodeMirror { border: solid #CCC 1px; + transition: box-shadow .1s; +} +#stylus#stylus .CodeMirror { + /* Using a specificity hack to override userstyles */ + /* Not using the ring-color hack as it became ugly in new Chrome */ + outline: none !important; } .CodeMirror-lint-mark-warning { background: none; @@ -14,9 +20,6 @@ .CodeMirror-dialog { -webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94); } -.CodeMirror-focused { - outline: #7dadd9 auto 1px; /* not using the ring-color hack as it became ugly in new Chrome */ -} .CodeMirror-bookmark { background: linear-gradient(to right, currentColor, transparent); position: absolute; diff --git a/edit/edit.css b/edit/edit.css index 8db215ba..deb7c0ba 100644 --- a/edit/edit.css +++ b/edit/edit.css @@ -611,6 +611,9 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high right: 4px; top: .5em; } +#help-popup input[type="search"] { + margin: 3px; +} .keymap-list { font-size: 12px; @@ -788,6 +791,10 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high justify-items: normal; } +.usercss .CodeMirror-focused { + box-shadow: none; +} + html:not(.usercss) .usercss-only, .usercss #mozilla-format-container, .usercss #sections > h2 { diff --git a/edit/edit.js b/edit/edit.js index b902a1e3..c310c881 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -352,8 +352,7 @@ function isUsercss(style) { } function initStyleData() { - // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) - const params = new URLSearchParams(location.search.replace(/^\?/, '')); + const params = new URLSearchParams(location.search); const id = Number(params.get('id')); const createEmptyStyle = () => ({ name: params.get('domain') || @@ -409,7 +408,7 @@ function showHelp(title = '', body) { !event || event.type === 'click' || ( - event.which === 27 && + event.key === 'Escape' && !event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey && !$('.CodeMirror-hints, #message-box') && ( @@ -470,7 +469,7 @@ function showCodeMirrorPopup(title, html, options) { popup.style.pointerEvents = 'auto'; const onKeyDown = event => { - if (event.which === 9 && !event.ctrlKey && !event.altKey && !event.metaKey) { + if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) { const search = $('#search-replace-dialog'); const area = search && search.contains(document.activeElement) ? search : popup; moveFocus(area, event.shiftKey ? -1 : 1); @@ -479,13 +478,12 @@ function showCodeMirrorPopup(title, html, options) { }; window.addEventListener('keydown', onKeyDown, true); - window.addEventListener('closeHelp', function _() { - window.removeEventListener('closeHelp', _); + window.addEventListener('closeHelp', () => { window.removeEventListener('keydown', onKeyDown, true); document.documentElement.style.removeProperty('pointer-events'); rerouteHotkeys(true); cm = popup.codebox = null; - }); + }, {once: true}); return popup; } diff --git a/edit/linter-config-dialog.js b/edit/linter-config-dialog.js index 2bdf941b..c570963a 100644 --- a/edit/linter-config-dialog.js +++ b/edit/linter-config-dialog.js @@ -53,11 +53,10 @@ cm.on('changes', updateButtonState); rerouteHotkeys(false); - window.addEventListener('closeHelp', function _() { - window.removeEventListener('closeHelp', _); + window.addEventListener('closeHelp', () => { rerouteHotkeys(true); cm = null; - }); + }, {once: true}); loadScript([ '/vendor/codemirror/mode/javascript/javascript.js', diff --git a/edit/live-preview.js b/edit/live-preview.js index c2ff3ca4..4372bbd0 100644 --- a/edit/live-preview.js +++ b/edit/live-preview.js @@ -10,7 +10,7 @@ function createLivePreview(preprocess) { const errorContainer = $('#preview-errors'); prefs.subscribe(['editor.livePreview'], (key, value) => { - if (value && data && data.id && data.enabled) { + if (value && data && data.id && (data.enabled || editor.dirty.has('enabled'))) { previewer = createPreviewer(); previewer.update(data); } diff --git a/edit/sections-editor-section.js b/edit/sections-editor-section.js index 2735ac2c..5b07875e 100644 --- a/edit/sections-editor-section.js +++ b/edit/sections-editor-section.js @@ -206,36 +206,32 @@ function createSection({ } function handleKeydown(cm, event) { - const key = event.which; - if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) { + if (event.shiftKey || event.altKey || event.metaKey) { return; } + const {key} = event; const {line, ch} = cm.getCursor(); switch (key) { - case 37: - // arrow Left + case 'ArrowLeft': if (line || ch) { return; } - // fallthrough to arrow Up - case 38: - // arrow Up + // fallthrough + case 'ArrowUp': cm = line === 0 && prevEditor(cm, false); if (!cm) { return; } event.preventDefault(); event.stopPropagation(); - cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch); + cm.setCursor(cm.doc.size - 1, key === 'ArrowLeft' ? 1e20 : ch); break; - case 39: - // arrow Right + case 'ArrowRight': if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) { return; } - // fallthrough to arrow Down - case 40: - // arrow Down + // fallthrough + case 'ArrowDown': cm = line === cm.doc.size - 1 && nextEditor(cm, false); if (!cm) { return; @@ -245,13 +241,6 @@ function createSection({ cm.setCursor(0, 0); break; } - // FIXME: what is this? - // const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0]; - // if (animation) { - // animation.playbackRate = -1; - // animation.currentTime = 2000; - // animation.play(); - // } } function showAppliesToHelp(event) { diff --git a/global.css b/global.css index c010a958..fa5ff877 100644 --- a/global.css +++ b/global.css @@ -54,18 +54,20 @@ button:active { border-color: hsl(0, 0%, 50%); } -input { +input { font: inherit; + border: 1px solid hsl(0, 0%, 66%); + transition: border-color .1s, box-shadow .1s; } -input:not([type]) { +input:not([type]), +input[type=search] { background: #fff; color: #000; height: 22px; min-height: 22px!important; line-height: 22px; padding: 0 3px; - font: inherit; border: 1px solid hsl(0, 0%, 66%); } @@ -208,9 +210,19 @@ select[disabled] + .select-arrow { display: none !important; } +:focus, +.CodeMirror-focused, +[data-focused-via-click] input[type="text"]:focus, +[data-focused-via-click] input[type="number"]:focus { + /* Using box-shadow instead of the ugly outline in new Chrome */ + outline: none; + box-shadow: 0 0 0 1px hsl(180, 100%, 38%), 0 0 3px hsla(180, 100%, 60%, .5); +} + [data-focused-via-click] :focus, [data-focused-via-click]:focus { outline: none; + box-shadow: none; } @supports (-moz-appearance: none) { diff --git a/install-usercss/install-usercss.css b/install-usercss/install-usercss.css index 88d47bd4..6214629e 100644 --- a/install-usercss/install-usercss.css +++ b/install-usercss/install-usercss.css @@ -288,9 +288,7 @@ li { #header:not(.meta-init) > *:not(.lds-spinner), #header.meta-init > .lds-spinner { - -webkit-user-select: none; -moz-user-select: none; - -ms-user-select: none; user-select: none; pointer-events: none; opacity: 0; @@ -299,9 +297,7 @@ li { #header.meta-init > * { opacity: 1; transition: opacity .5s; - -webkit-user-select: auto; -moz-user-select: auto; - -ms-user-select: auto; user-select: auto; } diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index f78044a7..41ee0ab1 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -3,8 +3,7 @@ 'use strict'; (() => { - // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) - const params = new URLSearchParams(location.search.replace(/^\?/, '')); + const params = new URLSearchParams(location.search); const tabId = params.has('tabId') ? Number(params.get('tabId')) : -1; const initialUrl = params.get('updateUrl'); diff --git a/js/dom.js b/js/dom.js index 4e40bc83..b95627d5 100644 --- a/js/dom.js +++ b/js/dom.js @@ -20,6 +20,9 @@ for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) } } +$.isTextLikeInput = el => + el.localName === 'input' && /^(text|search|number)$/.test(el.type); + $.remove = (selector, base = document) => { const el = selector && typeof selector === 'string' ? $(selector, base) : selector; if (el) { @@ -112,15 +115,9 @@ document.addEventListener('wheel', event => { }); function onDOMready() { - if (document.readyState !== 'loading') { - return Promise.resolve(); - } - return new Promise(resolve => { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - resolve(); - }); - }); + return document.readyState !== 'loading' + ? Promise.resolve() + : new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true})); } @@ -144,8 +141,7 @@ function animateElement( onComplete, } = {}) { return element && new Promise(resolve => { - element.addEventListener('animationend', function _() { - element.removeEventListener('animationend', _); + element.addEventListener('animationend', () => { element.classList.remove( className, // In Firefox, `resolve()` might be called one frame later. @@ -157,7 +153,7 @@ function animateElement( onComplete.call(element); } resolve(); - }); + }, {once: true}); element.classList.add(className); }); } @@ -355,20 +351,23 @@ function focusAccessibility() { 'a', 'button', 'input', - 'textarea', 'label', 'select', 'summary', ]; // try to find a focusable parent for this many parentElement jumps: const GIVE_UP_DEPTH = 4; + // allow outline on text/search inputs in addition to textareas + const isOutlineAllowed = el => + !focusAccessibility.ELEMENTS.includes(el.localName) || + $.isTextLikeInput(el); addEventListener('mousedown', suppressOutlineOnClick, {passive: true}); addEventListener('keydown', keepOutlineOnTab, {passive: true}); function suppressOutlineOnClick({target}) { for (let el = target, i = 0; el && i++ < GIVE_UP_DEPTH; el = el.parentElement) { - if (focusAccessibility.ELEMENTS.includes(el.localName)) { + if (!isOutlineAllowed(el)) { focusAccessibility.lastFocusedViaClick = true; if (el.dataset.focusedViaClick === undefined) { el.dataset.focusedViaClick = ''; @@ -379,7 +378,7 @@ function focusAccessibility() { } function keepOutlineOnTab(event) { - if (event.which === 9) { + if (event.key === 'Tab') { focusAccessibility.lastFocusedViaClick = false; setTimeout(keepOutlineOnTab, 0, true); return; @@ -387,7 +386,7 @@ function focusAccessibility() { return; } let el = document.activeElement; - if (!el || !focusAccessibility.ELEMENTS.includes(el.localName)) { + if (!el || isOutlineAllowed(el)) { return; } if (el.dataset.focusedViaClick !== undefined) { diff --git a/js/polyfill.js b/js/polyfill.js index 859de6e2..e299957f 100644 --- a/js/polyfill.js +++ b/js/polyfill.js @@ -62,5 +62,20 @@ self.INJECTED !== 1 && (() => { } } + if (!(new URLSearchParams({foo: 1})).get('foo')) { + // TODO: remove when minimum_chrome_version >= 61 + window.URLSearchParams = class extends URLSearchParams { + constructor(init) { + if (init && typeof init === 'object') { + super(); + for (const [key, val] of Object.entries(init)) { + this.set(key, val); + } + } else { + super(...arguments); + } + } + }; + } //#endregion })(); diff --git a/js/router.js b/js/router.js index 59566b59..f466eb30 100644 --- a/js/router.js +++ b/js/router.js @@ -31,18 +31,9 @@ const router = (() => { } function updateSearch(key, value) { - const search = new URLSearchParams(location.search.replace(/^\?/, '')); - if (!value) { - search.delete(key); - } else { - search.set(key, value); - } - const finalSearch = search.toString(); - if (finalSearch) { - history.replaceState(history.state, null, `?${finalSearch}${location.hash}`); - } else { - history.replaceState(history.state, null, `${location.pathname}${location.hash}`); - } + const u = new URL(location); + u.searchParams[value ? 'set' : 'delete'](key, value); + history.replaceState(history.state, null, `${u}`); update(true); } @@ -66,7 +57,7 @@ const router = (() => { } function getSearch(key) { - return new URLSearchParams(location.search.replace(/^\?/, '')).get(key); + return new URLSearchParams(location.search).get(key); } function update(replace) { @@ -86,8 +77,7 @@ const router = (() => { if (options.hash) { state = options.hash === location.hash; } else if (options.search) { - // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) - const search = new URLSearchParams(location.search.replace(/^\?/, '')); + const search = new URLSearchParams(location.search); state = options.search.map(key => search.get(key)); } if (!deepEqual(state, options.currentState)) { diff --git a/js/script-loader.js b/js/script-loader.js index 0279cae5..ada07824 100644 --- a/js/script-loader.js +++ b/js/script-loader.js @@ -82,7 +82,7 @@ const loadScript = (() => { for (const {addedNodes} of mutations) { for (const n of addedNodes) { if (n.src && getSubscribersForSrc(n.src)) { - n.addEventListener('load', notifySubscribers); + n.addEventListener('load', notifySubscribers, {once: true}); } } } @@ -97,7 +97,6 @@ const loadScript = (() => { } function notifySubscribers(event) { - this.removeEventListener('load', notifySubscribers); for (let data; (data = getSubscribersForSrc(this.src));) { data.listeners.forEach(fn => fn(event)); if (emptyAfterCleanup(data.suffix)) { diff --git a/manage/incremental-search.js b/manage/incremental-search.js index 293371ae..393b3a4d 100644 --- a/manage/incremental-search.js +++ b/manage/incremental-search.js @@ -24,12 +24,12 @@ onDOMready().then(() => { document.body.appendChild(input); window.addEventListener('keydown', maybeRefocus, true); - function incrementalSearch({which}, immediately) { + function incrementalSearch({key}, immediately) { if (!immediately) { debounce(incrementalSearch, 100, {}, true); return; } - const direction = which === 38 ? -1 : which === 40 ? 1 : 0; + const direction = key === 'ArrowUp' ? -1 : key === 'ArrowDown' ? 1 : 0; const text = input.value.toLocaleLowerCase(); if (!text.trim() || !direction && (text === prevText || focusedName.startsWith(text))) { prevText = text; @@ -76,40 +76,31 @@ onDOMready().then(() => { if (event.altKey || event.metaKey || $('#message-box')) { return; } - const inTextInput = event.target.matches('[type=text], [type=search], [type=number]'); - const {which: k, key} = event; - // focus search field on "/" or Ctrl-F key - if (event.ctrlKey - ? (event.code === 'KeyF' || !event.code && k === 70) && !event.shiftKey - : (key === '/' || !key && k === 191 && !event.shiftKey) && !inTextInput) { + const inTextInput = $.isTextLikeInput(event.target); + const {key, code, ctrlKey: ctrl} = event; + // `code` is independent of the current keyboard language + if ((code === 'KeyF' && ctrl && !event.shiftKey) || + (code === 'Slash' || key === '/') && !ctrl && !inTextInput) { + // focus search field on "/" or Ctrl-F key event.preventDefault(); $('#search').focus(); return; } - if (event.ctrlKey || inTextInput) { + if (ctrl || inTextInput) { return; } const time = performance.now(); - if ( - // 0-9 - k >= 48 && k <= 57 || - // a-z - k >= 65 && k <= 90 || - // numpad keys - k >= 96 && k <= 111 || - // marks - k >= 186 - ) { + if (key.length === 1) { input.focus(); if (time - prevTime > 1000) { input.value = ''; } prevTime = time; } else - if (k === 13 && focusedLink) { + if (key === 'Enter' && focusedLink) { focusedLink.dispatchEvent(new MouseEvent('click', {bubbles: true})); } else - if ((k === 38 || k === 40) && !event.shiftKey && + if ((key === 'ArrowUp' || key === 'ArrowDown') && !event.shiftKey && time - prevTime < 5000 && incrementalSearch(event, true)) { prevTime = time; } else diff --git a/manage/manage.css b/manage/manage.css index ef4d0279..5e3f6041 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -550,8 +550,6 @@ a:hover { .newUI .update-done .updated svg { top: -4px; position: relative; - /* unprefixed since Chrome 53 */ - -webkit-filter: drop-shadow(0 4px 0 currentColor); filter: drop-shadow(0 5px 0 currentColor); } @@ -663,8 +661,6 @@ a:hover { margin-left: -20px; margin-right: 4px; transition: opacity .5s, filter .5s; - /* unprefixed since Chrome 53 */ - -webkit-filter: grayscale(1); filter: grayscale(1); /* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */ backface-visibility: hidden; @@ -682,9 +678,7 @@ a:hover { .newUI .entry:hover .target img { opacity: 1; - /* unprefixed since Chrome 53 */ - -webkit-filter: grayscale(0); - filter: grayscale(0); + filter: none; } /* Default, no update buttons */ diff --git a/manage/manage.js b/manage/manage.js index 0df3ea8d..2b856600 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -631,13 +631,11 @@ function switchUI({styleOnly} = {}) { } ` + (newUI.faviconsGray ? ` .newUI .target img { - -webkit-filter: grayscale(1); filter: grayscale(1); opacity: .25; } ` : ` .newUI .target img { - -webkit-filter: none; filter: none; opacity: 1; } diff --git a/msgbox/msgbox.css b/msgbox/msgbox.css index 32e6c865..bbc70622 100644 --- a/msgbox/msgbox.css +++ b/msgbox/msgbox.css @@ -135,12 +135,7 @@ } .danger #message-box-buttons > button:not([data-focused-via-click]):first-child:focus { - outline: red auto 1px; -} - -/* FF ignores color with 'auto' */ -.firefox .danger #message-box-buttons > button:not([data-focused-via-click]):first-child:focus { - outline: red solid 1px; + box-shadow: 0 0 0 1px red; /* Using box-shadow instead of the ugly outline in new Chrome */ } .non-windows #message-box-buttons { diff --git a/msgbox/msgbox.js b/msgbox/msgbox.js index 3cf7a5bf..a537da32 100644 --- a/msgbox/msgbox.js +++ b/msgbox/msgbox.js @@ -62,28 +62,28 @@ function messageBox({ resolveWith({button: this.buttonIndex}); }, key(event) { - const {which, shiftKey, ctrlKey, altKey, metaKey, target} = event; - if (shiftKey && which !== 9 || ctrlKey || altKey || metaKey) { + const {key, shiftKey, ctrlKey, altKey, metaKey, target} = event; + if (shiftKey && key !== 'Tab' || ctrlKey || altKey || metaKey) { return; } - switch (which) { - case 13: + switch (key) { + case 'Enter': if (target.closest(focusAccessibility.ELEMENTS.join(','))) { return; } break; - case 27: + case 'Escape': event.preventDefault(); event.stopPropagation(); break; - case 9: + case 'Tab': moveFocus(messageBox.element, shiftKey ? -1 : 1); event.preventDefault(); return; default: return; } - resolveWith(which === 13 ? {enter: true} : {esc: true}); + resolveWith(key === 'Enter' ? {enter: true} : {esc: true}); }, scroll() { scrollTo(blockScroll.x, blockScroll.y); diff --git a/options/onoffswitch.css b/options/onoffswitch.css index c335c97e..a2fecfbe 100644 --- a/options/onoffswitch.css +++ b/options/onoffswitch.css @@ -3,9 +3,8 @@ .onoffswitch { position: relative; margin: 1ex 0; - -webkit-user-select: none; -moz-user-select: none; - -ms-user-select: none; + user-select: none; } .onoffswitch input { @@ -17,6 +16,7 @@ bottom: -10px; left: -10px; width: calc(100% + 12px); + border: 0; } #message-box .onoffswitch input { diff --git a/options/options.js b/options/options.js index dff74940..d1bbcb36 100644 --- a/options/options.js +++ b/options/options.js @@ -298,7 +298,7 @@ function customizeHotkeys() { } window.onkeydown = event => { - if (event.keyCode === 27) { + if (event.key === 'Escape') { top.dispatchEvent(new CustomEvent('closeOptions')); } }; diff --git a/popup/hotkeys.js b/popup/hotkeys.js index 157a34bb..492d36b2 100644 --- a/popup/hotkeys.js +++ b/popup/hotkeys.js @@ -9,14 +9,13 @@ const hotkeys = (() => { let enabled = false; let ready = false; - window.addEventListener('showStyles:done', function _() { - window.removeEventListener('showStyles:done', _); + window.addEventListener('showStyles:done', () => { togglablesShown = true; togglables = getTogglables(); ready = true; setState(true); initHotkeyInfo(); - }); + }, {once: true}); window.addEventListener('resize', adjustInfoPosition); @@ -38,40 +37,27 @@ const hotkeys = (() => { return; } let entry; - const {which: k, key, code} = event; + let {key, code, shiftKey} = event; - if (code.startsWith('Digit') || code.startsWith('Numpad') && code.length === 7) { + if (key >= '0' && key <= '9') { + entry = entries[(Number(key) || 10) - 1]; + } else if (code >= 'Digit0' && code <= 'Digit9') { entry = entries[(Number(code.slice(-1)) || 10) - 1]; - - } else if ( - code === 'Backquote' || code === 'NumpadMultiply' || - key && (key === '`' || key === '*') || - k === 192 || k === 106) { + } else if (key === '`' || key === '*' || code === 'Backquote' || code === 'NumpadMultiply') { invertTogglables(); - - } else if ( - code === 'NumpadSubtract' || - key && key === '-' || - k === 109) { + } else if (key === '-' || code === 'NumpadSubtract') { toggleState(entries, 'enabled', false); - - } else if ( - code === 'NumpadAdd' || - key && key === '+' || - k === 107) { + } else if (key === '+' || code === 'NumpadAdd') { toggleState(entries, 'disabled', true); - - } else if ( - // any single character - key && key.length === 1 || - k >= 65 && k <= 90) { - const letter = new RegExp(key ? '^' + key : '^\\x' + k.toString(16), 'i'); - entry = [...entries].find(entry => letter.test(entry.textContent)); + } else if (key.length === 1) { + shiftKey = false; // typing ':' etc. needs Shift so we hide it here to avoid opening editor + key = key.toLocaleLowerCase(); + entry = [...entries].find(e => e.innerText.toLocaleLowerCase().startsWith(key)); } if (!entry) { return; } - const target = $(event.shiftKey ? '.style-edit-link' : '.checker', entry); + const target = $(shiftKey ? '.style-edit-link' : '.checker', entry); target.dispatchEvent(new MouseEvent('click', {cancelable: true})); } diff --git a/popup/popup.css b/popup/popup.css index 5706b877..531d6f77 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -342,11 +342,7 @@ a.configure[target="_blank"] .svg-icon.config { text-overflow: ellipsis; } #confirm button[data-cmd="ok"]:not([data-focused-via-click]):focus { - outline: red auto 1px; -} -/* FF ignores color with 'auto' */ -.firefox #confirm button[data-cmd="ok"]:not([data-focused-via-click]):focus { - outline: red solid 1px; + box-shadow: 0 0 0 1px red; /* Using box-shadow instead of the ugly outline in new Chrome */ } .menu-items-wrapper { width: 80%; diff --git a/popup/popup.js b/popup/popup.js index fb2b10bf..1510ce76 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -507,16 +507,15 @@ Object.assign(handleEvent, { window.onkeydown = event => { const close = $('.menu-close', entry); const checkbox = $('.exclude-by-domain-checkbox', entry); - const keyCode = event.keyCode || event.which; - if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) { + if (document.activeElement === close && (event.key === 'Tab') && !event.shiftKey) { event.preventDefault(); checkbox.focus(); } - if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) { + if (document.activeElement === checkbox && (event.key === 'Tab') && event.shiftKey) { event.preventDefault(); close.focus(); } - if (keyCode === 27) { + if (event.key === 'Escape') { event.preventDefault(); close.click(); } @@ -542,20 +541,20 @@ Object.assign(handleEvent, { const close = $('.menu-close', entry); const checkbox = $('.exclude-by-domain-checkbox', entry); const confirmActive = $('#confirm[data-display="true"]'); - const keyCode = event.keyCode || event.which; - if (document.activeElement === cancel && (keyCode === 9)) { + const {key} = event; + if (document.activeElement === cancel && (key === 'Tab')) { event.preventDefault(); affirm.focus(); } - if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) { + if (document.activeElement === close && (key === 'Tab') && !event.shiftKey) { event.preventDefault(); checkbox.focus(); } - if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) { + if (document.activeElement === checkbox && (key === 'Tab') && event.shiftKey) { event.preventDefault(); close.focus(); } - if (keyCode === 27) { + if (key === 'Escape') { event.preventDefault(); if (confirmActive) { box.dataset.display = false; diff --git a/popup/search-results.css b/popup/search-results.css index a7eb69cf..ba0bf97c 100755 --- a/popup/search-results.css +++ b/popup/search-results.css @@ -271,9 +271,7 @@ body.search-results-shown { /* spinner: https://github.com/loadingio/css-spinner */ .lds-spinner { - -webkit-user-select: none; -moz-user-select: none; - -ms-user-select: none; user-select: none; pointer-events: none; position: absolute; diff --git a/popup/search-results.js b/popup/search-results.js index 590a2337..f596553e 100755 --- a/popup/search-results.js +++ b/popup/search-results.js @@ -54,7 +54,7 @@ window.addEventListener('showStyles:done', () => { href: URLS.usoArchive, onclick(event) { if (!prefs.get('popup.findStylesInline') || dom.container) { - this.search = `${new URLSearchParams({category, search: $('#search-query').value})}`; + this.search = new URLSearchParams({category, search: $('#search-query').value}); handleEvent.openURLandHide.call(this, event); return; } @@ -83,6 +83,9 @@ window.addEventListener('showStyles:done', () => { const n = Number(m[2]); query.push(n >= 2000 && n <= thisYear ? n : m[1] || m[2]); } + if (category === STYLUS_CATEGORY && !query.includes('stylus')) { + query.push('stylus'); + } ready = ready.then(start); }; $('#search-order').value = order; @@ -464,13 +467,18 @@ window.addEventListener('showStyles:done', () => { } function isResultMatching(res) { + // We're trying to call calcHaystack only when needed, not on all 100K items + const {c} = res; return ( - res.c === category || - searchGlobals && res.c === 'global' && (query.length || calcHaystack(res)._nLC.includes(category)) + c === category || + category !== STYLUS_CATEGORY && ( + searchGlobals && + c === 'global' && + (query.length || calcHaystack(res)._nLC.includes(category)) + ) ) && ( - category === STYLUS_CATEGORY - ? /\bStylus\b/.test(res.n) - : !query.length || query.every(isInHaystack, calcHaystack(res)) + !query.length || // to skip calling calcHaystack + query.every(isInHaystack, calcHaystack(res)) ); } diff --git a/vendor-overwrites/colorpicker/colorpicker.css b/vendor-overwrites/colorpicker/colorpicker.css index 45bdb9f0..fa6d5369 100644 --- a/vendor-overwrites/colorpicker/colorpicker.css +++ b/vendor-overwrites/colorpicker/colorpicker.css @@ -86,10 +86,7 @@ border: 1px solid var(--main-border-color); background-color: var(--main-background-color); box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.12); - -webkit-user-select: none; -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; user-select: none; } @@ -295,10 +292,7 @@ font-size: 11px; font-weight: bold; box-sizing: border-box; - -webkit-user-select: text; -moz-user-select: text; - -ms-user-select: text; - -o-user-select: text; user-select: text; border: 1px solid var(--input-border-color); background-color: var(--input-background-color); @@ -306,7 +300,6 @@ } .colorpicker-theme-dark .colorpicker-input::-webkit-inner-spin-button { - -webkit-filter: invert(1); filter: invert(1); } diff --git a/vendor-overwrites/colorpicker/colorpicker.js b/vendor-overwrites/colorpicker/colorpicker.js index 2fb84913..c3381495 100644 --- a/vendor-overwrites/colorpicker/colorpicker.js +++ b/vendor-overwrites/colorpicker/colorpicker.js @@ -355,29 +355,29 @@ } function setFromKeyboard(event) { - const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event; - switch (which) { - case 9: // Tab - case 33: // PgUp - case 34: // PgDn + const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event; + switch (key) { + case 'Tab': + case 'PageUp': + case 'PageDown': if (!ctrl && !alt && !meta) { const el = document.activeElement; const inputs = $inputs[currentFormat]; const lastInput = inputs[inputs.length - 1]; - if (which === 9 && shift && el === inputs[0]) { + if (key === 'Tab' && shift && el === inputs[0]) { maybeFocus(lastInput); - } else if (which === 9 && !shift && el === lastInput) { + } else if (key === 'Tab' && !shift && el === lastInput) { maybeFocus(inputs[0]); - } else if (which !== 9 && !shift) { - setFromFormatElement({shift: which === 33 || shift}); + } else if (key !== 'Tab' && !shift) { + setFromFormatElement({shift: key === 'PageUp' || shift}); } else { return; } event.preventDefault(); } return; - case 38: // Up - case 40: // Down + case 'ArrowUp': + case 'ArrowDown': if (!event.metaKey && document.activeElement.localName === 'input' && document.activeElement.checkValidity()) { @@ -389,8 +389,8 @@ function setFromKeyboardIncrement(event) { const el = document.activeElement; - const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event; - const dir = which === 38 ? 1 : -1; + const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event; + const dir = key === 'ArrowUp' ? 1 : -1; let value, newValue; if (currentFormat === 'hex') { value = el.value.trim(); @@ -617,9 +617,9 @@ function onKeyDown(e) { if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { - switch (e.which) { - case 13: - case 27: + switch (e.key) { + case 'Enter': + case 'Escape': e.preventDefault(); e.stopPropagation(); hide();