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
This commit is contained in:
tophf 2020-10-13 21:14:54 +03:00 committed by GitHub
parent 60fc6f2456
commit 9e487b03e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 240 additions and 388 deletions

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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',

View File

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

View File

@ -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) {

View File

@ -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) {

View File

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

View File

@ -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');

View File

@ -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) {

View File

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

View File

@ -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)) {

View File

@ -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)) {

View File

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

View File

@ -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 */

View File

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

View File

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

View File

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

View File

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

View File

@ -298,7 +298,7 @@ function customizeHotkeys() {
}
window.onkeydown = event => {
if (event.keyCode === 27) {
if (event.key === 'Escape') {
top.dispatchEvent(new CustomEvent('closeOptions'));
}
};

View File

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

View File

@ -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%;

View File

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

View File

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

View File

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

View File

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

View File

@ -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();