0162f39163
feature: retry sub.domain.tld as domain.tld if no styles are found old bug fix: show newly added style in popup dedupe/simplify bits of popup.js
733 lines
22 KiB
JavaScript
733 lines
22 KiB
JavaScript
/* global configDialog hotkeys msg
|
|
getActiveTab CHROME FIREFOX URLS API onDOMready $ $$ prefs
|
|
setupLivePrefs template t $create animateElement
|
|
tryJSONparse CHROME_HAS_BORDER_BUG */
|
|
|
|
'use strict';
|
|
|
|
/** @type Element */
|
|
let installed;
|
|
/** @type string */
|
|
let tabURL;
|
|
const handleEvent = {};
|
|
|
|
const ABOUT_BLANK = 'about:blank';
|
|
const ENTRY_ID_PREFIX_RAW = 'style-';
|
|
|
|
$.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`);
|
|
|
|
if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143
|
|
document.head.appendChild($create('style', 'html { overflow: overlay }'));
|
|
}
|
|
|
|
toggleSideBorders();
|
|
|
|
initTabUrls()
|
|
.then(frames =>
|
|
Promise.all([
|
|
onDOMready().then(() => initPopup(frames)),
|
|
...frames
|
|
.filter(f => f.url && !f.isDupe)
|
|
.map(({url}) => getStyleDataMerged(url).then(styles => ({styles, url}))),
|
|
]))
|
|
.then(([, ...results]) => {
|
|
if (results[0]) {
|
|
showStyles(results);
|
|
} else {
|
|
// unsupported URL;
|
|
$('#popup-manage-button').removeAttribute('title');
|
|
}
|
|
})
|
|
.catch(console.error);
|
|
|
|
msg.onExtension(onRuntimeMessage);
|
|
|
|
prefs.subscribe(['popup.stylesFirst'], (key, stylesFirst) => {
|
|
const actions = $('body > .actions');
|
|
const before = stylesFirst ? actions : actions.nextSibling;
|
|
document.body.insertBefore(installed, before);
|
|
});
|
|
prefs.subscribe(['popupWidth'], (key, value) => setPopupWidth(value));
|
|
|
|
if (CHROME_HAS_BORDER_BUG) {
|
|
prefs.subscribe(['popup.borders'], (key, value) => toggleSideBorders(value));
|
|
}
|
|
|
|
function onRuntimeMessage(msg) {
|
|
if (!tabURL) return;
|
|
let ready = Promise.resolve();
|
|
switch (msg.method) {
|
|
case 'styleAdded':
|
|
case 'styleUpdated':
|
|
if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
|
|
ready = handleUpdate(msg);
|
|
break;
|
|
case 'styleDeleted':
|
|
handleDelete(msg.style.id);
|
|
break;
|
|
}
|
|
ready.then(() => dispatchEvent(new CustomEvent(msg.method, {detail: msg})));
|
|
}
|
|
|
|
|
|
function setPopupWidth(width = prefs.get('popupWidth')) {
|
|
document.body.style.width =
|
|
Math.max(200, Math.min(800, width)) + 'px';
|
|
}
|
|
|
|
|
|
function toggleSideBorders(state = prefs.get('popup.borders')) {
|
|
// runs before <body> is parsed
|
|
const style = document.documentElement.style;
|
|
if (CHROME_HAS_BORDER_BUG && state) {
|
|
style.cssText +=
|
|
'border-left: 2px solid white !important;' +
|
|
'border-right: 2px solid white !important;';
|
|
} else if (style.cssText) {
|
|
style.borderLeft = style.borderRight = '';
|
|
}
|
|
}
|
|
|
|
function initTabUrls() {
|
|
return getActiveTab()
|
|
.then((tab = {}) =>
|
|
FIREFOX && tab.status === 'loading' && tab.url === ABOUT_BLANK
|
|
? waitForTabUrlFF(tab)
|
|
: tab)
|
|
.then(tab => new Promise(resolve =>
|
|
chrome.webNavigation.getAllFrames({tabId: tab.id}, frames =>
|
|
resolve({frames, tab}))))
|
|
.then(({frames, tab}) => {
|
|
let url = tab.pendingUrl || tab.url || ''; // new Chrome uses pendingUrl while connecting
|
|
frames = sortTabFrames(frames);
|
|
if (url === 'chrome://newtab/' && !URLS.chromeProtectsNTP) {
|
|
url = frames[0].url || '';
|
|
}
|
|
if (!URLS.supported(url)) {
|
|
url = '';
|
|
frames.length = 1;
|
|
}
|
|
tabURL = frames[0].url = url;
|
|
return frames;
|
|
});
|
|
}
|
|
|
|
/** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */
|
|
function initPopup(frames) {
|
|
installed = $('#installed');
|
|
|
|
setPopupWidth();
|
|
|
|
// action buttons
|
|
$('#disableAll').onchange = function () {
|
|
installed.classList.toggle('disabled', this.checked);
|
|
};
|
|
setupLivePrefs();
|
|
|
|
Object.assign($('#popup-manage-button'), {
|
|
onclick: handleEvent.openManager,
|
|
onmouseup: handleEvent.openManager,
|
|
oncontextmenu: handleEvent.openManager,
|
|
});
|
|
|
|
$('#popup-options-button').onclick = () => {
|
|
API.openManage({options: true});
|
|
window.close();
|
|
};
|
|
|
|
$('#popup-wiki-button').onclick = handleEvent.openURLandHide;
|
|
|
|
if (!prefs.get('popup.stylesFirst')) {
|
|
document.body.insertBefore(
|
|
$('body > .actions'),
|
|
installed);
|
|
}
|
|
|
|
if (!tabURL) {
|
|
blockPopup();
|
|
return;
|
|
}
|
|
|
|
frames.forEach(createWriterElement);
|
|
if (frames.length > 1) {
|
|
const el = $('#write-for-frames');
|
|
el.hidden = false;
|
|
el.onclick = () => el.classList.toggle('expanded');
|
|
}
|
|
|
|
getActiveTab().then(function ping(tab, retryCountdown = 10) {
|
|
msg.sendTab(tab.id, {method: 'ping'}, {frameId: 0})
|
|
.catch(() => false)
|
|
.then(pong => {
|
|
if (pong) {
|
|
return;
|
|
}
|
|
// FF and some Chrome forks (e.g. CentBrowser) implement tab-on-demand
|
|
// so we'll wait a bit to handle popup being invoked right after switching
|
|
if (retryCountdown > 0 && (
|
|
tab.status !== 'complete' ||
|
|
FIREFOX && tab.url === ABOUT_BLANK)) {
|
|
setTimeout(ping, 100, tab, --retryCountdown);
|
|
return;
|
|
}
|
|
const info = template.unreachableInfo;
|
|
if (!FIREFOX) {
|
|
// Chrome "Allow access to file URLs" in chrome://extensions message
|
|
info.appendChild($create('p', t('unreachableFileHint')));
|
|
}
|
|
if (FIREFOX && tabURL.startsWith(URLS.browserWebStore)) {
|
|
$('label', info).textContent = t('unreachableAMO');
|
|
const note = (FIREFOX < 59 ? t('unreachableAMOHintOldFF') : t('unreachableAMOHint')) +
|
|
(FIREFOX < 60 ? '' : '\n' + t('unreachableAMOHintNewFF'));
|
|
const renderToken = s => s[0] === '<'
|
|
? $create('a', {
|
|
textContent: s.slice(1, -1),
|
|
onclick: handleEvent.copyContent,
|
|
href: '#',
|
|
className: 'copy',
|
|
tabIndex: 0,
|
|
title: t('copy'),
|
|
})
|
|
: s;
|
|
const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken));
|
|
const noteNode = $create('fragment', note.split('\n').map(renderLine));
|
|
info.appendChild(noteNode);
|
|
}
|
|
// Inaccessible locally hosted file type, e.g. JSON, PDF, etc.
|
|
if (tabURL.length - tabURL.lastIndexOf('.') <= 5) {
|
|
info.appendChild($create('p', t('InaccessibleFileHint')));
|
|
}
|
|
document.body.classList.add('unreachable');
|
|
document.body.insertBefore(info, document.body.firstChild);
|
|
});
|
|
});
|
|
}
|
|
|
|
/** @param {chrome.webNavigation.GetAllFrameResultDetails} frame */
|
|
function createWriterElement(frame) {
|
|
const {url, frameId, parentFrameId, isDupe} = frame;
|
|
const targets = $create('span');
|
|
|
|
// For this URL
|
|
const urlLink = template.writeStyle.cloneNode(true);
|
|
const isAboutBlank = url === ABOUT_BLANK;
|
|
Object.assign(urlLink, {
|
|
href: 'edit.html?url-prefix=' + encodeURIComponent(url),
|
|
title: `url-prefix("${url}")`,
|
|
tabIndex: isAboutBlank ? -1 : 0,
|
|
textContent: prefs.get('popup.breadcrumbs.usePath')
|
|
? new URL(url).pathname.slice(1)
|
|
: frameId
|
|
? isAboutBlank ? url : 'URL'
|
|
: t('writeStyleForURL').replace(/ /g, '\u00a0'), // this URL
|
|
onclick: e => handleEvent.openEditor(e, {'url-prefix': url}),
|
|
});
|
|
if (prefs.get('popup.breadcrumbs')) {
|
|
urlLink.onmouseenter =
|
|
urlLink.onfocus = () => urlLink.parentNode.classList.add('url()');
|
|
urlLink.onmouseleave =
|
|
urlLink.onblur = () => urlLink.parentNode.classList.remove('url()');
|
|
}
|
|
targets.appendChild(urlLink);
|
|
|
|
// For domain
|
|
const domains = getDomains(url);
|
|
for (const domain of domains) {
|
|
const numParts = domain.length - domain.replace(/\./g, '').length + 1;
|
|
// Don't include TLD
|
|
if (domains.length > 1 && numParts === 1) {
|
|
continue;
|
|
}
|
|
const domainLink = template.writeStyle.cloneNode(true);
|
|
Object.assign(domainLink, {
|
|
href: 'edit.html?domain=' + encodeURIComponent(domain),
|
|
textContent: numParts > 2 ? domain.split('.')[0] : domain,
|
|
title: `domain("${domain}")`,
|
|
onclick: e => handleEvent.openEditor(e, {domain}),
|
|
});
|
|
domainLink.setAttribute('subdomain', numParts > 1 ? 'true' : '');
|
|
targets.appendChild(domainLink);
|
|
}
|
|
|
|
if (prefs.get('popup.breadcrumbs')) {
|
|
targets.classList.add('breadcrumbs');
|
|
targets.appendChild(urlLink); // making it the last element
|
|
}
|
|
|
|
const root = $('#write-style');
|
|
const parent = $(`[data-frame-id="${parentFrameId}"]`, root) || root;
|
|
const child = $create({
|
|
tag: 'span',
|
|
className: `match${isDupe ? ' dupe' : ''}${isAboutBlank ? ' about-blank' : ''}`,
|
|
dataset: {frameId},
|
|
appendChild: targets,
|
|
});
|
|
parent.appendChild(child);
|
|
parent.dataset.children = (Number(parent.dataset.children) || 0) + 1;
|
|
}
|
|
|
|
function getDomains(url) {
|
|
let d = url.split(/[/:]+/, 2)[1];
|
|
if (!d || url.startsWith('file:')) {
|
|
return [];
|
|
}
|
|
const domains = [d];
|
|
while (d.includes('.')) {
|
|
d = d.substring(d.indexOf('.') + 1);
|
|
domains.push(d);
|
|
}
|
|
return domains;
|
|
}
|
|
|
|
/** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */
|
|
function sortTabFrames(frames) {
|
|
const unknown = new Map(frames.map(f => [f.frameId, f]));
|
|
const known = new Map([[0, unknown.get(0) || {frameId: 0, url: ''}]]);
|
|
unknown.delete(0);
|
|
let lastSize = 0;
|
|
while (unknown.size !== lastSize) {
|
|
for (const [frameId, f] of unknown) {
|
|
if (known.has(f.parentFrameId)) {
|
|
unknown.delete(frameId);
|
|
if (!f.errorOccurred) known.set(frameId, f);
|
|
if (f.url === ABOUT_BLANK) f.url = known.get(f.parentFrameId).url;
|
|
}
|
|
}
|
|
lastSize = unknown.size; // guard against an infinite loop due to a weird frame structure
|
|
}
|
|
const sortedFrames = [...known.values(), ...unknown.values()];
|
|
const urls = new Set([ABOUT_BLANK]);
|
|
for (const f of sortedFrames) {
|
|
if (!f.url) f.url = '';
|
|
f.isDupe = urls.has(f.url);
|
|
urls.add(f.url);
|
|
}
|
|
return sortedFrames;
|
|
}
|
|
|
|
function sortStyles(entries) {
|
|
const enabledFirst = prefs.get('popup.enabledFirst');
|
|
return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
|
|
Boolean(a.frameUrl) - Boolean(b.frameUrl) ||
|
|
enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) ||
|
|
a.name.localeCompare(b.name));
|
|
}
|
|
|
|
function showStyles(frameResults) {
|
|
const entries = new Map();
|
|
frameResults.forEach(({styles = [], url}, index) => {
|
|
styles.forEach(style => {
|
|
const {id} = style;
|
|
if (!entries.has(id)) {
|
|
style.frameUrl = index === 0 ? '' : url;
|
|
entries.set(id, createStyleElement(style));
|
|
}
|
|
});
|
|
});
|
|
if (entries.size) {
|
|
resortEntries([...entries.values()]);
|
|
} else {
|
|
installed.appendChild(template.noStyles);
|
|
}
|
|
window.dispatchEvent(new Event('showStyles:done'));
|
|
}
|
|
|
|
function resortEntries(entries) {
|
|
// `entries` is specified only at startup, after that we respect the prefs
|
|
if (entries || prefs.get('popup.autoResort')) {
|
|
installed.append(...sortStyles(entries || $$('.entry', installed)));
|
|
}
|
|
}
|
|
|
|
function createStyleElement(style) {
|
|
let entry = $.entry(style);
|
|
if (!entry) {
|
|
entry = template.style.cloneNode(true);
|
|
entry.setAttribute('style-id', style.id);
|
|
Object.assign(entry, {
|
|
id: ENTRY_ID_PREFIX_RAW + style.id,
|
|
styleId: style.id,
|
|
styleIsUsercss: Boolean(style.usercssData),
|
|
onmousedown: handleEvent.maybeEdit,
|
|
styleMeta: style
|
|
});
|
|
const checkbox = $('.checker', entry);
|
|
Object.assign(checkbox, {
|
|
id: ENTRY_ID_PREFIX_RAW + style.id,
|
|
// title: t('exclusionsPopupTip'),
|
|
onclick: handleEvent.toggle,
|
|
// oncontextmenu: handleEvent.openExcludeMenu
|
|
});
|
|
const editLink = $('.style-edit-link', entry);
|
|
Object.assign(editLink, {
|
|
href: editLink.getAttribute('href') + style.id,
|
|
onclick: e => handleEvent.openEditor(e, {id: style.id}),
|
|
});
|
|
const styleName = $('.style-name', entry);
|
|
Object.assign(styleName, {
|
|
htmlFor: ENTRY_ID_PREFIX_RAW + style.id,
|
|
onclick: handleEvent.name,
|
|
});
|
|
styleName.checkbox = checkbox;
|
|
styleName.appendChild(document.createTextNode(' '));
|
|
|
|
const config = $('.configure', entry);
|
|
config.onclick = handleEvent.configure;
|
|
if (!style.usercssData) {
|
|
if (style.updateUrl && style.updateUrl.includes('?') && style.url) {
|
|
config.href = style.url;
|
|
config.target = '_blank';
|
|
config.title = t('configureStyleOnHomepage');
|
|
config.dataset.sendMessage = JSON.stringify({method: 'openSettings'});
|
|
$('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso';
|
|
} else {
|
|
config.classList.add('hidden');
|
|
}
|
|
} else if (Object.keys(style.usercssData.vars || {}).length === 0) {
|
|
config.classList.add('hidden');
|
|
}
|
|
|
|
$('.delete', entry).onclick = handleEvent.delete;
|
|
|
|
const indicator = template.regexpProblemIndicator.cloneNode(true);
|
|
indicator.appendChild(document.createTextNode('!'));
|
|
indicator.onclick = handleEvent.indicator;
|
|
$('.main-controls', entry).appendChild(indicator);
|
|
|
|
$('.menu-button', entry).onclick = handleEvent.toggleMenu;
|
|
$('.menu-close', entry).onclick = handleEvent.toggleMenu;
|
|
|
|
$('.exclude-by-domain-checkbox', entry).onchange = e => handleEvent.toggleExclude(e, 'domain');
|
|
$('.exclude-by-url-checkbox', entry).onchange = e => handleEvent.toggleExclude(e, 'url');
|
|
}
|
|
|
|
style = Object.assign(entry.styleMeta, style);
|
|
|
|
entry.classList.toggle('disabled', !style.enabled);
|
|
entry.classList.toggle('enabled', style.enabled);
|
|
$('.checker', entry).checked = style.enabled;
|
|
|
|
const styleName = $('.style-name', entry);
|
|
styleName.lastChild.textContent = style.name;
|
|
setTimeout(() => {
|
|
styleName.title = entry.styleMeta.sloppy ?
|
|
t('styleNotAppliedRegexpProblemTooltip') :
|
|
styleName.scrollWidth > styleName.clientWidth + 1 ?
|
|
styleName.textContent : '';
|
|
});
|
|
|
|
entry.classList.toggle('not-applied', style.excluded || style.sloppy);
|
|
entry.classList.toggle('regexp-partial', style.sloppy);
|
|
|
|
$('.exclude-by-domain-checkbox', entry).checked = styleExcluded(style, 'domain');
|
|
$('.exclude-by-url-checkbox', entry).checked = styleExcluded(style, 'url');
|
|
|
|
$('.exclude-by-domain', entry).title = getExcludeRule('domain');
|
|
$('.exclude-by-url', entry).title = getExcludeRule('url');
|
|
|
|
const {frameUrl} = style;
|
|
if (frameUrl) {
|
|
const sel = 'span.frame-url';
|
|
const frameEl = $(sel, entry) || styleName.insertBefore($create(sel), styleName.lastChild);
|
|
frameEl.title = frameUrl;
|
|
}
|
|
entry.classList.toggle('frame', Boolean(frameUrl));
|
|
|
|
return entry;
|
|
}
|
|
|
|
function styleExcluded({exclusions}, type) {
|
|
if (!exclusions) {
|
|
return false;
|
|
}
|
|
const rule = getExcludeRule(type);
|
|
return exclusions.includes(rule);
|
|
}
|
|
|
|
function getExcludeRule(type) {
|
|
const u = new URL(tabURL);
|
|
if (type === 'domain') {
|
|
return u.origin + '/*';
|
|
}
|
|
// current page
|
|
return escapeGlob(u.origin + u.pathname);
|
|
}
|
|
|
|
function escapeGlob(text) {
|
|
return text.replace(/\*/g, '\\*');
|
|
}
|
|
|
|
Object.assign(handleEvent, {
|
|
|
|
getClickedStyleId(event) {
|
|
return (handleEvent.getClickedStyleElement(event) || {}).styleId;
|
|
},
|
|
|
|
getClickedStyleElement(event) {
|
|
return event.target.closest('.entry');
|
|
},
|
|
|
|
name(event) {
|
|
this.checkbox.dispatchEvent(new MouseEvent('click'));
|
|
event.preventDefault();
|
|
},
|
|
|
|
toggle(event) {
|
|
// when fired on checkbox, prevent the parent label from seeing the event, see #501
|
|
event.stopPropagation();
|
|
API
|
|
.toggleStyle(handleEvent.getClickedStyleId(event), this.checked)
|
|
.then(() => resortEntries());
|
|
},
|
|
|
|
toggleExclude(event, type) {
|
|
const entry = handleEvent.getClickedStyleElement(event);
|
|
if (event.target.checked) {
|
|
API.addExclusion(entry.styleMeta.id, getExcludeRule(type));
|
|
} else {
|
|
API.removeExclusion(entry.styleMeta.id, getExcludeRule(type));
|
|
}
|
|
},
|
|
|
|
toggleMenu(event) {
|
|
const entry = handleEvent.getClickedStyleElement(event);
|
|
const menu = $('.menu', entry);
|
|
const menuActive = $('.menu[data-display=true]');
|
|
if (menuActive) {
|
|
// fade-out style menu
|
|
animateElement(menu, {
|
|
className: 'lights-on',
|
|
onComplete: () => (menu.dataset.display = false),
|
|
});
|
|
window.onkeydown = null;
|
|
} else {
|
|
$('.menu-title', entry).textContent = $('.style-name', entry).textContent;
|
|
menu.dataset.display = true;
|
|
menu.style.cssText = '';
|
|
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) {
|
|
event.preventDefault();
|
|
checkbox.focus();
|
|
}
|
|
if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) {
|
|
event.preventDefault();
|
|
close.focus();
|
|
}
|
|
if (keyCode === 27) {
|
|
event.preventDefault();
|
|
close.click();
|
|
}
|
|
};
|
|
}
|
|
event.preventDefault();
|
|
},
|
|
|
|
delete(event) {
|
|
const entry = handleEvent.getClickedStyleElement(event);
|
|
const id = entry.styleId;
|
|
const box = $('#confirm');
|
|
const menu = $('.menu', entry);
|
|
const cancel = $('[data-cmd="cancel"]', box);
|
|
const affirm = $('[data-cmd="ok"]', box);
|
|
box.dataset.display = true;
|
|
box.style.cssText = '';
|
|
$('b', box).textContent = $('.style-name', entry).textContent;
|
|
affirm.focus();
|
|
affirm.onclick = () => confirm(true);
|
|
cancel.onclick = () => confirm(false);
|
|
window.onkeydown = event => {
|
|
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)) {
|
|
event.preventDefault();
|
|
affirm.focus();
|
|
}
|
|
if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) {
|
|
event.preventDefault();
|
|
checkbox.focus();
|
|
}
|
|
if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) {
|
|
event.preventDefault();
|
|
close.focus();
|
|
}
|
|
if (keyCode === 27) {
|
|
event.preventDefault();
|
|
if (confirmActive) {
|
|
box.dataset.display = false;
|
|
menu.focus();
|
|
} else {
|
|
close.click();
|
|
}
|
|
}
|
|
};
|
|
function confirm(ok) {
|
|
if (ok) {
|
|
// fade-out deletion confirmation dialog
|
|
animateElement(box, {
|
|
className: 'lights-on',
|
|
onComplete: () => (box.dataset.display = false),
|
|
});
|
|
window.onkeydown = null;
|
|
API.deleteStyle(id);
|
|
} else {
|
|
box.dataset.display = false;
|
|
menu.focus();
|
|
}
|
|
}
|
|
},
|
|
|
|
configure(event) {
|
|
const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event);
|
|
if (styleIsUsercss) {
|
|
API.getStyle(styleId, true).then(style => {
|
|
hotkeys.setState(false);
|
|
configDialog(style).then(() => {
|
|
hotkeys.setState(true);
|
|
});
|
|
});
|
|
} else {
|
|
handleEvent.openURLandHide.call(this, event);
|
|
}
|
|
},
|
|
|
|
indicator(event) {
|
|
const entry = handleEvent.getClickedStyleElement(event);
|
|
const info = template.regexpProblemExplanation.cloneNode(true);
|
|
$.remove('#' + info.id);
|
|
$$('a', info).forEach(el => (el.onclick = handleEvent.openURLandHide));
|
|
$$('button', info).forEach(el => (el.onclick = handleEvent.closeExplanation));
|
|
entry.appendChild(info);
|
|
},
|
|
|
|
closeExplanation() {
|
|
$('#regexp-explanation').remove();
|
|
},
|
|
|
|
openEditor(event, options) {
|
|
event.preventDefault();
|
|
API.openEditor(options);
|
|
window.close();
|
|
},
|
|
|
|
maybeEdit(event) {
|
|
if (!(
|
|
event.button === 0 && (event.ctrlKey || event.metaKey) ||
|
|
event.button === 1 ||
|
|
event.button === 2)) {
|
|
return;
|
|
}
|
|
// open an editor on middleclick
|
|
if (event.target.matches('.entry, .style-name, .style-edit-link')) {
|
|
this.onmouseup = () => $('.style-edit-link', this).click();
|
|
this.oncontextmenu = event => event.preventDefault();
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
// prevent the popup being opened in a background tab
|
|
// when an irrelevant link was accidentally clicked
|
|
if (event.target.closest('a')) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
},
|
|
|
|
openURLandHide(event) {
|
|
event.preventDefault();
|
|
getActiveTab()
|
|
.then(activeTab => API.openURL({
|
|
url: this.href || this.dataset.href,
|
|
index: activeTab.index + 1,
|
|
message: tryJSONparse(this.dataset.sendMessage),
|
|
}))
|
|
.then(window.close);
|
|
},
|
|
|
|
openManager(event) {
|
|
if (event.button === 2 && !tabURL) return;
|
|
event.preventDefault();
|
|
if (!this.eventHandled) {
|
|
// FIXME: this only works if popup is closed
|
|
this.eventHandled = true;
|
|
API.openManage({
|
|
search: tabURL && (event.shiftKey || event.button === 2) ?
|
|
`url:${tabURL}` : null
|
|
});
|
|
window.close();
|
|
}
|
|
},
|
|
|
|
copyContent(event) {
|
|
event.preventDefault();
|
|
const target = document.activeElement;
|
|
const message = $('.copy-message');
|
|
navigator.clipboard.writeText(target.textContent);
|
|
target.classList.add('copied');
|
|
message.classList.add('show-message');
|
|
setTimeout(() => {
|
|
target.classList.remove('copied');
|
|
message.classList.remove('show-message');
|
|
}, 1000);
|
|
},
|
|
});
|
|
|
|
|
|
async function handleUpdate({style, reason}) {
|
|
if (reason !== 'toggle' || !$.entry(style)) {
|
|
style = await getStyleDataMerged(tabURL, style.id);
|
|
if (!style) return;
|
|
}
|
|
const el = createStyleElement(style);
|
|
if (!el.parentNode) {
|
|
installed.appendChild(el);
|
|
blockPopup(false);
|
|
}
|
|
resortEntries();
|
|
}
|
|
|
|
|
|
function handleDelete(id) {
|
|
const el = $.entry(id);
|
|
if (el) {
|
|
el.remove();
|
|
if (!$('.entry')) installed.appendChild(template.noStyles);
|
|
}
|
|
}
|
|
|
|
function waitForTabUrlFF(tab) {
|
|
return new Promise(resolve => {
|
|
browser.tabs.onUpdated.addListener(...[
|
|
function onUpdated(tabId, info, updatedTab) {
|
|
if (info.url && tabId === tab.id) {
|
|
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
resolve(updatedTab);
|
|
}
|
|
},
|
|
...'UpdateFilter' in browser.tabs ? [{tabId: tab.id}] : [],
|
|
// TODO: remove both spreads and tabId check when strict_min_version >= 61
|
|
]);
|
|
});
|
|
}
|
|
|
|
/* Merges the extra props from API into style data.
|
|
* When `id` is specified returns a single object otherwise an array */
|
|
async function getStyleDataMerged(url, id) {
|
|
const styles = (await API.getStylesByUrl(url, id))
|
|
.map(r => Object.assign(r.data, r));
|
|
return id ? styles[0] : styles;
|
|
}
|
|
|
|
function blockPopup(isBlocked = true) {
|
|
document.body.classList.toggle('blocked', isBlocked);
|
|
if (isBlocked) {
|
|
document.body.prepend(template.unavailableInfo);
|
|
} else {
|
|
template.unavailableInfo.remove();
|
|
template.noStyles.remove();
|
|
}
|
|
}
|