diff --git a/inject.js b/inject.js index e0ee74a..aed1d6b 100644 --- a/inject.js +++ b/inject.js @@ -449,6 +449,215 @@ if (document.querySelector('apple-tv-plus-player')) { console.log('Congratulations. There is the apple-tv-plus-player.') } + + // start of ally.js/src/observe/shadow-mutations.js + // import nodeArray from '../util/node-array'; + // input may be undefined, selector-tring, Node, NodeList, HTMLCollection, array of Nodes + // yes, to some extent this is a bad replica of jQuery's constructor function + function nodeArray(input) { + if (!input) { + return []; + } + + if (Array.isArray(input)) { + return input; + } + + // instanceof Node - does not work with iframes + if (input.nodeType !== undefined) { + return [input]; + } + + if (typeof input === 'string') { + input = document.querySelectorAll(input); + } + + if (input.length !== undefined) { + return [].slice.call(input, 0); + } + + throw new TypeError('unexpected input ' + String(input)); + } + //import queryShadowHosts from '../query/shadow-hosts'; + //import contextToElement from '../util/context-to-element'; + //import nodeArray from '../util/node-array'; already imported + + function contextToElement({ + context, + label = 'context-to-element', + resolveDocument, + defaultToDocument, + }) { + let element = nodeArray(context)[0]; + + if (resolveDocument && element && element.nodeType === Node.DOCUMENT_NODE) { + element = element.documentElement; + } + + if (!element && defaultToDocument) { + return document.documentElement; + } + + if (!element) { + throw new TypeError(label + ' requires valid options.context'); + } + + if (element.nodeType !== Node.ELEMENT_NODE && element.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) { + throw new TypeError(label + ' requires options.context to be an Element'); + } + + return element; + } + //import getDocument from '../util/get-document'; + function getDocument(node) { + if (!node) { + return document; + } + + if (node.nodeType === Node.DOCUMENT_NODE) { + return node; + } + + return node.ownerDocument || document; + } + + // see https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter + const filter = function(node) { + if (node.shadowRoot) { + return NodeFilter.FILTER_ACCEPT; + } + + return NodeFilter.FILTER_SKIP; + }; + // IE requires a function, Browsers require {acceptNode: function} + // see http://www.bennadel.com/blog/2607-finding-html-comment-nodes-in-the-dom-using-treewalker.htm + filter.acceptNode = filter; + + function queryShadowHosts({ context } = {}) { + const element = contextToElement({ + label: 'query/shadow-hosts', + resolveDocument: true, + defaultToDocument: true, + context, + }); + + const _document = getDocument(context); + // see https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker + const walker = _document.createTreeWalker( + // root element to start search in + element, + // element type filter + NodeFilter.SHOW_ELEMENT, + // custom NodeFilter filter + filter, + // deprecated, but IE requires it + false + ); + + let list = []; + + if (element.shadowRoot) { + // TreeWalker does not run the filter on the context element + list.push(element); + list = list.concat(queryShadowHosts({ + context: element.shadowRoot, + })); + } + + while (walker.nextNode()) { + list.push(walker.currentNode); + list = list.concat(queryShadowHosts({ + context: walker.currentNode.shadowRoot, + })); + } + + return list; + } + //import contextToElement from '../util/context-to-element'; already imported + + const shadowObserverConfig = { + childList: true, + subtree: true, + }; + + class ShadowMutationObserver { + constructor({context, callback, config} = {}) { + this.config = config; + + this.disengage = this.disengage.bind(this); + + this.clientObserver = new MutationObserver(callback); + this.hostObserver = new MutationObserver(mutations => mutations.forEach(this.handleHostMutation, this)); + + this.observeContext(context); + this.observeShadowHosts(context); + } + + disengage() { + this.clientObserver && this.clientObserver.disconnect(); + this.clientObserver = null; + this.hostObserver && this.hostObserver.disconnect(); + this.hostObserver = null; + } + + observeShadowHosts(context) { + const hosts = queryShadowHosts({ + context, + }); + + hosts.forEach(element => this.observeContext(element.shadowRoot)); + } + + observeContext(context) { + this.clientObserver.observe(context, this.config); + this.hostObserver.observe(context, shadowObserverConfig); + } + + handleHostMutation(mutation) { + if (mutation.type !== 'childList') { + return; + } + + const addedElements = nodeArray(mutation.addedNodes).filter(element => element.nodeType === Node.ELEMENT_NODE); + addedElements.forEach(this.observeShadowHosts, this); + } + } + + function shadowMutations(context, callback, config) { + if (typeof callback !== 'function') { + throw new TypeError('observe/shadow-mutations requires options.callback to be a function'); + } + + if (typeof config !== 'object') { + throw new TypeError('observe/shadow-mutations requires options.config to be an object'); + } + + if (!window.MutationObserver) { + // not supporting IE10 via Mutation Events, because they're too expensive + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events + return { + disengage: function() {}, + }; + } + + const element = contextToElement({ + label: 'observe/shadow-mutations', + resolveDocument: true, + defaultToDocument: true, + context, + }); + + const service = new ShadowMutationObserver({ + context: element, + callback, + config, + }); + + return { + disengage: service.disengage, + }; + } + //end of ally.js/src/observe/shadow-mutations.js } function runAction(action, document, value, e) {