show write-style entries for iframes in popup (#861)

* account for iframes in popup list/write-style and badge

* fix and simplify openURL + onTabReady + message from popup

* fixup! resolve about:blank iframes to their parent URL

* fixup! don't underline iframe links until hovered

* fix width bug in popup only when needed (Chrome 66-69)

* fixup! reset styleIds on main page navigation

* fixup! call updateCount explicitly on extension pages

* fixup! ensure frame url is present

* fixup! frameResults entry may be empty

* fixup! init main frame first

* fixup! track iframes via ports

* fixup! reduce badge update rate during page load

* fixup! cosmetics

* fixup! don't add frames with errors

* fixup! cosmetics
This commit is contained in:
tophf 2020-02-25 02:16:45 +03:00 committed by GitHub
parent 4bbce7cb9f
commit 8192fab1b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 368 additions and 245 deletions

View File

@ -2,7 +2,7 @@
URLS ignoreChromeError usercssHelper
styleManager msg navigatorUtil workerUtil contentScripts sync
findExistingTab createTab activateTab isTabReplaceable getActiveTab
iconManager tabManager */
tabManager */
'use strict';
@ -49,16 +49,27 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
openEditor,
updateIconBadge(count) {
iconManager.updateIconBadge(this.sender.tab.id, count);
return true;
/* 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)));
function onTabReady(tab) {
return new Promise((resolve, reject) =>
setTimeout(function ping(numTries = 10, delay = 100) {
msg.sendTab(tab.id, {method: 'ping'})
.catch(() => false)
.then(pong => pong
? resolve(tab)
: numTries && setTimeout(ping, delay, numTries - 1, delay * 1.5) ||
reject('timeout'));
}));
}
},
// exposed for stuff that requires followup sendMessage() like popup::openSettings
// that would fail otherwise if another extension forced the tab to open
// in the foreground thus auto-closing the popup (in Chrome)
openURL,
optionsCustomizeHotkeys() {
return browser.runtime.openOptionsPage()
.then(() => new Promise(resolve => setTimeout(resolve, 100)))

View File

@ -1,9 +1,10 @@
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager */
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API_METHODS */
/* exported iconManager */
'use strict';
const iconManager = (() => {
const ICON_SIZES = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38];
const staleBadges = new Set();
prefs.subscribe([
'disableAll',
@ -26,32 +27,51 @@ const iconManager = (() => {
refreshAllIcons();
});
return {updateIconBadge};
Object.assign(API_METHODS, {
/** @param {(number|string)[]} styleIds
* @param {boolean} [lazyBadge=false] preventing flicker during page load */
updateIconBadge(styleIds, {lazyBadge} = {}) {
// FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
const {frameId, tab: {id: tabId}} = this.sender;
const value = styleIds.length ? styleIds.map(Number) : undefined;
tabManager.set(tabId, 'styleIds', frameId, value);
debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0);
staleBadges.add(tabId);
if (!frameId) refreshIcon(tabId, true);
},
});
// FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
function updateIconBadge(tabId, count, force = true) {
tabManager.set(tabId, 'count', count);
refreshIconBadgeText(tabId);
refreshIcon(tabId, force);
navigatorUtil.onCommitted(({tabId, frameId}) => {
if (!frameId) tabManager.set(tabId, 'styleIds', undefined);
});
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'iframe') {
port.onDisconnect.addListener(onPortDisconnected);
}
});
function onPortDisconnected({sender}) {
if (tabManager.get(sender.tab.id, 'styleIds')) {
API_METHODS.updateIconBadge.call({sender}, [], {lazyBadge: true});
}
}
function refreshIconBadgeText(tabId) {
const count = tabManager.get(tabId, 'count');
iconUtil.setBadgeText({
text: prefs.get('show-badge') && count ? String(count) : '',
tabId
});
const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
iconUtil.setBadgeText({tabId, text});
}
function getIconName(count = 0) {
function getIconName(hasStyles = false) {
const iconset = prefs.get('iconset') === 1 ? 'light/' : '';
const postfix = prefs.get('disableAll') ? 'x' : !count ? 'w' : '';
const postfix = prefs.get('disableAll') ? 'x' : !hasStyles ? 'w' : '';
return `${iconset}$SIZE$${postfix}`;
}
function refreshIcon(tabId, force = false) {
const oldIcon = tabManager.get(tabId, 'icon');
const newIcon = getIconName(tabManager.get(tabId, 'count'));
const newIcon = getIconName(tabManager.get(tabId, 'styleIds', 0));
// (changing the icon only for the main page, frameId = 0)
if (!force && oldIcon === newIcon) {
return;
@ -73,6 +93,14 @@ const iconManager = (() => {
);
}
/** @return {number | ''} */
function getStyleCount(tabId) {
const allIds = new Set();
const data = tabManager.get(tabId, 'styleIds') || {};
Object.values(data).forEach(frameIds => frameIds.forEach(id => allIds.add(id)));
return allIds.size || '';
}
function refreshGlobalIcon() {
iconUtil.setIcon({
path: getIconPath(getIconName())
@ -98,4 +126,11 @@ const iconManager = (() => {
refreshIconBadgeText(tabId);
}
}
function refreshStaleBadges() {
for (const tabId of staleBadges) {
refreshIconBadgeText(tabId);
}
staleBadges.clear();
}
})();

View File

@ -1,4 +1,4 @@
/* global API_METHODS styleManager CHROME prefs iconManager */
/* global API_METHODS styleManager CHROME prefs */
'use strict';
API_METHODS.styleViaAPI = !CHROME && (() => {
@ -31,12 +31,13 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
.then(maybeToggleObserver);
};
function updateCount(request, {tab, frameId}) {
function updateCount(request, sender) {
const {tab, frameId} = sender;
if (frameId) {
throw new Error('we do not count styles for frames');
}
const {frameStyles} = getCachedData(tab.id, frameId);
iconManager.updateIconBadge(tab.id, Object.keys(frameStyles).length);
API_METHODS.updateIconBadge.call({sender}, Object.keys(frameStyles));
}
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {

View File

@ -24,17 +24,28 @@ const tabManager = (() => {
onUpdate(fn) {
listeners.push(fn);
},
get(tabId, key) {
const meta = cache.get(tabId);
return meta && meta[key];
get(tabId, ...keys) {
return keys.reduce((meta, key) => meta && meta[key], cache.get(tabId));
},
set(tabId, key, value) {
/**
* number of keys is arbitrary, last arg is value, `undefined` will delete the last key from meta
* (tabId, 'foo', 123) will set tabId's meta to {foo: 123},
* (tabId, 'foo', 'bar', 'etc', 123) will set tabId's meta to {foo: {bar: {etc: 123}}}
*/
set(tabId, ...args) {
let meta = cache.get(tabId);
if (!meta) {
meta = {};
cache.set(tabId, meta);
}
meta[key] = value;
const value = args.pop();
const lastKey = args.pop();
for (const key of args) meta = meta[key] || (meta[key] = {});
if (value === undefined) {
delete meta[lastKey];
} else {
meta[lastKey] = value;
}
},
list() {
return cache.keys();

View File

@ -9,12 +9,25 @@
self.INJECTED !== 1 && (() => {
self.INJECTED = 1;
let IS_TAB = !chrome.tabs || location.pathname !== '/popup.html';
const IS_FRAME = window !== parent;
const STYLE_VIA_API = !chrome.app && document instanceof XMLDocument;
const styleInjector = createStyleInjector({
compare: (a, b) => a.id - b.id,
onUpdate: onInjectorUpdate,
});
const initializing = init();
/** @type chrome.runtime.Port */
let port;
let lazyBadge = IS_FRAME;
// the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason
if (!IS_TAB) {
chrome.tabs.getCurrent(tab => {
IS_TAB = Boolean(tab);
if (tab && styleInjector.list.length) updateCount();
});
}
// save it now because chrome.runtime will be unavailable in the orphaned script
const orphanEventId = chrome.runtime.id;
@ -32,7 +45,7 @@ self.INJECTED !== 1 && (() => {
let parentDomain;
prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value));
if (window !== parent) {
if (IS_FRAME) {
prefs.subscribe(['exposeIframes'], updateExposeIframes);
}
@ -55,7 +68,7 @@ self.INJECTED !== 1 && (() => {
// dynamic about: and javascript: iframes don't have an URL yet
// so we'll try the parent frame which is guaranteed to have a real URL
try {
if (window !== parent) {
if (IS_FRAME) {
matchUrl = parent.location.href;
}
} catch (e) {}
@ -153,19 +166,19 @@ self.INJECTED !== 1 && (() => {
}
function updateCount() {
if (window !== parent) {
// we don't care about iframes
return;
}
if (/^\w+?-extension:\/\/.+(popup|options)\.html$/.test(location.href)) {
// popup and the option page are not tabs
return;
}
if (STYLE_VIA_API) {
API.styleViaAPI({method: 'updateCount'}).catch(msg.ignoreError);
} else {
API.updateIconBadge(styleInjector.list.length).catch(console.error);
if (!IS_TAB) return;
if (IS_FRAME) {
if (!port && styleInjector.list.length) {
port = chrome.runtime.connect({name: 'iframe'});
} else if (port && !styleInjector.list.length) {
port.disconnect();
}
if (lazyBadge && performance.now() > 1000) lazyBadge = false;
}
(STYLE_VIA_API ?
API.styleViaAPI({method: 'updateCount'}) :
API.updateIconBadge(styleInjector.list.map(style => style.id), {lazyBadge})
).catch(msg.ignoreError);
}
function orphanCheck() {

View File

@ -1,4 +1,4 @@
/* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL
/* exported getTab getActiveTab onTabReady stringAsRegExp openURL ignoreChromeError
getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual
closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */
/* global promisify */
@ -125,82 +125,6 @@ function getActiveTab() {
.then(tabs => tabs[0]);
}
function getTabRealURL(tab) {
return new Promise(resolve => {
if (tab.url !== 'chrome://newtab/' || URLS.chromeProtectsNTP) {
resolve(tab.url);
} else {
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
resolve(frame && frame.url || '');
});
}
});
}
/**
* Resolves when the [just created] tab is ready for communication.
* @param {Number|Tab} tabOrId
* @returns {Promise<?Tab>}
*/
function onTabReady(tabOrId) {
let tabId, tab;
if (Number.isInteger(tabOrId)) {
tabId = tabOrId;
} else {
tab = tabOrId;
tabId = tab && tab.id;
}
if (!tab) {
return getTab(tabId).then(onTabReady);
}
if (tab.status === 'complete') {
if (!FIREFOX || tab.url !== 'about:blank') {
return Promise.resolve(tab);
} else {
return new Promise(resolve => {
chrome.webNavigation.getFrame({tabId, frameId: 0}, frame => {
ignoreChromeError();
if (frame) {
onTabReady(tab).then(resolve);
} else {
setTimeout(() => onTabReady(tabId).then(resolve));
}
});
});
}
}
return new Promise((resolve, reject) => {
chrome.webNavigation.onCommitted.addListener(onCommitted);
chrome.webNavigation.onErrorOccurred.addListener(onErrorOccurred);
chrome.tabs.onRemoved.addListener(onTabRemoved);
chrome.tabs.onReplaced.addListener(onTabReplaced);
function onCommitted(info) {
if (info.tabId !== tabId) return;
unregister();
getTab(tab.id).then(resolve);
}
function onErrorOccurred(info) {
if (info.tabId !== tabId) return;
unregister();
reject();
}
function onTabRemoved(removedTabId) {
if (removedTabId !== tabId) return;
unregister();
reject();
}
function onTabReplaced(addedTabId, removedTabId) {
onTabRemoved(removedTabId);
}
function unregister() {
chrome.webNavigation.onCommitted.removeListener(onCommitted);
chrome.webNavigation.onErrorOccurred.removeListener(onErrorOccurred);
chrome.tabs.onRemoved.removeListener(onTabRemoved);
chrome.tabs.onReplaced.removeListener(onTabReplaced);
}
});
}
function urlToMatchPattern(url, ignoreSearch) {
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns
if (!/^(http|https|ws|wss|ftp|data|file)$/.test(url.protocol)) {

View File

@ -235,6 +235,7 @@
</span>
</div>
<div id="write-style">
<a id="write-for-frames" href="#" title="<IFRAME>..." hidden></a>
<span id="write-style-for" i18n-text="writeStyleFor"></span>
</div>
</div>

View File

@ -8,12 +8,6 @@
--outer-padding: 9px;
}
html {
/* Chrome 66-?? adds a gap equal to the scrollbar width,
which looks like a bug, see https://crbug.com/821143 */
overflow: overlay;
}
html, body {
height: min-content;
max-height: 600px;
@ -313,6 +307,15 @@ a.configure[target="_blank"] .svg-icon.config {
color: darkred;
}
.frame-url::before {
content: "iframe: ";
color: lightslategray;
}
.frame .style-name {
font-weight: normal;
}
/* entry menu */
.entry .menu {
display: none;
@ -516,13 +519,85 @@ body.blocked .actions > .main-controls {
content: "\00ad"; /* "soft" hyphen */
}
#match {
.about-blank > .breadcrumbs {
pointer-events: none;
}
.about-blank > .breadcrumbs a {
text-decoration: none;
}
.match {
overflow-wrap: break-word;
display: block;
flex-grow: 9;
}
.match[data-frame-id="0"] {
min-width: 200px;
}
.match[data-frame-id="0"] > .match {
margin-top: .25em;
}
.match:not([data-frame-id="0"]) a {
text-decoration: none; /* not underlining iframe links until hovered to reduce visual noise */
}
.match .match {
margin-left: .5rem;
}
.match .match::before {
content: "";
width: .25rem;
height: .25rem;
margin-left: -.5rem;
display: block;
position: absolute;
border-width: 1px;
border-style: none none solid solid;
}
.dupe > .breadcrumbs {
opacity: .5;
}
.dupe:not([data-children]) {
display: none;
}
#write-for-frames {
position: absolute;
width: 5px;
height: 5px;
margin-left: -12px;
margin-top: 4px;
--dash: transparent 2px, currentColor 2px, currentColor 3px, transparent 3px;
background: linear-gradient(var(--dash)), linear-gradient(90deg, var(--dash));
}
#write-for-frames.expanded {
background: linear-gradient(var(--dash));
}
#write-for-frames::after {
position: absolute;
margin: -2px;
border: 1px solid currentColor;
content: "";
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#write-for-frames:not(.expanded) ~ .match:not([data-frame-id="0"]),
#write-for-frames:not(.expanded) ~ .match .match {
display: none;
}
/* "breadcrumbs" 'new style' links */
.breadcrumbs > .write-style-link {
margin-left: 0

View File

@ -1,39 +1,41 @@
/* global configDialog hotkeys onTabReady msg
getActiveTab FIREFOX getTabRealURL URLS API onDOMready $ $$ prefs
/* global configDialog hotkeys msg
getActiveTab CHROME FIREFOX URLS API onDOMready $ $$ prefs
setupLivePrefs template t $create animateElement
tryJSONparse debounce CHROME_HAS_BORDER_BUG */
tryJSONparse CHROME_HAS_BORDER_BUG */
'use strict';
/** @type Element */
let installed;
/** @type string */
let tabURL;
let unsupportedURL;
const handleEvent = {};
const ABOUT_BLANK = 'about:blank';
const ENTRY_ID_PREFIX_RAW = 'style-';
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
if (CHROME >= 3345 && CHROME < 3533) { // Chrome 66-69 adds a gap, https://crbug.com/821143
document.head.appendChild($create('style', 'html { overflow: overlay }'));
}
toggleSideBorders();
getActiveTab()
.then(tab =>
FIREFOX && tab.url === 'about:blank' && tab.status === 'loading'
? getTabRealURLFirefox(tab)
: getTabRealURL(tab)
)
.then(url => Promise.all([
(tabURL = URLS.supported(url) ? url : '') &&
API.getStylesByUrl(tabURL),
onDOMready().then(initPopup),
]))
.then(([results]) => {
if (!results) {
initTabUrls()
.then(frames =>
Promise.all([
onDOMready().then(() => initPopup(frames)),
...frames
.filter(f => f.url && !f.isDupe)
.map(({url}) => API.getStylesByUrl(url).then(styles => ({styles, url}))),
]))
.then(([, ...results]) => {
if (results[0]) {
showStyles(results);
} else {
// unsupported URL;
unsupportedURL = true;
$('#popup-manage-button').removeAttribute('title');
return;
}
showStyles(results.map(r => Object.assign(r.data, r)));
})
.catch(console.error);
@ -83,8 +85,32 @@ function toggleSideBorders(state = prefs.get('popup.borders')) {
}
}
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;
});
}
function initPopup() {
/** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */
function initPopup(frames) {
installed = $('#installed');
setPopupWidth();
@ -120,6 +146,13 @@ function initPopup() {
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)
@ -131,7 +164,7 @@ function initPopup() {
// 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')) {
FIREFOX && tab.url === ABOUT_BLANK)) {
setTimeout(ping, 100, tab, --retryCountdown);
return;
}
@ -166,24 +199,26 @@ function initPopup() {
document.body.insertBefore(info, document.body.firstChild);
});
});
}
// Write new style links
const writeStyle = $('#write-style');
const matchTargets = document.createElement('span');
const matchWrapper = document.createElement('span');
matchWrapper.id = 'match';
matchWrapper.appendChild(matchTargets);
/** @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(tabURL),
title: `url-prefix("${tabURL}")`,
href: 'edit.html?url-prefix=' + encodeURIComponent(url),
title: `url-prefix("${url}")`,
tabIndex: isAboutBlank ? -1 : 0,
textContent: prefs.get('popup.breadcrumbs.usePath')
? new URL(tabURL).pathname.slice(1)
// this&nbsp;URL
: t('writeStyleForURL').replace(/ /g, '\u00a0'),
onclick: e => handleEvent.openEditor(e, {'url-prefix': tabURL}),
? new URL(url).pathname.slice(1)
: frameId
? isAboutBlank ? url : 'URL'
: t('writeStyleForURL').replace(/ /g, '\u00a0'), // this&nbsp;URL
onclick: e => handleEvent.openEditor(e, {'url-prefix': url}),
});
if (prefs.get('popup.breadcrumbs')) {
urlLink.onmouseenter =
@ -191,10 +226,10 @@ function initPopup() {
urlLink.onmouseleave =
urlLink.onblur = () => urlLink.parentNode.classList.remove('url()');
}
matchTargets.appendChild(urlLink);
targets.appendChild(urlLink);
// For domain
const domains = getDomains(tabURL);
const domains = getDomains(url);
for (const domain of domains) {
const numParts = domain.length - domain.replace(/\./g, '').length + 1;
// Don't include TLD
@ -209,66 +244,92 @@ function initPopup() {
onclick: e => handleEvent.openEditor(e, {domain}),
});
domainLink.setAttribute('subdomain', numParts > 1 ? 'true' : '');
matchTargets.appendChild(domainLink);
targets.appendChild(domainLink);
}
if (prefs.get('popup.breadcrumbs')) {
matchTargets.classList.add('breadcrumbs');
matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild));
targets.classList.add('breadcrumbs');
targets.appendChild(urlLink); // making it the last element
}
writeStyle.appendChild(matchWrapper);
function getDomains(url) {
let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
if (!d || url.startsWith('file:')) {
return [];
}
const domains = [d];
while (d.indexOf('.') !== -1) {
d = d.substring(d.indexOf('.') + 1);
domains.push(d);
}
return domains;
}
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');
entries.sort((a, b) =>
enabledFirst && a.styleMeta.enabled !== b.styleMeta.enabled ?
(a.styleMeta.enabled ? -1 : 1) :
a.styleMeta.name.localeCompare(b.styleMeta.name)
);
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(styles) {
if (!styles) {
return;
}
if (!styles.length) {
function showStyles(frameResults) {
const entries = new Map();
frameResults.forEach(({styles = [], url}, index) => {
styles.forEach(style => {
const {id} = style.data;
if (!entries.has(id)) {
style.frameUrl = index === 0 ? '' : url;
entries.set(id, createStyleElement(Object.assign(style.data, style)));
}
});
});
if (entries.size) {
installed.append(...sortStyles([...entries.values()]));
} else {
installed.appendChild(template.noStyles.cloneNode(true));
window.dispatchEvent(new Event('showStyles:done'));
return;
}
const entries = styles.map(createStyleElement);
sortStyles(entries);
entries.forEach(e => installed.appendChild(e));
window.dispatchEvent(new Event('showStyles:done'));
}
function sortStylesInPlace() {
if (!prefs.get('popup.autoResort')) {
return;
}
const entries = $$('.entry', installed);
if (!entries.length) {
return;
}
sortStyles(entries);
entries.forEach(e => installed.appendChild(e));
}
function createStyleElement(style) {
let entry = $(ENTRY_ID_PREFIX + style.id);
@ -356,6 +417,14 @@ function createStyleElement(style) {
$('.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;
}
@ -400,7 +469,11 @@ Object.assign(handleEvent, {
event.stopPropagation();
API
.toggleStyle(handleEvent.getClickedStyleId(event), this.checked)
.then(sortStylesInPlace);
.then(() => {
if (prefs.get('popup.autoResort')) {
installed.append(...sortStyles($$('.entry', installed)));
}
});
},
toggleExclude(event, type) {
@ -561,23 +634,17 @@ Object.assign(handleEvent, {
openURLandHide(event) {
event.preventDefault();
const message = tryJSONparse(this.dataset.sendMessage);
getActiveTab()
.then(activeTab => API.openURL({
url: this.href || this.dataset.href,
index: activeTab.index + 1
index: activeTab.index + 1,
message: tryJSONparse(this.dataset.sendMessage),
}))
.then(tab => {
if (message) {
return onTabReady(tab)
.then(() => msg.sendTab(tab.id, message));
}
})
.then(window.close);
},
openManager(event) {
if (event.button === 2 && unsupportedURL) return;
if (event.button === 2 && !tabURL) return;
event.preventDefault();
if (!this.eventHandled) {
// FIXME: this only works if popup is closed
@ -640,32 +707,17 @@ function handleDelete(id) {
}
}
function getTabRealURLFirefox(tab) {
// wait for FF tab-on-demand to get a real URL (initially about:blank), 5 sec max
function waitForTabUrlFF(tab) {
return new Promise(resolve => {
function onNavigation({tabId, url, frameId}) {
if (tabId === tab.id && frameId === 0) {
detach();
resolve(url);
}
}
function detach(timedOut) {
if (timedOut) {
resolve(tab.url);
} else {
debounce.unregister(detach);
}
chrome.webNavigation.onBeforeNavigate.removeListener(onNavigation);
chrome.webNavigation.onCommitted.removeListener(onNavigation);
chrome.tabs.onRemoved.removeListener(detach);
chrome.tabs.onReplaced.removeListener(detach);
}
chrome.webNavigation.onBeforeNavigate.addListener(onNavigation);
chrome.webNavigation.onCommitted.addListener(onNavigation);
chrome.tabs.onRemoved.addListener(detach);
chrome.tabs.onReplaced.addListener(detach);
debounce(detach, 5000, {timedOut: true});
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
]);
});
}