From 7ec41bcea150b2e42e68affc834ab7977e1a23de Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 12 Apr 2017 09:15:57 +0300 Subject: [PATCH] apply: refactor observers --- apply.js | 275 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 156 insertions(+), 119 deletions(-) diff --git a/apply.js b/apply.js index 73a4e3a9..91d3d240 100644 --- a/apply.js +++ b/apply.js @@ -8,10 +8,7 @@ var disableAll = false; var styleElements = new Map(); var disabledElements = new Map(); var retiredStyleIds = []; -var iframeObserver; -var docRewriteObserver; -initIFrameObserver(); requestStyles(); chrome.runtime.onMessage.addListener(applyOnMessage); @@ -71,7 +68,7 @@ function applyOnMessage(request, sender, sendResponse) { case 'styleAdded': if (request.style.enabled) { - requestStyles({id: request.style.id}, applyStyles); + requestStyles({id: request.style.id}); } break; @@ -96,35 +93,29 @@ function applyOnMessage(request, sender, sendResponse) { } -function doDisableAll(disable) { - if (!disable === !disableAll) { +function doDisableAll(disable, doc = document) { + if (doc == document && !disable === !disableAll) { return; } disableAll = disable; - if (disableAll) { - iframeObserver.disconnect(); + if (disable && doc.iframeObserver) { + doc.iframeObserver.stop(); } - - disableSheets(disableAll, document); - - if (!disableAll && document.readyState != 'loading') { - iframeObserver.start(); - } - - function disableSheets(disable, doc) { - Array.prototype.forEach.call(doc.styleSheets, stylesheet => { - if (stylesheet.ownerNode.classList.contains('stylus') - && stylesheet.disabled != disable) { - stylesheet.disabled = disable; - } - }); - for (const iframe of getDynamicIFrames(doc)) { - if (!disable) { - // update the IFRAME if it was created while the observer was disconnected - addDocumentStylesToIFrame(iframe); - } - disableSheets(disable, iframe.contentDocument); + Array.prototype.forEach.call(doc.styleSheets, stylesheet => { + if (stylesheet.ownerNode.matches('stylus[id^="stylus-"]') + && stylesheet.disabled != disable) { + stylesheet.disabled = disable; } + }); + for (const iframe of getDynamicIFrames(doc)) { + if (!disable) { + // update the IFRAME if it was created while the observer was disconnected + addDocumentStylesToIFrame(iframe); + } + doDisableAll(disable, iframe.contentDocument); + } + if (!disable && doc.readyState != 'loading' && doc.iframeObserver) { + doc.iframeObserver.start(); } } @@ -141,15 +132,23 @@ function applyStyleState(id, enabled, doc) { } if (enabled && inCache) { const el = inCache.cloneNode(true); - document.documentElement.appendChild(el); + doc.documentElement.appendChild(el); el.sheet.disabled = disableAll; processDynamicIFrames(doc, applyStyleState, id, enabled); disabledElements.delete(id); return; } if (!enabled && inDoc) { - disabledElements.set(id, inDoc); + if (!inCache) { + disabledElements.set(id, inDoc); + } inDoc.remove(); + if (doc.location.href == 'about:srcdoc') { + const original = doc.getElementById('stylus-' + id); + if (original) { + original.remove(); + } + } processDynamicIFrames(doc, applyStyleState, id, enabled); return; } @@ -162,7 +161,7 @@ function removeStyle(id, doc) { styleElements.delete('stylus-' + id); disabledElements.delete(id); if (!styleElements.size) { - iframeObserver.disconnect(); + doc.iframeObserver.disconnect(); } } processDynamicIFrames(doc, removeStyle, id); @@ -213,12 +212,7 @@ function applyStyles(styleHash) { document.head.appendChild(document.getElementById(id)); } } - if (document.readyState != 'loading') { - onDOMContentLoaded(); - } else { - document.addEventListener('DOMContentLoaded', onDOMContentLoaded); - } - initDocRewriteObserver(); + initObservers(); } if (retiredStyleIds.length) { @@ -231,12 +225,6 @@ function applyStyles(styleHash) { } -function onDOMContentLoaded() { - addDocumentStylesToAllIFrames(); - iframeObserver.start(); -} - - function applySections(styleId, sections) { let el = document.getElementById('stylus-' + styleId); // Already there. @@ -288,18 +276,18 @@ function addDocumentStylesToIFrame(iframe) { addStyleElement(el, doc); } } - initDocRewriteObserver(doc); + initObservers(doc); } -function addDocumentStylesToAllIFrames() { - getDynamicIFrames(document).forEach(addDocumentStylesToIFrame); +function addDocumentStylesToAllIFrames(doc = document) { + getDynamicIFrames(doc).forEach(addDocumentStylesToIFrame); } // Only dynamic iframes get the parent document's styles. Other ones should get styles based on their own URLs. function getDynamicIFrames(doc) { - return [...doc.getElementsByTagName('iframe')].filter(iframeIsDynamic); + return Array.prototype.filter.call(doc.getElementsByTagName('iframe'), iframeIsDynamic); } @@ -319,7 +307,9 @@ function iframeIsDynamic(f) { function processDynamicIFrames(doc, fn, ...args) { - for (const iframe of [...doc.getElementsByTagName('iframe')]) { + var iframes = doc.getElementsByTagName('iframe'); + for (var i = 0, il = iframes.length; i < il; i++) { + var iframe = iframes[i]; if (iframeIsDynamic(iframe)) { fn(...args, iframe.contentDocument); } @@ -344,8 +334,8 @@ function addStyleToIFrameSrcDoc(iframe, el) { function replaceAll(newStyles, doc) { - const oldStyles = [...doc.querySelectorAll('STYLE.stylus')]; - oldStyles.forEach(style => (style.id += '-ghost')); + Array.prototype.forEach.call(doc.querySelectorAll('STYLE.stylus[id^="stylus-"]'), + e => (e.id += '-ghost')); processDynamicIFrames(doc, replaceAll, newStyles); if (doc == document) { styleElements.clear(); @@ -357,80 +347,123 @@ function replaceAll(newStyles, doc) { function replaceAllpass2(newStyles, doc) { - const oldStyles = [...doc.querySelectorAll('STYLE.stylus[id$="-ghost"]')]; + const oldStyles = doc.querySelectorAll('STYLE.stylus[id$="-ghost"]'); processDynamicIFrames(doc, replaceAllpass2, newStyles); - oldStyles.forEach(e => e.remove()); + Array.prototype.forEach.call(oldStyles, + e => e.remove()); } -function initIFrameObserver() { - iframeObserver = Object.assign(new MutationObserver(observer), { - start() { - this.observe(document, {childList: true, subtree: true}); - } - }); - const iframesCollection = document.getElementsByTagName('iframe'); - - function observer(mutations) { - // autoupdated HTMLCollection is superfast - if (!iframesCollection[0]) { - return; - } - // use a much faster method for very complex pages with lots of mutations - // (observer usually receives 1k-10k mutations per call) - if (mutations.length > 1000) { - addDocumentStylesToAllIFrames(); - return; - } - // move the check out of current execution context - // because some same-domain (!) iframes fail to load when their 'contentDocument' is accessed (!) - // namely gmail's old chat iframe talkgadget.google.com - setTimeout(process, 0, mutations); - } - - function process(mutations) { - for (var m = 0, mutation; (mutation = mutations[m++]);) { - var added = mutation.addedNodes; - for (var n = 0, node; (node = added[n++]);) { - // process only ELEMENT_NODE - if (node.nodeType != 1) { - continue; - } - var iframes = node.localName === 'iframe' ? [node] : - node.children.length && node.getElementsByTagName('iframe'); - for (var i = 0, iframe; (iframe = iframes[i++]);) { - if (iframeIsDynamic(iframe)) { - addDocumentStylesToIFrame(iframe); - } - } - } - } +function onDOMContentLoaded({target = document} = {}) { + addDocumentStylesToAllIFrames(target); + if (target.iframeObserver) { + target.iframeObserver.start(); } } -function initDocRewriteObserver() { - if (isOwnPage) { +function initObservers(doc = document) { + if (isOwnPage || doc.rewriteObserver) { return; } - // re-add styles if we detect documentElement being recreated - docRewriteObserver = new MutationObserver(observer); - docRewriteObserver.observe(document, {childList: true}); + initIFrameObserver(doc); + initDocRewriteObserver(doc); + if (doc.readyState != 'loading') { + onDOMContentLoaded({target: doc}); + } else { + doc.addEventListener('DOMContentLoaded', onDOMContentLoaded); + } +} - function observer(mutations) { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if (node.localName != 'html') { - continue; - } - for (const [id, el] of styleElements.entries()) { - if (!document.getElementById(id)) { - document.documentElement.appendChild(el); + +function initIFrameObserver(doc = document) { + if (!initIFrameObserver.methods) { + initIFrameObserver.methods = { + start() { + this.observe(this.doc, {childList: true, subtree: true}); + }, + stop() { + this.disconnect(); + getDynamicIFrames(this.doc).forEach(iframe => { + const observer = iframe.contentDocument.iframeObserver; + if (observer) { + observer.stop(); } - } - document.addEventListener('DOMContentLoaded', onDOMContentLoaded); - return; + }); + }, + }; + } + doc.iframeObserver = Object.assign( + new MutationObserver(iframeObserver), + initIFrameObserver.methods, { + iframes: doc.getElementsByTagName('iframe'), + doc, + }); +} + + +function iframeObserver(mutations, observer) { + // autoupdated HTMLCollection is superfast + if (!observer.iframes[0]) { + return; + } + // use a much faster method for very complex pages with lots of mutations + // (observer usually receives 1k-10k mutations per call) + if (mutations.length > 1000) { + addDocumentStylesToAllIFrames(observer.doc); + return; + } + for (var m = 0, ml = mutations.length; m < ml; m++) { + var added = mutations[m].addedNodes; + for (var n = 0, nl = added.length; n < nl; n++) { + var node = added[n]; + // process only ELEMENT_NODE + if (node.nodeType != 1) { + continue; } + var iframes = node.localName === 'iframe' ? [node] : + node.children.length && node.getElementsByTagName('iframe'); + if (iframes.length) { + // move the check out of current execution context + // because some same-domain (!) iframes fail to load when their 'contentDocument' is accessed (!) + // namely gmail's old chat iframe talkgadget.google.com + setTimeout(testIFrames, 0, iframes); + } + } + } +} + + +function testIFrames(iframes) { + for (const iframe of iframes) { + if (iframeIsDynamic(iframe)) { + addDocumentStylesToIFrame(iframe); + } + } +} + + +function initDocRewriteObserver(doc = document) { + // re-add styles if we detect documentElement being recreated + doc.rewriteObserver = new MutationObserver(docRewriteObserver); + doc.rewriteObserver.observe(doc, {childList: true}); +} + + +function docRewriteObserver(mutations) { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.localName != 'html') { + continue; + } + const doc = node.ownerDocument; + for (const [id, el] of styleElements.entries()) { + if (!doc.getElementById(id)) { + doc.documentElement.appendChild(el); + } + } + initObservers(doc); + return; } } } @@ -447,15 +480,19 @@ function orphanCheck() { // we're orphaned due to an extension update // we can detach the mutation observer - iframeObserver.takeRecords(); - iframeObserver.disconnect(); - iframeObserver = null; - if (docRewriteObserver) { - docRewriteObserver.disconnect(); - docRewriteObserver = null; - } // we can detach event listeners - document.removeEventListener('DOMContentLoaded', onDOMContentLoaded); + (function unbind(doc) { + if (doc.iframeObserver) { + doc.iframeObserver.disconnect(); + delete doc.iframeObserver; + } + if (doc.rewriteObserver) { + doc.rewriteObserver.disconnect(); + delete doc.rewriteObserver; + } + doc.removeEventListener('DOMContentLoaded', onDOMContentLoaded); + getDynamicIFrames(doc).forEach(iframe => unbind(iframe.contentDocument)); + })(document); 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