From 6bc9534088bf665c8d1f3258f978630b192fc25a Mon Sep 17 00:00:00 2001 From: Daniel Freedman Date: Wed, 8 May 2019 15:20:58 -0700 Subject: [PATCH 1/3] Implement scopeSubtree for ShadyDOM noPatch mode Add scopeSubtree as a util function, and use in LegacyElement --- lib/legacy/legacy-element-mixin.js | 10 +- lib/utils/scope-subtree.js | 76 +++++++++++++++ test/runner.html | 3 +- test/unit/styling-scoped-nopatch.html | 133 ++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 lib/utils/scope-subtree.js create mode 100644 test/unit/styling-scoped-nopatch.html diff --git a/lib/legacy/legacy-element-mixin.js b/lib/legacy/legacy-element-mixin.js index 861fefb4c0..213c3078b4 100644 --- a/lib/legacy/legacy-element-mixin.js +++ b/lib/legacy/legacy-element-mixin.js @@ -20,6 +20,7 @@ import { Debouncer } from '../utils/debounce.js'; import { timeOut, microTask } from '../utils/async.js'; import { get } from '../utils/path.js'; import { wrap } from '../utils/wrap.js'; +import { scopeSubtree } from '../utils/scope-subtree.js'; let styleInterface = window.ShadyCSS; @@ -701,12 +702,13 @@ export const LegacyElementMixin = dedupingMixin((base) => { /** * No-op for backwards compatibility. This should now be handled by * ShadyCss library. - * @param {*} container Unused - * @param {*} shouldObserve Unused - * @return {void} + * @param {!Element} container Container element to scope + * @param {boolean=} shouldObserve if true, start a mutation observer for added nodes to the container + * @return {?MutationObserver} Returns a new MutationObserver on `container` if `shouldObserve` is true. * @override */ - scopeSubtree(container, shouldObserve) { // eslint-disable-line no-unused-vars + scopeSubtree(container, shouldObserve = false) { + return scopeSubtree(container, shouldObserve); } /** diff --git a/lib/utils/scope-subtree.js b/lib/utils/scope-subtree.js new file mode 100644 index 0000000000..662a6acfd8 --- /dev/null +++ b/lib/utils/scope-subtree.js @@ -0,0 +1,76 @@ +/** +@license +Copyright (c) 2019 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 +*/ + +import './boot.js'; +import { wrap } from './wrap.js'; + +const ShadyDOM = window.ShadyDOM; +const ShadyCSS = window.ShadyCSS; + +/** + * Ensure that elements in a ShadowDOM container are scoped correctly. + * + * @param {!Element} container Container element to scope + * @param {boolean=} shouldObserve if true, start a mutation observer for added nodes to the container + * @return {?MutationObserver} Returns a new MutationObserver on `container` if `shouldObserve` is true. + */ +export function scopeSubtree(container, shouldObserve = false) { + // If using native ShadowDOM, abort + if (!ShadyDOM || !ShadyCSS) { + return null; + } + // ShadyCSS handles DOM mutations in patched mode + if (!ShadyDOM['noPatch']) { + return null; + } + const ScopingShim = ShadyCSS['ScopingShim']; + // if ScopingShim is not available, abort + if (!ScopingShim) { + return null; + } + // capture correct scope for container + const containerScope = ScopingShim['scopeForNode'](container); + + const scopify = (node) => { + const wrappedNode = wrap(node); + const elements = [node, ...(wrappedNode.querySelectorAll('*'))]; + for (let i = 0; i < elements.length; i++) { + const el = elements[i]; + const currentScope = ScopingShim['currentScopeForNode'](el); + if (currentScope !== containerScope) { + if (currentScope !== '') { + ScopingShim['unscopeNode'](el, currentScope); + } + ScopingShim['scopeNode'](el, containerScope); + } + } + }; + + // scope everything in container + scopify(container); + + if (shouldObserve) { + const mo = new MutationObserver((mxns) => { + for (let i = 0; i < mxns.length; i++) { + const mxn = mxns[i]; + for (let j = 0; j < mxn.addedNodes.length; j++) { + const addedNode = mxn.addedNodes[j]; + if (addedNode.nodeType === Node.ELEMENT_NODE) { + scopify(addedNode); + } + } + } + }); + mo.observe(container, {childList: true, subtree: true}); + return mo; + } else { + return null; + } +} \ No newline at end of file diff --git a/test/runner.html b/test/runner.html index 338dd11253..e174cab993 100644 --- a/test/runner.html +++ b/test/runner.html @@ -88,7 +88,8 @@ 'unit/html-tag.html', 'unit/legacy-data.html', // 'unit/multi-style.html' - 'unit/class-properties.html' + 'unit/class-properties.html', + 'unit/styling-scoped-nopatch.html' ]; function combinations(suites, flags) { diff --git a/test/unit/styling-scoped-nopatch.html b/test/unit/styling-scoped-nopatch.html new file mode 100644 index 0000000000..b1f82b1fea --- /dev/null +++ b/test/unit/styling-scoped-nopatch.html @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From e10019a064c96837718fdbe1bb99153f4415d52d Mon Sep 17 00:00:00 2001 From: Daniel Freedman Date: Wed, 8 May 2019 16:17:41 -0700 Subject: [PATCH 2/3] Use native qSA Add documentation about when to use scopeSubtree --- lib/utils/scope-subtree.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/utils/scope-subtree.js b/lib/utils/scope-subtree.js index 662a6acfd8..1ec29e5bcf 100644 --- a/lib/utils/scope-subtree.js +++ b/lib/utils/scope-subtree.js @@ -9,13 +9,14 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN */ import './boot.js'; -import { wrap } from './wrap.js'; const ShadyDOM = window.ShadyDOM; const ShadyCSS = window.ShadyCSS; /** * Ensure that elements in a ShadowDOM container are scoped correctly. + * This function is only needed when ShadyDOM is used and unpatched DOM APIs are used in third party code. + * This can happen in noPatch mode or when specialized APIs like ranges or tables are used to mutate DOM. * * @param {!Element} container Container element to scope * @param {boolean=} shouldObserve if true, start a mutation observer for added nodes to the container @@ -26,8 +27,8 @@ export function scopeSubtree(container, shouldObserve = false) { if (!ShadyDOM || !ShadyCSS) { return null; } - // ShadyCSS handles DOM mutations in patched mode - if (!ShadyDOM['noPatch']) { + // ShadyCSS handles DOM mutations when ShadyDOM does not handle scoping itself + if (!ShadyDOM['handlesDynamicScoping']) { return null; } const ScopingShim = ShadyCSS['ScopingShim']; @@ -39,8 +40,9 @@ export function scopeSubtree(container, shouldObserve = false) { const containerScope = ScopingShim['scopeForNode'](container); const scopify = (node) => { - const wrappedNode = wrap(node); - const elements = [node, ...(wrappedNode.querySelectorAll('*'))]; + // NOTE: native qSA does not honor scoped DOM, but it is faster, and the same behavior as Polymer v1 + const descendants = ShadyDOM['nativeMethods']['querySelectorAll'].call(node, '*'); + const elements = [node, ...descendants]; for (let i = 0; i < elements.length; i++) { const el = elements[i]; const currentScope = ScopingShim['currentScopeForNode'](el); From 338d420c2c66c4c29a49eed47976fa942bb9996d Mon Sep 17 00:00:00 2001 From: Daniel Freedman Date: Thu, 16 May 2019 13:56:31 -0700 Subject: [PATCH 3/3] Use Array.from instead of a list comprehension --- lib/utils/scope-subtree.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utils/scope-subtree.js b/lib/utils/scope-subtree.js index 1ec29e5bcf..7932d27f81 100644 --- a/lib/utils/scope-subtree.js +++ b/lib/utils/scope-subtree.js @@ -41,8 +41,8 @@ export function scopeSubtree(container, shouldObserve = false) { const scopify = (node) => { // NOTE: native qSA does not honor scoped DOM, but it is faster, and the same behavior as Polymer v1 - const descendants = ShadyDOM['nativeMethods']['querySelectorAll'].call(node, '*'); - const elements = [node, ...descendants]; + const elements = Array.from(ShadyDOM['nativeMethods']['querySelectorAll'].call(node, '*')); + elements.push(node); for (let i = 0; i < elements.length; i++) { const el = elements[i]; const currentScope = ScopingShim['currentScopeForNode'](el);