226 lines
5.8 KiB
JavaScript
226 lines
5.8 KiB
JavaScript
/*
|
|
global API_METHODS cachedStyles
|
|
global getStyles filterStyles invalidateCache normalizeStyleSections
|
|
global updateIcon
|
|
*/
|
|
'use strict';
|
|
|
|
(() => {
|
|
const previewFromTabs = new Map();
|
|
|
|
/**
|
|
* When style id and state is provided, only that style is propagated.
|
|
* Otherwise all styles are replaced and the toolbar icon is updated.
|
|
* @param {Object} [msg]
|
|
* @param {{id:Number, enabled?:Boolean, sections?: (Array|String)}} [msg.style] -
|
|
* style to propagate
|
|
* @param {Boolean} [msg.codeIsUpdated]
|
|
* @returns {Promise<void>}
|
|
*/
|
|
API_METHODS.refreshAllTabs = (msg = {}) =>
|
|
Promise.all([
|
|
queryTabs(),
|
|
maybeParseUsercss(msg),
|
|
getStyles(),
|
|
]).then(([tabs, style]) =>
|
|
new Promise(resolve => {
|
|
if (style) msg.style.sections = normalizeStyleSections(style);
|
|
run(tabs, msg, resolve);
|
|
}));
|
|
|
|
|
|
function run(tabs, msg, resolve) {
|
|
const {style, codeIsUpdated} = msg;
|
|
|
|
// the style was updated/saved so we need to remove the old copy of the original style
|
|
if (msg.method === 'styleUpdated' && msg.reason !== 'editPreview') {
|
|
for (const [tabId, original] of previewFromTabs.entries()) {
|
|
if (style.id === original.id) {
|
|
previewFromTabs.delete(tabId);
|
|
}
|
|
}
|
|
if (!previewFromTabs.size) {
|
|
unregisterTabListeners();
|
|
}
|
|
}
|
|
|
|
if (!style) {
|
|
msg = {method: 'styleReplaceAll'};
|
|
|
|
// live preview puts the code in cachedStyles, saves the original in previewFromTabs,
|
|
// and if preview is being disabled, but the style is already deleted, we bail out
|
|
} else if (msg.reason === 'editPreview' && !updateCache(msg)) {
|
|
return;
|
|
|
|
// simple style update:
|
|
// * if disabled, apply.js will remove the element
|
|
// * if toggled and code is unchanged, apply.js will toggle the element
|
|
} else if (!style.enabled || codeIsUpdated === false) {
|
|
msg = {
|
|
method: 'styleUpdated',
|
|
reason: msg.reason,
|
|
style: {
|
|
id: style.id,
|
|
enabled: style.enabled,
|
|
},
|
|
codeIsUpdated,
|
|
};
|
|
|
|
// live preview normal operation, the new code is already in cachedStyles
|
|
} else {
|
|
msg.method = 'styleApply';
|
|
msg.style = {id: msg.style.id};
|
|
}
|
|
|
|
if (!tabs || !tabs.length) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const last = tabs[tabs.length - 1];
|
|
for (const tab of tabs) {
|
|
if (FIREFOX && !tab.width) continue;
|
|
chrome.webNavigation.getAllFrames({tabId: tab.id}, frames =>
|
|
refreshFrame(tab, frames, msg, tab === last && resolve));
|
|
}
|
|
}
|
|
|
|
function refreshFrame(tab, frames, msg, resolve) {
|
|
ignoreChromeError();
|
|
if (!frames || !frames.length) {
|
|
frames = [{
|
|
frameId: 0,
|
|
url: tab.url,
|
|
}];
|
|
}
|
|
msg.tabId = tab.id;
|
|
const styleId = msg.style && msg.style.id;
|
|
|
|
for (const frame of frames) {
|
|
|
|
const styles = filterStyles({
|
|
matchUrl: getFrameUrl(frame, frames),
|
|
asHash: true,
|
|
id: styleId,
|
|
});
|
|
|
|
msg = Object.assign({}, msg);
|
|
msg.frameId = frame.frameId;
|
|
|
|
if (msg.method !== 'styleUpdated') {
|
|
msg.styles = styles;
|
|
}
|
|
|
|
if (msg.method === 'styleApply' && !styles.length) {
|
|
// remove the style from a previously matching frame
|
|
invokeOrPostpone(tab.active, sendMessage, {
|
|
method: 'styleUpdated',
|
|
reason: 'editPreview',
|
|
style: {
|
|
id: styleId,
|
|
enabled: false,
|
|
},
|
|
tabId: tab.id,
|
|
frameId: frame.frameId,
|
|
}, ignoreChromeError);
|
|
} else {
|
|
invokeOrPostpone(tab.active, sendMessage, msg, ignoreChromeError);
|
|
}
|
|
|
|
if (!frame.frameId) {
|
|
setTimeout(updateIcon, 0, {
|
|
tab,
|
|
styles: msg.method === 'styleReplaceAll' ? styles : undefined,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (resolve) resolve();
|
|
}
|
|
|
|
|
|
function getFrameUrl(frame, frames) {
|
|
while (frame.url === 'about:blank' && frame.frameId > 0) {
|
|
const parent = frames.find(f => f.frameId === frame.parentFrameId);
|
|
if (!parent) break;
|
|
frame.url = parent.url;
|
|
frame = parent;
|
|
}
|
|
return (frame || frames[0]).url;
|
|
}
|
|
|
|
|
|
function maybeParseUsercss({style}) {
|
|
if (style && typeof style.sections === 'string') {
|
|
return API_METHODS.parseUsercss({sourceCode: style.sections});
|
|
}
|
|
}
|
|
|
|
|
|
function updateCache(msg) {
|
|
const {style, tabId, restoring} = msg;
|
|
const spoofed = !restoring && previewFromTabs.get(tabId);
|
|
const original = cachedStyles.byId.get(style.id);
|
|
|
|
if (style.sections && !restoring) {
|
|
if (!previewFromTabs.size) {
|
|
registerTabListeners();
|
|
}
|
|
if (!spoofed) {
|
|
previewFromTabs.set(tabId, Object.assign({}, original));
|
|
}
|
|
|
|
} else {
|
|
previewFromTabs.delete(tabId);
|
|
if (!previewFromTabs.size) {
|
|
unregisterTabListeners();
|
|
}
|
|
if (!original) {
|
|
return;
|
|
}
|
|
if (!restoring) {
|
|
msg.style = spoofed || original;
|
|
}
|
|
}
|
|
invalidateCache({updated: msg.style});
|
|
return true;
|
|
}
|
|
|
|
|
|
function registerTabListeners() {
|
|
chrome.tabs.onRemoved.addListener(onTabRemoved);
|
|
chrome.tabs.onReplaced.addListener(onTabReplaced);
|
|
chrome.webNavigation.onCommitted.addListener(onTabNavigated);
|
|
}
|
|
|
|
|
|
function unregisterTabListeners() {
|
|
chrome.tabs.onRemoved.removeListener(onTabRemoved);
|
|
chrome.tabs.onReplaced.removeListener(onTabReplaced);
|
|
chrome.webNavigation.onCommitted.removeListener(onTabNavigated);
|
|
}
|
|
|
|
|
|
function onTabRemoved(tabId) {
|
|
const style = previewFromTabs.get(tabId);
|
|
if (style) {
|
|
API_METHODS.refreshAllTabs({
|
|
style,
|
|
tabId,
|
|
reason: 'editPreview',
|
|
restoring: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
function onTabReplaced(addedTabId, removedTabId) {
|
|
onTabRemoved(removedTabId);
|
|
}
|
|
|
|
|
|
function onTabNavigated({tabId}) {
|
|
onTabRemoved(tabId);
|
|
}
|
|
})();
|