simplify orphanCheck; use IIFE; fix comm issues
This commit is contained in:
parent
babeb695c1
commit
fb3554a351
909
content/apply.js
909
content/apply.js
|
@ -1,499 +1,498 @@
|
||||||
/* eslint no-var: 0 */
|
/* eslint no-var: 0 */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var ID_PREFIX = 'stylus-';
|
(() => {
|
||||||
var ROOT = document.documentElement;
|
var ID_PREFIX = 'stylus-';
|
||||||
var isOwnPage = location.protocol.endsWith('-extension:');
|
var ROOT = document.documentElement;
|
||||||
var disableAll = false;
|
var isOwnPage = location.protocol.endsWith('-extension:');
|
||||||
var exposeIframes = false;
|
var disableAll = false;
|
||||||
var styleElements = new Map();
|
var exposeIframes = false;
|
||||||
var disabledElements = new Map();
|
var styleElements = new Map();
|
||||||
var retiredStyleTimers = new Map();
|
var disabledElements = new Map();
|
||||||
var docRewriteObserver;
|
var retiredStyleTimers = new Map();
|
||||||
var docRootObserver;
|
var docRewriteObserver;
|
||||||
|
var docRootObserver;
|
||||||
|
|
||||||
requestStyles();
|
requestStyles();
|
||||||
chrome.runtime.onMessage.addListener(applyOnMessage);
|
chrome.runtime.onMessage.addListener(applyOnMessage);
|
||||||
|
window.applyOnMessage = applyOnMessage;
|
||||||
|
|
||||||
if (!isOwnPage) {
|
if (!isOwnPage) {
|
||||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id));
|
window.dispatchEvent(new CustomEvent(chrome.runtime.id));
|
||||||
window.addEventListener(chrome.runtime.id, orphanCheck, true);
|
window.addEventListener(chrome.runtime.id, orphanCheck, true);
|
||||||
}
|
|
||||||
|
|
||||||
function requestStyles(options, callback = applyStyles) {
|
|
||||||
if (!chrome.app && document instanceof XMLDocument) {
|
|
||||||
chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var matchUrl = location.href;
|
|
||||||
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
|
|
||||||
// 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) {
|
|
||||||
matchUrl = parent.location.href;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
const request = Object.assign({
|
|
||||||
method: 'getStyles',
|
|
||||||
matchUrl,
|
|
||||||
enabled: true,
|
|
||||||
asHash: true,
|
|
||||||
}, options);
|
|
||||||
// On own pages we request the styles directly to minimize delay and flicker
|
|
||||||
if (typeof getStylesSafe === 'function') {
|
|
||||||
getStylesSafe(request).then(callback);
|
|
||||||
} else {
|
|
||||||
chrome.runtime.sendMessage(request, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function applyOnMessage(request, sender, sendResponse) {
|
|
||||||
if (request.styles === 'DIY') {
|
|
||||||
// Do-It-Yourself tells our built-in pages to fetch the styles directly
|
|
||||||
// which is faster because IPC messaging JSON-ifies everything internally
|
|
||||||
requestStyles({}, styles => {
|
|
||||||
request.styles = styles;
|
|
||||||
applyOnMessage(request);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chrome.app && document instanceof XMLDocument && request.method !== 'ping') {
|
function requestStyles(options, callback = applyStyles) {
|
||||||
request.action = request.method;
|
if (!chrome.app && document instanceof XMLDocument) {
|
||||||
request.method = 'styleViaAPI';
|
chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'});
|
||||||
request.styles = null;
|
return;
|
||||||
if (request.style) {
|
}
|
||||||
request.style.sections = null;
|
var matchUrl = location.href;
|
||||||
|
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
|
||||||
|
// 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) {
|
||||||
|
matchUrl = parent.location.href;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
const request = Object.assign({
|
||||||
|
method: 'getStyles',
|
||||||
|
matchUrl,
|
||||||
|
enabled: true,
|
||||||
|
asHash: true,
|
||||||
|
}, options);
|
||||||
|
// On own pages we request the styles directly to minimize delay and flicker
|
||||||
|
if (typeof getStylesSafe === 'function') {
|
||||||
|
getStylesSafe(request).then(callback);
|
||||||
|
} else {
|
||||||
|
chrome.runtime.sendMessage(request, callback);
|
||||||
}
|
}
|
||||||
chrome.runtime.sendMessage(request);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (request.method) {
|
|
||||||
case 'styleDeleted':
|
|
||||||
removeStyle(request);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'styleUpdated':
|
function applyOnMessage(request, sender, sendResponse) {
|
||||||
if (request.codeIsUpdated === false) {
|
if (request.styles === 'DIY') {
|
||||||
applyStyleState(request.style);
|
// Do-It-Yourself tells our built-in pages to fetch the styles directly
|
||||||
|
// which is faster because IPC messaging JSON-ifies everything internally
|
||||||
|
requestStyles({}, styles => {
|
||||||
|
request.styles = styles;
|
||||||
|
applyOnMessage(request);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chrome.app && document instanceof XMLDocument && request.method !== 'ping') {
|
||||||
|
request.action = request.method;
|
||||||
|
request.method = 'styleViaAPI';
|
||||||
|
request.styles = null;
|
||||||
|
if (request.style) {
|
||||||
|
request.style.sections = null;
|
||||||
|
}
|
||||||
|
chrome.runtime.sendMessage(request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (request.method) {
|
||||||
|
case 'styleDeleted':
|
||||||
|
removeStyle(request);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'styleUpdated':
|
||||||
|
if (request.codeIsUpdated === false) {
|
||||||
|
applyStyleState(request.style);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (request.style.enabled) {
|
||||||
|
removeStyle({id: request.style.id, retire: true});
|
||||||
|
requestStyles({id: request.style.id});
|
||||||
|
} else {
|
||||||
|
removeStyle(request.style);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'styleAdded':
|
||||||
|
if (request.style.enabled) {
|
||||||
|
requestStyles({id: request.style.id});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'styleApply':
|
||||||
|
applyStyles(request.styles);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'styleReplaceAll':
|
||||||
|
replaceAll(request.styles);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'prefChanged':
|
||||||
|
if ('disableAll' in request.prefs) {
|
||||||
|
doDisableAll(request.prefs.disableAll);
|
||||||
|
}
|
||||||
|
if ('exposeIframes' in request.prefs) {
|
||||||
|
doExposeIframes(request.prefs.exposeIframes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ping':
|
||||||
|
sendResponse(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function doDisableAll(disable = disableAll) {
|
||||||
|
if (!disable === !disableAll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
disableAll = disable;
|
||||||
|
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
|
||||||
|
if (stylesheet.ownerNode.matches(`style.stylus[id^="${ID_PREFIX}"]`)
|
||||||
|
&& stylesheet.disabled !== disable) {
|
||||||
|
stylesheet.disabled = disable;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function doExposeIframes(state = exposeIframes) {
|
||||||
|
if (state === exposeIframes || window === parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exposeIframes = state;
|
||||||
|
const attr = document.documentElement.getAttribute('stylus-iframe');
|
||||||
|
if (state && attr !== '') {
|
||||||
|
document.documentElement.setAttribute('stylus-iframe', '');
|
||||||
|
} else if (!state && attr === '') {
|
||||||
|
document.documentElement.removeAttribute('stylus-iframe');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function applyStyleState({id, enabled}) {
|
||||||
|
const inCache = disabledElements.get(id) || styleElements.get(id);
|
||||||
|
const inDoc = document.getElementById(ID_PREFIX + id);
|
||||||
|
if (enabled) {
|
||||||
|
if (inDoc) {
|
||||||
|
return;
|
||||||
|
} else if (inCache) {
|
||||||
|
addStyleElement(inCache);
|
||||||
|
disabledElements.delete(id);
|
||||||
|
} else {
|
||||||
|
requestStyles({id});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (inDoc) {
|
||||||
|
disabledElements.set(id, inDoc);
|
||||||
|
docRootObserver.stop();
|
||||||
|
inDoc.remove();
|
||||||
|
docRootObserver.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function removeStyle({id, retire = false}) {
|
||||||
|
const el = document.getElementById(ID_PREFIX + id);
|
||||||
|
if (el) {
|
||||||
|
if (retire) {
|
||||||
|
// to avoid page flicker when the style is updated
|
||||||
|
// instead of removing it immediately we rename its ID and queue it
|
||||||
|
// to be deleted in applyStyles after a new version is fetched and applied
|
||||||
|
const deadID = 'ghost-' + id;
|
||||||
|
el.id = ID_PREFIX + deadID;
|
||||||
|
// in case something went wrong and new style was never applied
|
||||||
|
retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
|
||||||
|
} else {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styleElements.delete(ID_PREFIX + id);
|
||||||
|
disabledElements.delete(id);
|
||||||
|
retiredStyleTimers.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function applyStyles(styles) {
|
||||||
|
if (!styles) {
|
||||||
|
// Chrome is starting up
|
||||||
|
requestStyles();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ('disableAll' in styles) {
|
||||||
|
doDisableAll(styles.disableAll);
|
||||||
|
delete styles.disableAll;
|
||||||
|
}
|
||||||
|
if ('exposeIframes' in styles) {
|
||||||
|
doExposeIframes(styles.exposeIframes);
|
||||||
|
delete styles.exposeIframes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gotNewStyles = Object.keys(styles).length || styles.needTransitionPatch;
|
||||||
|
if (gotNewStyles) {
|
||||||
|
if (docRootObserver) {
|
||||||
|
docRootObserver.stop();
|
||||||
|
} else {
|
||||||
|
initDocRootObserver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (styles.needTransitionPatch) {
|
||||||
|
// CSS transition bug workaround: since we insert styles asynchronously,
|
||||||
|
// the browsers, especially Firefox, may apply all transitions on page load
|
||||||
|
delete styles.needTransitionPatch;
|
||||||
|
const className = chrome.runtime.id + '-transition-bug-fix';
|
||||||
|
const docId = document.documentElement.id ? '#' + document.documentElement.id : '';
|
||||||
|
document.documentElement.classList.add(className);
|
||||||
|
applySections(0, `
|
||||||
|
${docId}.${className}:root * {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
setTimeout(() => {
|
||||||
|
removeStyle({id: 0});
|
||||||
|
document.documentElement.classList.remove(className);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gotNewStyles) {
|
||||||
|
for (const id in styles) {
|
||||||
|
applySections(id, styles[id].map(section => section.code).join('\n'));
|
||||||
|
}
|
||||||
|
docRootObserver.start({sort: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOwnPage && !docRewriteObserver && styleElements.size) {
|
||||||
|
initDocRewriteObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retiredStyleTimers.size) {
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const [id, timer] of retiredStyleTimers.entries()) {
|
||||||
|
removeStyle({id});
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function applySections(styleId, code) {
|
||||||
|
const id = ID_PREFIX + styleId;
|
||||||
|
let el = styleElements.get(id) || document.getElementById(id);
|
||||||
|
if (!el) {
|
||||||
|
if (document.documentElement instanceof SVGSVGElement) {
|
||||||
|
// SVG document style
|
||||||
|
el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
||||||
|
} else if (document instanceof XMLDocument) {
|
||||||
|
// XML document style
|
||||||
|
el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style');
|
||||||
|
} else {
|
||||||
|
// HTML document style; also works on HTML-embedded SVG
|
||||||
|
el = document.createElement('style');
|
||||||
|
}
|
||||||
|
Object.assign(el, {
|
||||||
|
id,
|
||||||
|
type: 'text/css',
|
||||||
|
textContent: code,
|
||||||
|
});
|
||||||
|
// SVG className is not a string, but an instance of SVGAnimatedString
|
||||||
|
el.classList.add('stylus');
|
||||||
|
addStyleElement(el);
|
||||||
|
}
|
||||||
|
styleElements.set(id, el);
|
||||||
|
disabledElements.delete(Number(styleId));
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addStyleElement(newElement) {
|
||||||
|
if (!ROOT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let next;
|
||||||
|
const newStyleId = getStyleId(newElement);
|
||||||
|
for (const el of styleElements.values()) {
|
||||||
|
if (el.parentNode && !el.id.endsWith('-ghost') && getStyleId(el) > newStyleId) {
|
||||||
|
next = el.parentNode === ROOT ? el : null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (request.style.enabled) {
|
|
||||||
removeStyle({id: request.style.id, retire: true});
|
|
||||||
requestStyles({id: request.style.id});
|
|
||||||
} else {
|
|
||||||
removeStyle(request.style);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'styleAdded':
|
|
||||||
if (request.style.enabled) {
|
|
||||||
requestStyles({id: request.style.id});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'styleApply':
|
|
||||||
applyStyles(request.styles);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'styleReplaceAll':
|
|
||||||
replaceAll(request.styles);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'prefChanged':
|
|
||||||
if ('disableAll' in request.prefs) {
|
|
||||||
doDisableAll(request.prefs.disableAll);
|
|
||||||
}
|
|
||||||
if ('exposeIframes' in request.prefs) {
|
|
||||||
doExposeIframes(request.prefs.exposeIframes);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ping':
|
|
||||||
sendResponse(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function doDisableAll(disable = disableAll) {
|
|
||||||
if (!disable === !disableAll) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
disableAll = disable;
|
|
||||||
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
|
|
||||||
if (stylesheet.ownerNode.matches(`style.stylus[id^="${ID_PREFIX}"]`)
|
|
||||||
&& stylesheet.disabled !== disable) {
|
|
||||||
stylesheet.disabled = disable;
|
|
||||||
}
|
}
|
||||||
});
|
if (next === newElement.nextElementSibling) {
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function doExposeIframes(state = exposeIframes) {
|
|
||||||
if (state === exposeIframes || window === parent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
exposeIframes = state;
|
|
||||||
const attr = document.documentElement.getAttribute('stylus-iframe');
|
|
||||||
if (state && attr !== '') {
|
|
||||||
document.documentElement.setAttribute('stylus-iframe', '');
|
|
||||||
} else if (!state && attr === '') {
|
|
||||||
document.documentElement.removeAttribute('stylus-iframe');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function applyStyleState({id, enabled}) {
|
|
||||||
const inCache = disabledElements.get(id) || styleElements.get(id);
|
|
||||||
const inDoc = document.getElementById(ID_PREFIX + id);
|
|
||||||
if (enabled) {
|
|
||||||
if (inDoc) {
|
|
||||||
return;
|
|
||||||
} else if (inCache) {
|
|
||||||
addStyleElement(inCache);
|
|
||||||
disabledElements.delete(id);
|
|
||||||
} else {
|
|
||||||
requestStyles({id});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (inDoc) {
|
|
||||||
disabledElements.set(id, inDoc);
|
|
||||||
docRootObserver.stop();
|
|
||||||
inDoc.remove();
|
|
||||||
docRootObserver.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function removeStyle({id, retire = false}) {
|
|
||||||
const el = document.getElementById(ID_PREFIX + id);
|
|
||||||
if (el) {
|
|
||||||
if (retire) {
|
|
||||||
// to avoid page flicker when the style is updated
|
|
||||||
// instead of removing it immediately we rename its ID and queue it
|
|
||||||
// to be deleted in applyStyles after a new version is fetched and applied
|
|
||||||
const deadID = 'ghost-' + id;
|
|
||||||
el.id = ID_PREFIX + deadID;
|
|
||||||
// in case something went wrong and new style was never applied
|
|
||||||
retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
|
|
||||||
} else {
|
|
||||||
el.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
styleElements.delete(ID_PREFIX + id);
|
|
||||||
disabledElements.delete(id);
|
|
||||||
retiredStyleTimers.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function applyStyles(styles) {
|
|
||||||
if (!styles) {
|
|
||||||
// Chrome is starting up
|
|
||||||
requestStyles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ('disableAll' in styles) {
|
|
||||||
doDisableAll(styles.disableAll);
|
|
||||||
delete styles.disableAll;
|
|
||||||
}
|
|
||||||
if ('exposeIframes' in styles) {
|
|
||||||
doExposeIframes(styles.exposeIframes);
|
|
||||||
delete styles.exposeIframes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gotNewStyles = Object.keys(styles).length || styles.needTransitionPatch;
|
|
||||||
if (gotNewStyles) {
|
|
||||||
if (docRootObserver) {
|
|
||||||
docRootObserver.stop();
|
|
||||||
} else {
|
|
||||||
initDocRootObserver();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (styles.needTransitionPatch) {
|
|
||||||
// CSS transition bug workaround: since we insert styles asynchronously,
|
|
||||||
// the browsers, especially Firefox, may apply all transitions on page load
|
|
||||||
delete styles.needTransitionPatch;
|
|
||||||
const className = chrome.runtime.id + '-transition-bug-fix';
|
|
||||||
const docId = document.documentElement.id ? '#' + document.documentElement.id : '';
|
|
||||||
document.documentElement.classList.add(className);
|
|
||||||
applySections(0, `
|
|
||||||
${docId}.${className}:root * {
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
setTimeout(() => {
|
|
||||||
removeStyle({id: 0});
|
|
||||||
document.documentElement.classList.remove(className);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gotNewStyles) {
|
|
||||||
for (const id in styles) {
|
|
||||||
applySections(id, styles[id].map(section => section.code).join('\n'));
|
|
||||||
}
|
|
||||||
docRootObserver.start({sort: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOwnPage && !docRewriteObserver && styleElements.size) {
|
|
||||||
initDocRewriteObserver();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retiredStyleTimers.size) {
|
|
||||||
setTimeout(() => {
|
|
||||||
for (const [id, timer] of retiredStyleTimers.entries()) {
|
|
||||||
removeStyle({id});
|
|
||||||
clearTimeout(timer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function applySections(styleId, code) {
|
|
||||||
let el = document.getElementById(ID_PREFIX + styleId);
|
|
||||||
if (el) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (document.documentElement instanceof SVGSVGElement) {
|
|
||||||
// SVG document style
|
|
||||||
el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
|
||||||
} else if (document instanceof XMLDocument) {
|
|
||||||
// XML document style
|
|
||||||
el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style');
|
|
||||||
} else {
|
|
||||||
// HTML document style; also works on HTML-embedded SVG
|
|
||||||
el = document.createElement('style');
|
|
||||||
}
|
|
||||||
Object.assign(el, {
|
|
||||||
styleId,
|
|
||||||
id: ID_PREFIX + styleId,
|
|
||||||
type: 'text/css',
|
|
||||||
textContent: code,
|
|
||||||
});
|
|
||||||
// SVG className is not a string, but an instance of SVGAnimatedString
|
|
||||||
el.classList.add('stylus');
|
|
||||||
addStyleElement(el);
|
|
||||||
styleElements.set(el.id, el);
|
|
||||||
disabledElements.delete(Number(styleId));
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function addStyleElement(newElement) {
|
|
||||||
if (!ROOT) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let next;
|
|
||||||
const newStyleId = getStyleId(newElement);
|
|
||||||
for (const el of styleElements.values()) {
|
|
||||||
if (el.parentNode && !el.id.endsWith('-ghost') && getStyleId(el) > newStyleId) {
|
|
||||||
next = el.parentNode === ROOT ? el : null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (next === newElement.nextElementSibling) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
docRootObserver.stop();
|
|
||||||
ROOT.insertBefore(newElement, next || null);
|
|
||||||
if (disableAll) {
|
|
||||||
newElement.disabled = true;
|
|
||||||
}
|
|
||||||
docRootObserver.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function replaceAll(newStyles) {
|
|
||||||
const oldStyles = Array.prototype.slice.call(
|
|
||||||
document.querySelectorAll(`style.stylus[id^="${ID_PREFIX}"]`));
|
|
||||||
oldStyles.forEach(el => (el.id += '-ghost'));
|
|
||||||
styleElements.clear();
|
|
||||||
disabledElements.clear();
|
|
||||||
[...retiredStyleTimers.values()].forEach(clearTimeout);
|
|
||||||
retiredStyleTimers.clear();
|
|
||||||
applyStyles(newStyles);
|
|
||||||
oldStyles.forEach(el => el.remove());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function initDocRewriteObserver() {
|
|
||||||
// detect documentElement being rewritten from inside the script
|
|
||||||
docRewriteObserver = new MutationObserver(mutations => {
|
|
||||||
for (let m = mutations.length; --m >= 0;) {
|
|
||||||
const added = mutations[m].addedNodes;
|
|
||||||
for (let n = added.length; --n >= 0;) {
|
|
||||||
if (added[n].localName === 'html') {
|
|
||||||
reinjectStyles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
docRewriteObserver.observe(document, {childList: true});
|
|
||||||
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
|
|
||||||
setTimeout(() => {
|
|
||||||
if (document.documentElement !== ROOT) {
|
|
||||||
reinjectStyles();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// re-add styles if we detect documentElement being recreated
|
|
||||||
function reinjectStyles() {
|
|
||||||
if (!styleElements) {
|
|
||||||
if (orphanCheck) {
|
|
||||||
orphanCheck();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ROOT = document.documentElement;
|
|
||||||
docRootObserver.stop();
|
docRootObserver.stop();
|
||||||
const imported = [];
|
ROOT.insertBefore(newElement, next || null);
|
||||||
for (const [id, el] of styleElements.entries()) {
|
if (disableAll) {
|
||||||
const copy = document.importNode(el, true);
|
newElement.disabled = true;
|
||||||
el.textContent += ' '; // invalidate CSSOM cache
|
|
||||||
imported.push([id, copy]);
|
|
||||||
addStyleElement(copy);
|
|
||||||
}
|
}
|
||||||
docRootObserver.start();
|
docRootObserver.start();
|
||||||
styleElements = new Map(imported);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function initDocRootObserver() {
|
function replaceAll(newStyles) {
|
||||||
let lastRestorationTime = 0;
|
const oldStyles = Array.prototype.slice.call(
|
||||||
let restorationCounter = 0;
|
document.querySelectorAll(`style.stylus[id^="${ID_PREFIX}"]`));
|
||||||
let observing = false;
|
oldStyles.forEach(el => (el.id += '-ghost'));
|
||||||
|
styleElements.clear();
|
||||||
|
disabledElements.clear();
|
||||||
|
[...retiredStyleTimers.values()].forEach(clearTimeout);
|
||||||
|
retiredStyleTimers.clear();
|
||||||
|
applyStyles(newStyles);
|
||||||
|
oldStyles.forEach(el => el.remove());
|
||||||
|
}
|
||||||
|
|
||||||
docRootObserver = Object.assign(new MutationObserver(sortStyleElements), {
|
|
||||||
start({sort = false} = {}) {
|
function getStyleId(el) {
|
||||||
|
return parseInt(el.id.substr(ID_PREFIX.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function orphanCheck() {
|
||||||
|
if (chrome.i18n && chrome.i18n.getUILanguage()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// In Chrome content script is orphaned on an extension update/reload
|
||||||
|
// so we need to detach event listeners
|
||||||
|
[docRewriteObserver, docRootObserver].forEach(ob => ob && ob.takeRecords() && ob.disconnect());
|
||||||
|
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function initDocRewriteObserver() {
|
||||||
|
// detect documentElement being rewritten from inside the script
|
||||||
|
docRewriteObserver = new MutationObserver(mutations => {
|
||||||
|
for (let m = mutations.length; --m >= 0;) {
|
||||||
|
const added = mutations[m].addedNodes;
|
||||||
|
for (let n = added.length; --n >= 0;) {
|
||||||
|
if (added[n].localName === 'html') {
|
||||||
|
reinjectStyles();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
docRewriteObserver.observe(document, {childList: true});
|
||||||
|
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.documentElement !== ROOT) {
|
||||||
|
reinjectStyles();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// re-add styles if we detect documentElement being recreated
|
||||||
|
function reinjectStyles() {
|
||||||
|
if (!styleElements) {
|
||||||
|
orphanCheck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ROOT = document.documentElement;
|
||||||
|
docRootObserver.stop();
|
||||||
|
const imported = [];
|
||||||
|
for (const [id, el] of styleElements.entries()) {
|
||||||
|
const copy = document.importNode(el, true);
|
||||||
|
el.textContent += ' '; // invalidate CSSOM cache
|
||||||
|
imported.push([id, copy]);
|
||||||
|
addStyleElement(copy);
|
||||||
|
}
|
||||||
|
docRootObserver.start();
|
||||||
|
styleElements = new Map(imported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function initDocRootObserver() {
|
||||||
|
let lastRestorationTime = 0;
|
||||||
|
let restorationCounter = 0;
|
||||||
|
let observing = false;
|
||||||
|
let sorting = false;
|
||||||
|
// allow any types of elements between ours, except for the following:
|
||||||
|
const ORDERED_TAGS = ['head', 'body', 'style', 'link'];
|
||||||
|
|
||||||
|
init();
|
||||||
|
return;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
docRootObserver = new MutationObserver(sortStyleElements);
|
||||||
|
Object.assign(docRootObserver, {start, stop});
|
||||||
|
}
|
||||||
|
function start({sort = false} = {}) {
|
||||||
if (sort && sortStyleMap()) {
|
if (sort && sortStyleMap()) {
|
||||||
sortStyleElements();
|
sortStyleElements();
|
||||||
}
|
}
|
||||||
if (!observing) {
|
if (!observing && ROOT) {
|
||||||
this.observe(ROOT, {childList: true});
|
docRootObserver.observe(ROOT, {childList: true});
|
||||||
observing = true;
|
observing = true;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
stop() {
|
function stop() {
|
||||||
if (observing) {
|
if (observing) {
|
||||||
this.disconnect();
|
docRootObserver.disconnect();
|
||||||
observing = false;
|
observing = false;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
|
|
||||||
function sortStyleMap() {
|
|
||||||
const list = [];
|
|
||||||
let prevStyleId = 0;
|
|
||||||
let needsSorting = false;
|
|
||||||
for (const entry of styleElements.entries()) {
|
|
||||||
list.push(entry);
|
|
||||||
const el = entry[1];
|
|
||||||
const styleId = getStyleId(el);
|
|
||||||
el.styleId = styleId;
|
|
||||||
needsSorting |= styleId < prevStyleId;
|
|
||||||
prevStyleId = styleId;
|
|
||||||
}
|
}
|
||||||
if (needsSorting) {
|
function sortStyleMap() {
|
||||||
styleElements = new Map(list.sort((a, b) => a[1].styleId - b[1].styleId));
|
const list = [];
|
||||||
return true;
|
let prevStyleId = 0;
|
||||||
}
|
let needsSorting = false;
|
||||||
}
|
for (const entry of styleElements.entries()) {
|
||||||
|
list.push(entry);
|
||||||
function sortStyleElements() {
|
const el = entry[1];
|
||||||
let prev = document.body || document.head;
|
const styleId = getStyleId(el);
|
||||||
if (!prev) {
|
el.styleId = styleId;
|
||||||
return;
|
needsSorting |= styleId < prevStyleId;
|
||||||
}
|
prevStyleId = styleId;
|
||||||
let appliedChanges = false;
|
|
||||||
for (const [idStr, el] of styleElements.entries()) {
|
|
||||||
if (!el.parentNode && disabledElements.has(getStyleId(idStr))) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
if (el.previousElementSibling === prev) {
|
if (needsSorting) {
|
||||||
prev = el;
|
styleElements = new Map(list.sort((a, b) => a[1].styleId - b[1].styleId));
|
||||||
continue;
|
return true;
|
||||||
}
|
}
|
||||||
if (!appliedChanges) {
|
}
|
||||||
if (restorationLimitExceeded()) {
|
function sortStyleElements() {
|
||||||
return;
|
let expected = document.body || document.head;
|
||||||
|
if (!expected || sorting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const el of styleElements.values()) {
|
||||||
|
if (!isMovable(el)) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
appliedChanges = true;
|
let prev = el.previousElementSibling;
|
||||||
|
while (prev !== expected) {
|
||||||
|
if (prev && isSkippable(prev)) {
|
||||||
|
expected = prev;
|
||||||
|
prev = prev.nextElementSibling;
|
||||||
|
} else if (!moveAfter(el, expected)) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expected = el;
|
||||||
|
}
|
||||||
|
if (sorting) {
|
||||||
|
sorting = false;
|
||||||
|
docRootObserver.takeRecords();
|
||||||
|
setTimeout(start);
|
||||||
|
//docRootObserver.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function isMovable(el) {
|
||||||
|
return el.parentNode || !disabledElements.has(getStyleId(el));
|
||||||
|
}
|
||||||
|
function isSkippable(el) {
|
||||||
|
return !ORDERED_TAGS.includes(el.localName) ||
|
||||||
|
el.id.startsWith(ID_PREFIX) &&
|
||||||
|
el.id.endsWith('-ghost') &&
|
||||||
|
el.localName === 'style' &&
|
||||||
|
el.className === 'stylus';
|
||||||
|
}
|
||||||
|
function moveAfter(el, expected) {
|
||||||
|
if (!sorting) {
|
||||||
|
if (restorationLimitExceeded()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sorting = true;
|
||||||
docRootObserver.stop();
|
docRootObserver.stop();
|
||||||
}
|
}
|
||||||
prev.insertAdjacentElement('afterend', el);
|
expected.insertAdjacentElement('afterend', el);
|
||||||
if (el.disabled !== disableAll) {
|
if (el.disabled !== disableAll) {
|
||||||
// moving an element resets its 'disabled' state
|
// moving an element resets its 'disabled' state
|
||||||
el.disabled = disableAll;
|
el.disabled = disableAll;
|
||||||
}
|
}
|
||||||
prev = el;
|
|
||||||
}
|
|
||||||
if (appliedChanges) {
|
|
||||||
docRootObserver.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function restorationLimitExceeded() {
|
|
||||||
const t = performance.now();
|
|
||||||
if (t - lastRestorationTime > 1000) {
|
|
||||||
restorationCounter = 0;
|
|
||||||
}
|
|
||||||
lastRestorationTime = t;
|
|
||||||
if (++restorationCounter > 100) {
|
|
||||||
console.error('Stylus stopped restoring userstyle elements after 100 failed attempts.\n' +
|
|
||||||
'Please report on https://github.com/openstyles/stylus/issues');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
function restorationLimitExceeded() {
|
||||||
|
const t = performance.now();
|
||||||
|
if (t - lastRestorationTime > 1000) {
|
||||||
|
restorationCounter = 0;
|
||||||
|
}
|
||||||
|
lastRestorationTime = t;
|
||||||
|
if (++restorationCounter > 100) {
|
||||||
|
console.error('Stylus stopped restoring userstyle elements after 100 failed attempts.\n' +
|
||||||
|
'Please report on https://github.com/openstyles/stylus/issues');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})();
|
||||||
|
|
||||||
|
|
||||||
function getStyleId(el) {
|
|
||||||
return parseInt((el.id || el).substr(ID_PREFIX.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function orphanCheck() {
|
|
||||||
const port = chrome.runtime.connect();
|
|
||||||
if (port) {
|
|
||||||
port.disconnect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we're orphaned due to an extension update
|
|
||||||
// we can detach the mutation observer
|
|
||||||
[docRewriteObserver, docRootObserver].forEach(ob => ob && ob.disconnect());
|
|
||||||
// we can detach event listeners
|
|
||||||
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
|
|
||||||
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
|
||||||
// we can destroy our globals in this context to free up memory
|
|
||||||
[ // functions
|
|
||||||
'addStyleElement',
|
|
||||||
'applyOnMessage',
|
|
||||||
'applySections',
|
|
||||||
'applyStyles',
|
|
||||||
'applyStyleState',
|
|
||||||
'doDisableAll',
|
|
||||||
'initDocRewriteObserver',
|
|
||||||
'initDocRootObserver',
|
|
||||||
'orphanCheck',
|
|
||||||
'removeStyle',
|
|
||||||
'replaceAll',
|
|
||||||
'requestStyles',
|
|
||||||
// variables
|
|
||||||
'ROOT',
|
|
||||||
'disabledElements',
|
|
||||||
'retiredStyleTimers',
|
|
||||||
'styleElements',
|
|
||||||
'docRewriteObserver',
|
|
||||||
'docRootObserver',
|
|
||||||
].forEach(fn => (window[fn] = null));
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,28 +1,283 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const FIREFOX = !chrome.app;
|
(() => {
|
||||||
const VIVALDI = chrome.app && /Vivaldi/.test(navigator.userAgent);
|
const FIREFOX = !chrome.app;
|
||||||
const OPERA = chrome.app && /OPR/.test(navigator.userAgent);
|
const VIVALDI = chrome.app && /Vivaldi/.test(navigator.userAgent);
|
||||||
|
const OPERA = chrome.app && /OPR/.test(navigator.userAgent);
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
|
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
|
||||||
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||||
|
|
||||||
['Update', 'Install'].forEach(type =>
|
['Update', 'Install'].forEach(type =>
|
||||||
['', 'Chrome', 'Opera'].forEach(browser =>
|
['', 'Chrome', 'Opera'].forEach(browser =>
|
||||||
document.addEventListener('stylish' + type + browser, onClick)));
|
document.addEventListener('stylish' + type + browser, onClick)));
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
// orphaned content script check
|
// orphaned content script check
|
||||||
if (msg.method === 'ping') {
|
if (msg.method === 'ping') {
|
||||||
sendResponse(true);
|
sendResponse(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new MutationObserver((mutations, observer) => {
|
||||||
|
if (document.body) {
|
||||||
|
observer.disconnect();
|
||||||
|
// TODO: remove the following statement when USO pagination title is fixed
|
||||||
|
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
method: 'getStyles',
|
||||||
|
url: getMeta('stylish-id-url') || location.href
|
||||||
|
}, checkUpdatability);
|
||||||
|
}
|
||||||
|
}).observe(document.documentElement, {childList: true});
|
||||||
|
|
||||||
|
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
||||||
|
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
||||||
|
we need to fix this URL using "stylish-update-url" meta key
|
||||||
|
*/
|
||||||
|
function getStyleURL() {
|
||||||
|
const textUrl = getMeta('stylish-update-url') || '';
|
||||||
|
const jsonUrl = getMeta('stylish-code-chrome') ||
|
||||||
|
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
|
||||||
|
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
|
||||||
|
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
function checkUpdatability([installedStyle]) {
|
||||||
|
// TODO: remove the following statement when USO is fixed
|
||||||
|
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
||||||
|
detail: installedStyle && installedStyle.updateUrl,
|
||||||
|
}));
|
||||||
|
if (!installedStyle) {
|
||||||
|
sendEvent('styleCanBeInstalledChrome');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const md5Url = getMeta('stylish-md5-url');
|
||||||
|
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
||||||
|
getResource(md5Url).then(md5 => {
|
||||||
|
reportUpdatable(md5 !== installedStyle.originalMd5);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
getStyleJson().then(json => {
|
||||||
|
reportUpdatable(!json ||
|
||||||
|
!styleSectionsEqual(json, installedStyle));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportUpdatable(isUpdatable) {
|
||||||
|
sendEvent(
|
||||||
|
isUpdatable
|
||||||
|
? 'styleCanBeUpdatedChrome'
|
||||||
|
: 'styleAlreadyInstalledChrome',
|
||||||
|
{
|
||||||
|
updateUrl: installedStyle.updateUrl
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function sendEvent(type, detail = null) {
|
||||||
|
if (FIREFOX) {
|
||||||
|
type = type.replace('Chrome', '');
|
||||||
|
} else if (OPERA || VIVALDI) {
|
||||||
|
type = type.replace('Chrome', 'Opera');
|
||||||
|
}
|
||||||
|
detail = {detail};
|
||||||
|
if (typeof cloneInto !== 'undefined') {
|
||||||
|
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
||||||
|
// because USO tries to use a global "event" variable deprecated in Firefox
|
||||||
|
detail = cloneInto(detail, document); // eslint-disable-line no-undef
|
||||||
|
}
|
||||||
|
onDOMready().then(() => {
|
||||||
|
document.dispatchEvent(new CustomEvent(type, detail));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onClick(event) {
|
||||||
|
if (onClick.processing || !orphanCheck()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onClick.processing = true;
|
||||||
|
(event.type.includes('Update') ? onUpdate() : onInstall())
|
||||||
|
.then(done, done);
|
||||||
|
function done() {
|
||||||
|
setTimeout(() => {
|
||||||
|
onClick.processing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onInstall() {
|
||||||
|
return getResource(getMeta('stylish-description'))
|
||||||
|
.then(name => saveStyleCode('styleInstall', name))
|
||||||
|
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onUpdate() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
method: 'getStyles',
|
||||||
|
url: getMeta('stylish-id-url') || location.href,
|
||||||
|
}, ([style]) => {
|
||||||
|
saveStyleCode('styleUpdate', style.name, {id: style.id})
|
||||||
|
.then(resolve, reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function saveStyleCode(message, name, addProps) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const needsConfirmation = message === 'styleInstall' || !saveStyleCode.confirmed;
|
||||||
|
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveStyleCode.confirmed = true;
|
||||||
|
enableUpdateButton(false);
|
||||||
|
getStyleJson().then(json => {
|
||||||
|
if (!json) {
|
||||||
|
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
||||||
|
'https://github.com/openstyles/stylus/issues/195');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
Object.assign(json, addProps, {
|
||||||
|
method: 'saveStyle',
|
||||||
|
reason: 'update',
|
||||||
|
}),
|
||||||
|
style => {
|
||||||
|
if (message === 'styleUpdate' && style.updateUrl.includes('?')) {
|
||||||
|
enableUpdateButton(true);
|
||||||
|
} else {
|
||||||
|
sendEvent('styleInstalledChrome');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function enableUpdateButton(state) {
|
||||||
|
const important = s => s.replace(/;/g, '!important;');
|
||||||
|
const button = document.getElementById('update_style_button');
|
||||||
|
if (button) {
|
||||||
|
button.style.cssText = state ? '' : important('pointer-events: none; opacity: .35;');
|
||||||
|
const icon = button.querySelector('img[src*=".svg"]');
|
||||||
|
if (icon) {
|
||||||
|
icon.style.cssText = state ? '' : important('transition: transform 5s; transform: rotate(0);');
|
||||||
|
if (state) {
|
||||||
|
setTimeout(() => (icon.style.cssText += important('transform: rotate(10turn);')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getMeta(name) {
|
||||||
|
const e = document.querySelector(`link[rel="${name}"]`);
|
||||||
|
return e ? e.getAttribute('href') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getResource(url) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (url.startsWith('#')) {
|
||||||
|
resolve(document.getElementById(url.slice(1)).textContent);
|
||||||
|
} else {
|
||||||
|
chrome.runtime.sendMessage({method: 'download', url}, resolve);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getStyleJson() {
|
||||||
|
const url = getStyleURL();
|
||||||
|
return getResource(url).then(code => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(code);
|
||||||
|
} catch (e) {
|
||||||
|
return fetch(url).then(r => r.json()).catch(() => null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||||
|
if (!a || !b) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (a.length !== b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// order of sections should be identical to account for the case of multiple
|
||||||
|
// sections matching the same URL because the order of rules is part of cascading
|
||||||
|
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
|
||||||
|
|
||||||
|
function propertiesEqual(secA, secB) {
|
||||||
|
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
||||||
|
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function equalOrEmpty(a, b, telltale, comparator) {
|
||||||
|
const typeA = a && typeof a[telltale] === 'function';
|
||||||
|
const typeB = b && typeof b[telltale] === 'function';
|
||||||
|
return (
|
||||||
|
(a === null || a === undefined || (typeA && !a.length)) &&
|
||||||
|
(b === null || b === undefined || (typeB && !b.length))
|
||||||
|
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayMirrors(array1, array2) {
|
||||||
|
return (
|
||||||
|
array1.every(el => array2.includes(el)) &&
|
||||||
|
array2.every(el => array1.includes(el))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onDOMready() {
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
document.addEventListener('DOMContentLoaded', function _() {
|
||||||
|
document.removeEventListener('DOMContentLoaded', _);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function orphanCheck() {
|
||||||
|
if (chrome.i18n && chrome.i18n.getUILanguage()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// In Chrome content script is orphaned on an extension update/reload
|
||||||
|
// so we need to detach event listeners
|
||||||
|
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||||
|
['Update', 'Install'].forEach(type =>
|
||||||
|
['', 'Chrome', 'Opera'].forEach(browser =>
|
||||||
|
document.addEventListener('stylish' + type + browser, onClick)));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// TODO: remove the following statement when USO is fixed
|
// TODO: remove the following statement when USO is fixed
|
||||||
document.documentElement.appendChild(document.createElement('script')).text = '(' +
|
document.documentElement.appendChild(document.createElement('script')).text = '(' +
|
||||||
function () {
|
function () {
|
||||||
let settings;
|
let settings;
|
||||||
const originalResponseJson = Response.prototype.json;
|
const originalResponseJson = Response.prototype.json;
|
||||||
|
document.currentScript.remove();
|
||||||
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
|
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
|
||||||
document.removeEventListener('stylusFixBuggyUSOsettings', _);
|
document.removeEventListener('stylusFixBuggyUSOsettings', _);
|
||||||
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search);
|
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search);
|
||||||
|
@ -110,257 +365,3 @@ if (location.search.includes('category=')) {
|
||||||
}).observe(document, {childList: true, subtree: true});
|
}).observe(document, {childList: true, subtree: true});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
new MutationObserver((mutations, observer) => {
|
|
||||||
if (document.body) {
|
|
||||||
observer.disconnect();
|
|
||||||
// TODO: remove the following statement when USO pagination title is fixed
|
|
||||||
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
|
|
||||||
chrome.runtime.sendMessage({
|
|
||||||
method: 'getStyles',
|
|
||||||
url: getMeta('stylish-id-url') || location.href
|
|
||||||
}, checkUpdatability);
|
|
||||||
}
|
|
||||||
}).observe(document.documentElement, {childList: true});
|
|
||||||
|
|
||||||
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
|
||||||
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
|
||||||
we need to fix this URL using "stylish-update-url" meta key
|
|
||||||
*/
|
|
||||||
function getStyleURL() {
|
|
||||||
const textUrl = getMeta('stylish-update-url') || '';
|
|
||||||
const jsonUrl = getMeta('stylish-code-chrome') ||
|
|
||||||
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
|
|
||||||
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
|
|
||||||
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkUpdatability([installedStyle]) {
|
|
||||||
// TODO: remove the following statement when USO is fixed
|
|
||||||
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
|
||||||
detail: installedStyle && installedStyle.updateUrl,
|
|
||||||
}));
|
|
||||||
if (!installedStyle) {
|
|
||||||
sendEvent('styleCanBeInstalledChrome');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const md5Url = getMeta('stylish-md5-url');
|
|
||||||
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
|
||||||
getResource(md5Url).then(md5 => {
|
|
||||||
reportUpdatable(md5 !== installedStyle.originalMd5);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
getStyleJson().then(json => {
|
|
||||||
reportUpdatable(!json ||
|
|
||||||
!styleSectionsEqual(json, installedStyle));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportUpdatable(isUpdatable) {
|
|
||||||
sendEvent(
|
|
||||||
isUpdatable
|
|
||||||
? 'styleCanBeUpdatedChrome'
|
|
||||||
: 'styleAlreadyInstalledChrome',
|
|
||||||
{
|
|
||||||
updateUrl: installedStyle.updateUrl
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function sendEvent(type, detail = null) {
|
|
||||||
if (FIREFOX) {
|
|
||||||
type = type.replace('Chrome', '');
|
|
||||||
} else if (OPERA || VIVALDI) {
|
|
||||||
type = type.replace('Chrome', 'Opera');
|
|
||||||
}
|
|
||||||
detail = {detail};
|
|
||||||
if (typeof cloneInto !== 'undefined') {
|
|
||||||
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
|
||||||
// because USO tries to use a global "event" variable deprecated in Firefox
|
|
||||||
detail = cloneInto(detail, document); // eslint-disable-line no-undef
|
|
||||||
}
|
|
||||||
onDOMready().then(() => {
|
|
||||||
document.dispatchEvent(new CustomEvent(type, detail));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onClick(event) {
|
|
||||||
if (onClick.processing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onClick.processing = true;
|
|
||||||
(event.type.includes('Update') ? onUpdate() : onInstall())
|
|
||||||
.then(done, done);
|
|
||||||
function done() {
|
|
||||||
setTimeout(() => {
|
|
||||||
onClick.processing = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onInstall() {
|
|
||||||
return getResource(getMeta('stylish-description'))
|
|
||||||
.then(name => saveStyleCode('styleInstall', name))
|
|
||||||
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onUpdate() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
chrome.runtime.sendMessage({
|
|
||||||
method: 'getStyles',
|
|
||||||
url: getMeta('stylish-id-url') || location.href,
|
|
||||||
}, ([style]) => {
|
|
||||||
saveStyleCode('styleUpdate', style.name, {id: style.id})
|
|
||||||
.then(resolve, reject);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function saveStyleCode(message, name, addProps) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const needsConfirmation = message === 'styleInstall' || !saveStyleCode.confirmed;
|
|
||||||
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
|
||||||
reject();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveStyleCode.confirmed = true;
|
|
||||||
enableUpdateButton(false);
|
|
||||||
getStyleJson().then(json => {
|
|
||||||
if (!json) {
|
|
||||||
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
|
||||||
'https://github.com/openstyles/stylus/issues/195');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chrome.runtime.sendMessage(
|
|
||||||
Object.assign(json, addProps, {
|
|
||||||
method: 'saveStyle',
|
|
||||||
reason: 'update',
|
|
||||||
}),
|
|
||||||
style => {
|
|
||||||
if (message === 'styleUpdate' && style.updateUrl.includes('?')) {
|
|
||||||
enableUpdateButton(true);
|
|
||||||
} else {
|
|
||||||
sendEvent('styleInstalledChrome');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function enableUpdateButton(state) {
|
|
||||||
const important = s => s.replace(/;/g, '!important;');
|
|
||||||
const button = document.getElementById('update_style_button');
|
|
||||||
if (button) {
|
|
||||||
button.style.cssText = state ? '' : important('pointer-events: none; opacity: .35;');
|
|
||||||
const icon = button.querySelector('img[src*=".svg"]');
|
|
||||||
if (icon) {
|
|
||||||
icon.style.cssText = state ? '' : important('transition: transform 5s; transform: rotate(0);');
|
|
||||||
if (state) {
|
|
||||||
setTimeout(() => (icon.style.cssText += important('transform: rotate(10turn);')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getMeta(name) {
|
|
||||||
const e = document.querySelector(`link[rel="${name}"]`);
|
|
||||||
return e ? e.getAttribute('href') : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getResource(url) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (url.startsWith('#')) {
|
|
||||||
resolve(document.getElementById(url.slice(1)).textContent);
|
|
||||||
} else {
|
|
||||||
chrome.runtime.sendMessage({method: 'download', url}, resolve);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getStyleJson() {
|
|
||||||
const url = getStyleURL();
|
|
||||||
return getResource(url).then(code => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(code);
|
|
||||||
} catch (e) {
|
|
||||||
return fetch(url).then(r => r.json()).catch(() => null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
|
||||||
if (!a || !b) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (a.length !== b.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// order of sections should be identical to account for the case of multiple
|
|
||||||
// sections matching the same URL because the order of rules is part of cascading
|
|
||||||
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
|
|
||||||
|
|
||||||
function propertiesEqual(secA, secB) {
|
|
||||||
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
|
||||||
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function equalOrEmpty(a, b, telltale, comparator) {
|
|
||||||
const typeA = a && typeof a[telltale] === 'function';
|
|
||||||
const typeB = b && typeof b[telltale] === 'function';
|
|
||||||
return (
|
|
||||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
|
||||||
(b === null || b === undefined || (typeB && !b.length))
|
|
||||||
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function arrayMirrors(array1, array2) {
|
|
||||||
return (
|
|
||||||
array1.every(el => array2.includes(el)) &&
|
|
||||||
array2.every(el => array1.includes(el))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onDOMready() {
|
|
||||||
if (document.readyState !== 'loading') {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return new Promise(resolve => {
|
|
||||||
document.addEventListener('DOMContentLoaded', function _() {
|
|
||||||
document.removeEventListener('DOMContentLoaded', _);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function orphanCheck() {
|
|
||||||
const port = chrome.runtime.connect();
|
|
||||||
if (port) {
|
|
||||||
port.disconnect();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// we're orphaned due to an extension update
|
|
||||||
// we can detach event listeners
|
|
||||||
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
|
||||||
['Update', 'Install'].forEach(type =>
|
|
||||||
['', 'Chrome', 'Opera'].forEach(browser =>
|
|
||||||
document.addEventListener('stylish' + type + browser, onClick)));
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user