faster popup load to avoid resize flicker

This commit is contained in:
tophf 2020-11-22 22:41:36 +03:00
parent 51f125113d
commit 4d198f56e2
3 changed files with 138 additions and 112 deletions

View File

@ -16,12 +16,6 @@
} }
</style> </style>
<!-- Notes:
* Chrome doesn't garbage-collect (or even leaks) SVG <symbol> referenced via <use> so we'll embed the code directly
* inter-tag whitespace in templates is automatically removed in localization.js
* i18n-anything attribute automatically creates "anything" attribute
-->
<template data-id="style"> <template data-id="style">
<div class="entry"> <div class="entry">
<div class="entry-content"> <div class="entry-content">
@ -178,33 +172,35 @@
<div class="search-result-empty"></div> <div class="search-result-empty"></div>
</template> </template>
<link rel="stylesheet" href="vendor-overwrites/colorpicker/colorpicker.css">
<script src="vendor-overwrites/colorpicker/colorconverter.js"></script>
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
<link rel="stylesheet" href="msgbox/msgbox.css">
<script src="msgbox/msgbox.js"></script>
<link rel="stylesheet" href="options/onoffswitch.css">
<link rel="stylesheet" href="manage/config-dialog.css">
<script src="manage/config-dialog.js"></script>
<script src="js/polyfill.js"></script> <script src="js/polyfill.js"></script>
<script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/localization.js"></script>
<script src="js/msg.js"></script> <script src="js/msg.js"></script>
<script src="js/messaging.js"></script>
<script src="popup/popup-preinit.js"></script>
<script src="js/dom.js"></script>
<script src="js/localization.js"></script>
<script src="js/prefs.js"></script> <script src="js/prefs.js"></script>
<script src="content/style-injector.js"></script> <script src="content/style-injector.js"></script>
<script src="content/apply.js"></script> <script src="content/apply.js"></script>
<!-- async hack to load auxiliary stylesheets later so they don't delay the UI -->
<link rel="stylesheet" href="vendor-overwrites/colorpicker/colorpicker.css" media="print">
<link rel="stylesheet" href="msgbox/msgbox.css" media="print">
<link rel="stylesheet" href="manage/config-dialog.css" media="print">
<link rel="stylesheet" href="popup/search-results.css" media="print">
<link rel="stylesheet" href="options/onoffswitch.css">
<link rel="stylesheet" href="popup/popup.css"> <link rel="stylesheet" href="popup/popup.css">
<link rel="stylesheet" href="popup/search-results.css">
<script src="popup/popup.js"></script> <script src="popup/popup.js"></script>
<script src="popup/search-results.js"></script> <script src="popup/search-results.js"></script>
<script src="popup/hotkeys.js"></script> <script src="popup/hotkeys.js"></script>
<script src="js/script-loader.js" async></script> <script src="js/script-loader.js" async></script>
<script src="js/storage-util.js" async></script> <script src="js/storage-util.js" async></script>
<script src="vendor-overwrites/colorpicker/colorconverter.js" async></script>
<script src="vendor-overwrites/colorpicker/colorpicker.js" async></script>
<script src="msgbox/msgbox.js" async></script>
<script src="manage/config-dialog.js" async></script>
</head> </head>
<body id="stylus-popup"> <body id="stylus-popup">

87
popup/popup-preinit.js Normal file
View File

@ -0,0 +1,87 @@
/* global
API
URLS
*/
'use strict';
const ABOUT_BLANK = 'about:blank';
/* exported tabURL */
/** @type string */
let tabURL;
/* exported initializing */
const initializing = (async () => {
let [tab] = await browser.tabs.query({currentWindow: true, active: true});
if (!chrome.app && tab.status === 'loading' && tab.url === 'about:blank') {
tab = await waitForTabUrlFF(tab);
}
const frames = sortTabFrames(await browser.webNavigation.getAllFrames({tabId: tab.id}));
let url = tab.pendingUrl || tab.url || ''; // new Chrome uses pendingUrl while connecting
if (url === 'chrome://newtab/' && !URLS.chromeProtectsNTP) {
url = frames[0].url || '';
}
if (!URLS.supported(url)) {
url = '';
frames.length = 1;
}
tabURL = frames[0].url = url;
const uniqFrames = frames.filter(f => f.url && !f.isDupe);
const styles = await Promise.all(uniqFrames.map(getFrameStyles));
return {frames, styles};
async function getFrameStyles({url}) {
return {
url,
styles: await getStyleDataMerged(url),
};
}
/** @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 waitForTabUrlFF(tab) {
return new Promise(resolve => {
browser.tabs.onUpdated.addListener(...[
function onUpdated(tabId, info, updatedTab) {
if (info.url && tabId === tab.id) {
browser.tabs.onUpdated.removeListener(onUpdated);
resolve(updatedTab);
}
},
...'UpdateFilter' in browser.tabs ? [{tabId: tab.id}] : [],
// TODO: remove both spreads and tabId check when strict_min_version >= 61
]);
});
}
})();
/* Merges the extra props from API into style data.
* When `id` is specified returns a single object otherwise an array */
async function getStyleDataMerged(url, id) {
const styles = (await API.getStylesByUrl(url, id))
.map(r => Object.assign(r.data, r));
return id ? styles[0] : styles;
}

View File

@ -3,18 +3,22 @@
$$ $$
$create $create
animateElement animateElement
ABOUT_BLANK
API API
CHROME CHROME
CHROME_HAS_BORDER_BUG CHROME_HAS_BORDER_BUG
configDialog configDialog
FIREFOX FIREFOX
getActiveTab getActiveTab
getStyleDataMerged
hotkeys hotkeys
initializing
msg msg
onDOMready onDOMready
prefs prefs
setupLivePrefs setupLivePrefs
t t
tabURL
tryJSONparse tryJSONparse
URLS URLS
*/ */
@ -23,11 +27,8 @@
/** @type Element */ /** @type Element */
let installed; let installed;
/** @type string */
let tabURL;
const handleEvent = {}; const handleEvent = {};
const ABOUT_BLANK = 'about:blank';
const ENTRY_ID_PREFIX_RAW = 'style-'; const ENTRY_ID_PREFIX_RAW = 'style-';
$.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`); $.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`);
@ -38,28 +39,21 @@ if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.co
toggleSideBorders(); toggleSideBorders();
initTabUrls() Promise.all([
.then(frames => initializing,
Promise.all([ onDOMready(),
onDOMready().then(() => initPopup(frames)), ]).then(([
...frames {frames, styles},
.filter(f => f.url && !f.isDupe) ]) => {
.map(({url}) => getStyleDataMerged(url).then(styles => ({styles, url}))), toggleUiSliders();
])) initPopup(frames);
.then(([, ...results]) => { if (styles[0]) {
const sliders = prefs.get('ui.sliders'); showStyles(styles);
const slot = $('toggle', t.template.style); } else {
const toggle = t.template[sliders ? 'toggleSlider' : 'toggleChecker']; // unsupported URL;
slot.parentElement.replaceChild(toggle.cloneNode(true), slot); $('#popup-manage-button').removeAttribute('title');
document.body.classList.toggle('has-sliders', sliders); }
if (results[0]) { });
showStyles(results);
} else {
// unsupported URL;
$('#popup-manage-button').removeAttribute('title');
}
})
.catch(console.error);
msg.onExtension(onRuntimeMessage); msg.onExtension(onRuntimeMessage);
@ -109,23 +103,12 @@ function toggleSideBorders(state = prefs.get('popup.borders')) {
} }
} }
async function initTabUrls() { function toggleUiSliders() {
let tab = await getActiveTab(); const sliders = prefs.get('ui.sliders');
if (FIREFOX && tab.status === 'loading' && tab.url === ABOUT_BLANK) { const slot = $('toggle', t.template.style);
tab = await waitForTabUrlFF(tab); const toggle = t.template[sliders ? 'toggleSlider' : 'toggleChecker'];
} slot.parentElement.replaceChild(toggle.cloneNode(true), slot);
let frames = await browser.webNavigation.getAllFrames({tabId: tab.id}); document.body.classList.toggle('has-sliders', sliders);
let url = tab.pendingUrl || tab.url || ''; // new Chrome uses pendingUrl while connecting
frames = sortTabFrames(frames);
if (url === 'chrome://newtab/' && !URLS.chromeProtectsNTP) {
url = frames[0].url || '';
}
if (!URLS.supported(url)) {
url = '';
frames.length = 1;
}
tabURL = frames[0].url = url;
return frames;
} }
/** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */ /** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */
@ -158,6 +141,10 @@ async function initPopup(frames) {
installed); installed);
} }
for (const el of $$('link[media=print]')) {
el.removeAttribute('media');
}
if (!tabURL) { if (!tabURL) {
blockPopup(); blockPopup();
return; return;
@ -188,6 +175,11 @@ async function initPopup(frames) {
// 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
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
} }
initUnreachable(isStore);
}
function initUnreachable(isStore) {
const info = t.template.unreachableInfo; const info = t.template.unreachableInfo;
if (!FIREFOX) { if (!FIREFOX) {
// Chrome "Allow access to file URLs" in chrome://extensions message // Chrome "Allow access to file URLs" in chrome://extensions message
@ -294,32 +286,6 @@ function getDomains(url) {
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');
return entries.sort(({styleMeta: a}, {styleMeta: b}) => return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
@ -697,29 +663,6 @@ function handleDelete(id) {
} }
} }
function waitForTabUrlFF(tab) {
return new Promise(resolve => {
browser.tabs.onUpdated.addListener(...[
function onUpdated(tabId, info, updatedTab) {
if (info.url && tabId === tab.id) {
chrome.tabs.onUpdated.removeListener(onUpdated);
resolve(updatedTab);
}
},
...'UpdateFilter' in browser.tabs ? [{tabId: tab.id}] : [],
// TODO: remove both spreads and tabId check when strict_min_version >= 61
]);
});
}
/* Merges the extra props from API into style data.
* When `id` is specified returns a single object otherwise an array */
async function getStyleDataMerged(url, id) {
const styles = (await API.getStylesByUrl(url, id))
.map(r => Object.assign(r.data, r));
return id ? styles[0] : styles;
}
function blockPopup(isBlocked = true) { function blockPopup(isBlocked = true) {
document.body.classList.toggle('blocked', isBlocked); document.body.classList.toggle('blocked', isBlocked);
if (isBlocked) { if (isBlocked) {