diff options
Diffstat (limited to 'catapult/third_party/polymer/components/shadycss/src/document-watcher.js')
-rw-r--r-- | catapult/third_party/polymer/components/shadycss/src/document-watcher.js | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/catapult/third_party/polymer/components/shadycss/src/document-watcher.js b/catapult/third_party/polymer/components/shadycss/src/document-watcher.js new file mode 100644 index 00000000..9cf34f05 --- /dev/null +++ b/catapult/third_party/polymer/components/shadycss/src/document-watcher.js @@ -0,0 +1,198 @@ +/** +@license +Copyright (c) 2017 The Polymer Project Authors. All rights reserved. +This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt +The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt +The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt +Code distributed by Google as part of the polymer project is also +subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt +*/ + +'use strict'; + +import {nativeShadow} from './style-settings.js'; +import StyleTransformer from './style-transformer.js'; +import {getIsExtends, elementHasBuiltCss, wrap} from './style-util.js'; + +export let flush = function() {}; + +/** + * @param {!Element} element + * @return {string} + */ +function getClasses(element) { + if (element.classList && element.classList.value) { + return element.classList.value; + } else { + // NOTE: className is patched to remove scoping classes in ShadyDOM + // use getAttribute('class') instead, which is unpatched + return element.getAttribute('class') || ''; + } +} + +const scopeRegExp = new RegExp(`${StyleTransformer.SCOPE_NAME}\\s*([^\\s]*)`); + +/** + * @param {!Element} element + * @return {string} + */ +export function getCurrentScope(element) { + const match = getClasses(element).match(scopeRegExp); + if (match) { + return match[1]; + } else { + return ''; + } +} + +/** + * @param {!Node} node + */ +export function getOwnerScope(node) { + const ownerRoot = wrap(node).getRootNode(); + if (ownerRoot === node || ownerRoot === node.ownerDocument) { + return ''; + } + const host = /** @type {!ShadowRoot} */(ownerRoot).host; + if (!host) { + // this may actually be a document fragment + return ''; + } + return getIsExtends(host).is; +} + +/** + * @param {!Element} element + */ +export function ensureCorrectScope(element) { + const currentScope = getCurrentScope(element); + const ownerRoot = wrap(element).getRootNode(); + if (ownerRoot === element) { + return; + } + if (currentScope && ownerRoot === element.ownerDocument) { + // node was scoped, but now is in document + StyleTransformer.domRemoveScope(element, currentScope); + } else if (ownerRoot instanceof ShadowRoot) { + const ownerScope = getOwnerScope(element); + if (ownerScope !== currentScope) { + // node was scoped, but not by its current owner + StyleTransformer.domReplaceScope(element, currentScope, ownerScope); + } + } +} + +/** + * @param {!HTMLElement|!HTMLDocument} element + */ +export function ensureCorrectSubtreeScoping(element) { + // find unscoped subtree nodes + const unscopedNodes = window['ShadyDOM']['nativeMethods']['querySelectorAll'].call( + element, `:not(.${StyleTransformer.SCOPE_NAME})`); + + for (let j = 0; j < unscopedNodes.length; j++) { + // it's possible, during large batch inserts, that nodes that aren't + // scoped within the current scope were added. + // To make sure that any unscoped nodes that were inserted in the current batch are correctly styled, + // query all unscoped nodes and force their style-scope to be applied. + // This could happen if a sub-element appended an unscoped node in its shadowroot and this function + // runs on a parent element of the host of that unscoped node: + // parent-element -> element -> unscoped node + // Here unscoped node should have the style-scope element, not parent-element. + const unscopedNode = unscopedNodes[j]; + const scopeForPreviouslyUnscopedNode = getOwnerScope(unscopedNode); + if (scopeForPreviouslyUnscopedNode) { + StyleTransformer.element(unscopedNode, scopeForPreviouslyUnscopedNode); + } + } +} + +/** + * @param {HTMLElement} el + * @return {boolean} + */ +function isElementWithBuiltCss(el) { + if (el.localName === 'style' || el.localName === 'template') { + return elementHasBuiltCss(el); + } + return false; +} + +/** + * @param {Array<MutationRecord|null>|null} mxns + */ +function handler(mxns) { + for (let x=0; x < mxns.length; x++) { + let mxn = mxns[x]; + if (mxn.target === document.documentElement || + mxn.target === document.head) { + continue; + } + for (let i=0; i < mxn.addedNodes.length; i++) { + let n = mxn.addedNodes[i]; + if (n.nodeType !== Node.ELEMENT_NODE) { + continue; + } + n = /** @type {HTMLElement} */(n); // eslint-disable-line no-self-assign + let root = n.getRootNode(); + let currentScope = getCurrentScope(n); + // node was scoped, but now is in document + // If this element has built css, we must not remove scoping as this node + // will be used as a template or style without re - applying scoping as an optimization + if (currentScope && root === n.ownerDocument && !isElementWithBuiltCss(n)) { + StyleTransformer.domRemoveScope(n, currentScope); + } else if (root instanceof ShadowRoot) { + const newScope = getOwnerScope(n); + // rescope current node and subtree if necessary + if (newScope !== currentScope) { + StyleTransformer.domReplaceScope(n, currentScope, newScope); + } + // make sure all the subtree elements are scoped correctly + ensureCorrectSubtreeScoping(n); + } + } + } +} + +// if native Shadow DOM is being used, or ShadyDOM handles dynamic scoiping, do not activate the MutationObserver +if (!nativeShadow && !(window['ShadyDOM'] && window['ShadyDOM']['handlesDynamicScoping'])) { + let observer = new MutationObserver(handler); + let start = (node) => { + observer.observe(node, {childList: true, subtree: true}); + } + let nativeCustomElements = (window['customElements'] && + !window['customElements']['polyfillWrapFlushCallback']); + // need to start immediately with native custom elements + // TODO(dfreedm): with polyfilled HTMLImports and native custom elements + // excessive mutations may be observed; this can be optimized via cooperation + // with the HTMLImports polyfill. + if (nativeCustomElements) { + start(document); + } else { + let delayedStart = () => { + start(document.body); + } + // use polyfill timing if it's available + if (window['HTMLImports']) { + window['HTMLImports']['whenReady'](delayedStart); + // otherwise push beyond native imports being ready + // which requires RAF + readystate interactive. + } else { + requestAnimationFrame(function() { + if (document.readyState === 'loading') { + let listener = function() { + delayedStart(); + document.removeEventListener('readystatechange', listener); + } + document.addEventListener('readystatechange', listener); + } else { + delayedStart(); + } + }); + } + } + + flush = function() { + handler(observer.takeRecords()); + } +} |