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 URLS ignoreChromeError usercssHelper
styleManager msg navigatorUtil workerUtil contentScripts sync styleManager msg navigatorUtil workerUtil contentScripts sync
findExistingTab createTab activateTab isTabReplaceable getActiveTab findExistingTab createTab activateTab isTabReplaceable getActiveTab
iconManager tabManager */ tabManager */
'use strict'; 'use strict';
@ -49,16 +49,27 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
openEditor, openEditor,
updateIconBadge(count) { /* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent when the tab is ready,
iconManager.updateIconBadge(this.sender.tab.id, count); which is needed in the popup, otherwise another extension could force the tab to open in foreground
return true; 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() { optionsCustomizeHotkeys() {
return browser.runtime.openOptionsPage() return browser.runtime.openOptionsPage()
.then(() => new Promise(resolve => setTimeout(resolve, 100))) .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 */ /* exported iconManager */
'use strict'; 'use strict';
const iconManager = (() => { const iconManager = (() => {
const ICON_SIZES = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38]; const ICON_SIZES = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38];
const staleBadges = new Set();
prefs.subscribe([ prefs.subscribe([
'disableAll', 'disableAll',
@ -26,32 +27,51 @@ const iconManager = (() => {
refreshAllIcons(); 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? // FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
function updateIconBadge(tabId, count, force = true) { const {frameId, tab: {id: tabId}} = this.sender;
tabManager.set(tabId, 'count', count); const value = styleIds.length ? styleIds.map(Number) : undefined;
refreshIconBadgeText(tabId); tabManager.set(tabId, 'styleIds', frameId, value);
refreshIcon(tabId, force); debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0);
staleBadges.add(tabId);
if (!frameId) refreshIcon(tabId, true);
},
});
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) { function refreshIconBadgeText(tabId) {
const count = tabManager.get(tabId, 'count'); const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
iconUtil.setBadgeText({ iconUtil.setBadgeText({tabId, text});
text: prefs.get('show-badge') && count ? String(count) : '',
tabId
});
} }
function getIconName(count = 0) { function getIconName(hasStyles = false) {
const iconset = prefs.get('iconset') === 1 ? 'light/' : ''; 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}`; return `${iconset}$SIZE$${postfix}`;
} }
function refreshIcon(tabId, force = false) { function refreshIcon(tabId, force = false) {
const oldIcon = tabManager.get(tabId, 'icon'); 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) { if (!force && oldIcon === newIcon) {
return; 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() { function refreshGlobalIcon() {
iconUtil.setIcon({ iconUtil.setIcon({
path: getIconPath(getIconName()) path: getIconPath(getIconName())
@ -98,4 +126,11 @@ const iconManager = (() => {
refreshIconBadgeText(tabId); 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'; 'use strict';
API_METHODS.styleViaAPI = !CHROME && (() => { API_METHODS.styleViaAPI = !CHROME && (() => {
@ -31,12 +31,13 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
.then(maybeToggleObserver); .then(maybeToggleObserver);
}; };
function updateCount(request, {tab, frameId}) { function updateCount(request, sender) {
const {tab, frameId} = sender;
if (frameId) { if (frameId) {
throw new Error('we do not count styles for frames'); throw new Error('we do not count styles for frames');
} }
const {frameStyles} = getCachedData(tab.id, frameId); 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}) { function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {

View File

@ -24,17 +24,28 @@ const tabManager = (() => {
onUpdate(fn) { onUpdate(fn) {
listeners.push(fn); listeners.push(fn);
}, },
get(tabId, key) { get(tabId, ...keys) {
const meta = cache.get(tabId); return keys.reduce((meta, key) => meta && meta[key], cache.get(tabId));
return meta && meta[key];
}, },
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); let meta = cache.get(tabId);
if (!meta) { if (!meta) {
meta = {}; meta = {};
cache.set(tabId, 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() { list() {
return cache.keys(); return cache.keys();

View File

@ -9,12 +9,25 @@
self.INJECTED !== 1 && (() => { self.INJECTED !== 1 && (() => {
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 STYLE_VIA_API = !chrome.app && document instanceof XMLDocument;
const styleInjector = createStyleInjector({ const styleInjector = createStyleInjector({
compare: (a, b) => a.id - b.id, compare: (a, b) => a.id - b.id,
onUpdate: onInjectorUpdate, onUpdate: onInjectorUpdate,
}); });
const initializing = init(); 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 // save it now because chrome.runtime will be unavailable in the orphaned script
const orphanEventId = chrome.runtime.id; const orphanEventId = chrome.runtime.id;
@ -32,7 +45,7 @@ self.INJECTED !== 1 && (() => {
let parentDomain; let parentDomain;
prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value)); prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value));
if (window !== parent) { if (IS_FRAME) {
prefs.subscribe(['exposeIframes'], updateExposeIframes); prefs.subscribe(['exposeIframes'], updateExposeIframes);
} }
@ -55,7 +68,7 @@ self.INJECTED !== 1 && (() => {
// dynamic about: and javascript: iframes don't have an URL yet // 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 // so we'll try the parent frame which is guaranteed to have a real URL
try { try {
if (window !== parent) { if (IS_FRAME) {
matchUrl = parent.location.href; matchUrl = parent.location.href;
} }
} catch (e) {} } catch (e) {}
@ -153,19 +166,19 @@ self.INJECTED !== 1 && (() => {
} }
function updateCount() { function updateCount() {
if (window !== parent) { if (!IS_TAB) return;
// we don't care about iframes if (IS_FRAME) {
return; if (!port && styleInjector.list.length) {
port = chrome.runtime.connect({name: 'iframe'});
} else if (port && !styleInjector.list.length) {
port.disconnect();
} }
if (/^\w+?-extension:\/\/.+(popup|options)\.html$/.test(location.href)) { if (lazyBadge && performance.now() > 1000) lazyBadge = false;
// 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);
} }
(STYLE_VIA_API ?
API.styleViaAPI({method: 'updateCount'}) :
API.updateIconBadge(styleInjector.list.map(style => style.id), {lazyBadge})
).catch(msg.ignoreError);
} }
function orphanCheck() { 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 getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual
closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */ closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */
/* global promisify */ /* global promisify */
@ -125,82 +125,6 @@ function getActiveTab() {
.then(tabs => tabs[0]); .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) { function urlToMatchPattern(url, ignoreSearch) {
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns
if (!/^(http|https|ws|wss|ftp|data|file)$/.test(url.protocol)) { if (!/^(http|https|ws|wss|ftp|data|file)$/.test(url.protocol)) {

View File

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

View File

@ -8,12 +8,6 @@
--outer-padding: 9px; --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 { html, body {
height: min-content; height: min-content;
max-height: 600px; max-height: 600px;
@ -313,6 +307,15 @@ a.configure[target="_blank"] .svg-icon.config {
color: darkred; color: darkred;
} }
.frame-url::before {
content: "iframe: ";
color: lightslategray;
}
.frame .style-name {
font-weight: normal;
}
/* entry menu */ /* entry menu */
.entry .menu { .entry .menu {
display: none; display: none;
@ -516,13 +519,85 @@ body.blocked .actions > .main-controls {
content: "\00ad"; /* "soft" hyphen */ content: "\00ad"; /* "soft" hyphen */
} }
#match { .about-blank > .breadcrumbs {
pointer-events: none;
}
.about-blank > .breadcrumbs a {
text-decoration: none;
}
.match {
overflow-wrap: break-word; overflow-wrap: break-word;
display: block; display: block;
flex-grow: 9; flex-grow: 9;
}
.match[data-frame-id="0"] {
min-width: 200px; 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" 'new style' links */
.breadcrumbs > .write-style-link { .breadcrumbs > .write-style-link {
margin-left: 0 margin-left: 0

View File

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