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:
parent
4bbce7cb9f
commit
8192fab1b8
|
@ -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)))
|
||||
|
|
|
@ -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?
|
||||
function updateIconBadge(tabId, count, force = true) {
|
||||
tabManager.set(tabId, 'count', count);
|
||||
refreshIconBadgeText(tabId);
|
||||
refreshIcon(tabId, force);
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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}) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 (!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 (/^\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 (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() {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
262
popup/popup.js
262
popup/popup.js
|
@ -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),
|
||||
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) {
|
||||
.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 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 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,64 +244,90 @@ 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];
|
||||
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.indexOf('.') !== -1) {
|
||||
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;
|
||||
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 (!styles.length) {
|
||||
});
|
||||
});
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
browser.tabs.onUpdated.addListener(...[
|
||||
function onUpdated(tabId, info, updatedTab) {
|
||||
if (info.url && tabId === tab.id) {
|
||||
chrome.tabs.onUpdated.removeListener(onUpdated);
|
||||
resolve(updatedTab);
|
||||
}
|
||||
}
|
||||
|
||||
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});
|
||||
},
|
||||
...'UpdateFilter' in browser.tabs ? [{tabId: tab.id}] : [],
|
||||
// TODO: remove both spreads and tabId check when strict_min_version >= 61
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user