| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- // Not using some slow features of ES6, see http://kpdecker.github.io/six-speed/
- // like destructring, classes, defaults, spread, calculated key names
- /* eslint no-var: 0 */
- 'use strict';
- var ID_PREFIX = 'stylus-';
- var ROOT = document.documentElement;
- var isOwnPage = location.href.startsWith('chrome-extension:');
- var disableAll = false;
- var exposeIframes = false;
- var styleElements = new Map();
- var disabledElements = new Map();
- var retiredStyleTimers = new Map();
- var docRewriteObserver;
- requestStyles();
- chrome.runtime.onMessage.addListener(applyOnMessage);
- if (!isOwnPage) {
- window.dispatchEvent(new CustomEvent(chrome.runtime.id));
- window.addEventListener(chrome.runtime.id, orphanCheck, true);
- }
- function requestStyles(options, callback = applyStyles) {
- 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 !== 'undefined') {
- 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;
- }
- 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);
- inDoc.remove();
- }
- }
- }
- 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;
- }
- if (document.head
- && document.head.firstChild
- && document.head.firstChild.id == 'xml-viewer-style') {
- // when site response is application/xml Chrome displays our style elements
- // under document.documentElement as plain text so we need to move them into HEAD
- // which is already autogenerated at this moment
- ROOT = document.head;
- }
- for (const id in styles) {
- applySections(id, styles[id]);
- }
- initDocRewriteObserver();
- if (retiredStyleTimers.size) {
- setTimeout(() => {
- for (const [id, timer] of retiredStyleTimers.entries()) {
- removeStyle({id});
- clearTimeout(timer);
- }
- });
- }
- }
- function applySections(styleId, sections) {
- 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, {
- id: ID_PREFIX + styleId,
- className: 'stylus',
- type: 'text/css',
- textContent: sections.map(section => section.code).join('\n'),
- });
- addStyleElement(el);
- styleElements.set(el.id, el);
- disabledElements.delete(styleId);
- }
- function addStyleElement(el) {
- if (ROOT && !document.getElementById(el.id)) {
- ROOT.appendChild(el);
- el.disabled = disableAll;
- }
- }
- 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() {
- if (isOwnPage || docRewriteObserver || !styleElements.size) {
- return;
- }
- // re-add styles if we detect documentElement being recreated
- const reinjectStyles = () => {
- if (!styleElements) {
- return orphanCheck && orphanCheck();
- }
- ROOT = document.documentElement;
- for (const el of styleElements.values()) {
- addStyleElement(document.importNode(el, true));
- }
- };
- // 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();
- }
- });
- }
- 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
- if (docRewriteObserver) {
- docRewriteObserver.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',
- 'orphanCheck',
- 'removeStyle',
- 'replaceAll',
- 'requestStyles',
- // variables
- 'ROOT',
- 'disabledElements',
- 'retiredStyleTimers',
- 'styleElements',
- 'docRewriteObserver',
- ].forEach(fn => (window[fn] = null));
- }
|