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

This commit is contained in:
tophf 2020-02-20 18:59:23 +03:00
parent 4bbce7cb9f
commit 6c09a765e6
9 changed files with 279 additions and 166 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,11 +49,6 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
openEditor,
updateIconBadge(count) {
iconManager.updateIconBadge(this.sender.tab.id, count);
return true;
},
// 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)

View File

@ -1,4 +1,4 @@
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager */
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager API_METHODS */
/* exported iconManager */
'use strict';
@ -26,32 +26,32 @@ const iconManager = (() => {
refreshAllIcons();
});
return {updateIconBadge};
// 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);
}
Object.assign(API_METHODS, {
/** @param {(number|string)[]} styleIds */
updateIconBadge(styleIds) {
// FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
const {frameId, tab: {id: tabId}} = this.sender;
tabManager.set(tabId, 'styleIds', frameId, styleIds.length ? styleIds.map(Number) : undefined);
refreshIconBadgeText(tabId);
if (!frameId) refreshIcon(tabId, 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 +73,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())

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

@ -16,6 +16,10 @@ self.INJECTED !== 1 && (() => {
});
const initializing = init();
// if chrome.tabs is absent it's a web page, otherwise we'll check for popup/options as those aren't tabs
let isTab = !chrome.tabs;
if (chrome.tabs) chrome.tabs.getCurrent(tab => (isTab = Boolean(tab)));
// save it now because chrome.runtime will be unavailable in the orphaned script
const orphanEventId = chrome.runtime.id;
let isOrphaned;
@ -153,18 +157,11 @@ 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 (isTab) {
(STYLE_VIA_API ?
API.styleViaAPI({method: 'updateCount'}) :
API.updateIconBadge(styleInjector.list.map(style => style.id))
).catch(msg.ignoreError);
}
}

View File

@ -1,4 +1,4 @@
/* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL
/* exported getActiveTab onTabReady stringAsRegExp openURL
getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual
closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */
/* global promisify */
@ -125,18 +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

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

@ -313,6 +313,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 +525,81 @@ 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 .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,13 +1,14 @@
/* global configDialog hotkeys onTabReady msg
getActiveTab FIREFOX getTabRealURL URLS API onDOMready $ $$ prefs
getActiveTab 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 ENTRY_ID_PREFIX_RAW = 'style-';
@ -15,25 +16,21 @@ const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
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.isDupe)
.map(({url}) => 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 +80,32 @@ function toggleSideBorders(state = prefs.get('popup.borders')) {
}
}
function initTabUrls() {
return getActiveTab()
.then((tab = {}) =>
FIREFOX && tab.status === 'loading' && tab.url === 'about:blank'
? onTabReady(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 +141,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)
@ -166,24 +194,49 @@ 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[]} frames */
function sortTabFrames(frames) {
const unknown = new Map(frames.map(f => [f.frameId, f]));
const known = new Map([[0, {frameId: 0}]]);
let lastSize = -1;
while (unknown.size && unknown.size !== lastSize) {
for (const [frameId, f] of unknown) {
if (known.has(f.parentFrameId) || f.parentFrameId < 0) {
known.set(frameId, f);
unknown.delete(frameId);
}
}
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) {
f.isDupe = urls.has(f.url);
urls.add(f.url);
}
return sortedFrames;
}
/** @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 +244,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 +262,66 @@ 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;
}
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 +409,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 +461,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) {
@ -577,7 +642,7 @@ Object.assign(handleEvent, {
},
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
@ -639,33 +704,3 @@ function handleDelete(id) {
installed.appendChild(template.noStyles.cloneNode(true));
}
}
function getTabRealURLFirefox(tab) {
// wait for FF tab-on-demand to get a real URL (initially about:blank), 5 sec max
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});
});
}