UNPKG

chrome-devtools-frontend

Version:
317 lines (301 loc) • 14.6 kB
/** * @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 */ /** * Module to add shady DOM/shady CSS polyfill support to lit-html template * rendering. See the [[render]] method for details. * * @packageDocumentation */ /** * Do not remove this comment; it keeps typedoc from misplacing the module * docs. */ import {removeNodes} from './dom.js'; import {insertNodeIntoTemplate, removeNodesFromTemplate} from './modify-template.js'; import {RenderOptions} from './render-options.js'; import {parts, render as litRender} from './render.js'; import {templateCaches} from './template-factory.js'; import {TemplateInstance} from './template-instance.js'; import {TemplateResult} from './template-result.js'; import {marker, Template} from './template.js'; export {html, svg, TemplateResult} from '../lit-html.js'; // Get a key to lookup in `templateCaches`. const getTemplateCacheKey = (type: string, scopeName: string) => `${type}--${scopeName}`; let compatibleShadyCSSVersion = true; if (typeof window.ShadyCSS === 'undefined') { compatibleShadyCSSVersion = false; } else if (typeof window.ShadyCSS.prepareTemplateDom === 'undefined') { console.warn( `Incompatible ShadyCSS version detected. ` + `Please update to at least @webcomponents/webcomponentsjs@2.0.2 and ` + `@webcomponents/shadycss@1.3.1.`); compatibleShadyCSSVersion = false; } /** * Template factory which scopes template DOM using ShadyCSS. * @param scopeName {string} */ export const shadyTemplateFactory = (scopeName: string) => (result: TemplateResult) => { const cacheKey = getTemplateCacheKey(result.type, scopeName); let templateCache = templateCaches.get(cacheKey); if (templateCache === undefined) { templateCache = { stringsArray: new WeakMap<TemplateStringsArray, Template>(), keyString: new Map<string, Template>() }; templateCaches.set(cacheKey, templateCache); } let template = templateCache.stringsArray.get(result.strings); if (template !== undefined) { return template; } const key = result.strings.join(marker); template = templateCache.keyString.get(key); if (template === undefined) { const element = result.getTemplateElement(); if (compatibleShadyCSSVersion) { window.ShadyCSS!.prepareTemplateDom(element, scopeName); } template = new Template(result, element); templateCache.keyString.set(key, template); } templateCache.stringsArray.set(result.strings, template); return template; }; const TEMPLATE_TYPES = ['html', 'svg']; /** * Removes all style elements from Templates for the given scopeName. */ const removeStylesFromLitTemplates = (scopeName: string) => { TEMPLATE_TYPES.forEach((type) => { const templates = templateCaches.get(getTemplateCacheKey(type, scopeName)); if (templates !== undefined) { templates.keyString.forEach((template) => { const {element: {content}} = template; // IE 11 doesn't support the iterable param Set constructor const styles = new Set<Element>(); Array.from(content.querySelectorAll('style')).forEach((s: Element) => { styles.add(s); }); removeNodesFromTemplate(template, styles); }); } }); }; const shadyRenderSet = new Set<string>(); /** * For the given scope name, ensures that ShadyCSS style scoping is performed. * This is done just once per scope name so the fragment and template cannot * be modified. * (1) extracts styles from the rendered fragment and hands them to ShadyCSS * to be scoped and appended to the document * (2) removes style elements from all lit-html Templates for this scope name. * * Note, <style> elements can only be placed into templates for the * initial rendering of the scope. If <style> elements are included in templates * dynamically rendered to the scope (after the first scope render), they will * not be scoped and the <style> will be left in the template and rendered * output. */ const prepareTemplateStyles = (scopeName: string, renderedDOM: DocumentFragment, template?: Template) => { shadyRenderSet.add(scopeName); // If `renderedDOM` is stamped from a Template, then we need to edit that // Template's underlying template element. Otherwise, we create one here // to give to ShadyCSS, which still requires one while scoping. const templateElement = !!template ? template.element : document.createElement('template'); // Move styles out of rendered DOM and store. const styles = renderedDOM.querySelectorAll('style'); const {length} = styles; // If there are no styles, skip unnecessary work if (length === 0) { // Ensure prepareTemplateStyles is called to support adding // styles via `prepareAdoptedCssText` since that requires that // `prepareTemplateStyles` is called. // // ShadyCSS will only update styles containing @apply in the template // given to `prepareTemplateStyles`. If no lit Template was given, // ShadyCSS will not be able to update uses of @apply in any relevant // template. However, this is not a problem because we only create the // template for the purpose of supporting `prepareAdoptedCssText`, // which doesn't support @apply at all. window.ShadyCSS!.prepareTemplateStyles(templateElement, scopeName); return; } const condensedStyle = document.createElement('style'); // Collect styles into a single style. This helps us make sure ShadyCSS // manipulations will not prevent us from being able to fix up template // part indices. // NOTE: collecting styles is inefficient for browsers but ShadyCSS // currently does this anyway. When it does not, this should be changed. for (let i = 0; i < length; i++) { const style = styles[i]; style.parentNode!.removeChild(style); condensedStyle.textContent! += style.textContent; } // Remove styles from nested templates in this scope. removeStylesFromLitTemplates(scopeName); // And then put the condensed style into the "root" template passed in as // `template`. const content = templateElement.content; if (!!template) { insertNodeIntoTemplate(template, condensedStyle, content.firstChild); } else { content.insertBefore(condensedStyle, content.firstChild); } // Note, it's important that ShadyCSS gets the template that `lit-html` // will actually render so that it can update the style inside when // needed (e.g. @apply native Shadow DOM case). window.ShadyCSS!.prepareTemplateStyles(templateElement, scopeName); const style = content.querySelector('style'); if (window.ShadyCSS!.nativeShadow && style !== null) { // When in native Shadow DOM, ensure the style created by ShadyCSS is // included in initially rendered output (`renderedDOM`). renderedDOM.insertBefore(style.cloneNode(true), renderedDOM.firstChild); } else if (!!template) { // When no style is left in the template, parts will be broken as a // result. To fix this, we put back the style node ShadyCSS removed // and then tell lit to remove that node from the template. // There can be no style in the template in 2 cases (1) when Shady DOM // is in use, ShadyCSS removes all styles, (2) when native Shadow DOM // is in use ShadyCSS removes the style if it contains no content. // NOTE, ShadyCSS creates its own style so we can safely add/remove // `condensedStyle` here. content.insertBefore(condensedStyle, content.firstChild); const removes = new Set<Node>(); removes.add(condensedStyle); removeNodesFromTemplate(template, removes); } }; export interface ShadyRenderOptions extends Partial<RenderOptions> { scopeName: string; } /** * Extension to the standard `render` method which supports rendering * to ShadowRoots when the ShadyDOM (https://github.com/webcomponents/shadydom) * and ShadyCSS (https://github.com/webcomponents/shadycss) polyfills are used * or when the webcomponentsjs * (https://github.com/webcomponents/webcomponentsjs) polyfill is used. * * Adds a `scopeName` option which is used to scope element DOM and stylesheets * when native ShadowDOM is unavailable. The `scopeName` will be added to * the class attribute of all rendered DOM. In addition, any style elements will * be automatically re-written with this `scopeName` selector and moved out * of the rendered DOM and into the document `<head>`. * * It is common to use this render method in conjunction with a custom element * which renders a shadowRoot. When this is done, typically the element's * `localName` should be used as the `scopeName`. * * In addition to DOM scoping, ShadyCSS also supports a basic shim for css * custom properties (needed only on older browsers like IE11) and a shim for * a deprecated feature called `@apply` that supports applying a set of css * custom properties to a given location. * * Usage considerations: * * * Part values in `<style>` elements are only applied the first time a given * `scopeName` renders. Subsequent changes to parts in style elements will have * no effect. Because of this, parts in style elements should only be used for * values that will never change, for example parts that set scope-wide theme * values or parts which render shared style elements. * * * Note, due to a limitation of the ShadyDOM polyfill, rendering in a * custom element's `constructor` is not supported. Instead rendering should * either done asynchronously, for example at microtask timing (for example * `Promise.resolve()`), or be deferred until the first time the element's * `connectedCallback` runs. * * Usage considerations when using shimmed custom properties or `@apply`: * * * Whenever any dynamic changes are made which affect * css custom properties, `ShadyCSS.styleElement(element)` must be called * to update the element. There are two cases when this is needed: * (1) the element is connected to a new parent, (2) a class is added to the * element that causes it to match different custom properties. * To address the first case when rendering a custom element, `styleElement` * should be called in the element's `connectedCallback`. * * * Shimmed custom properties may only be defined either for an entire * shadowRoot (for example, in a `:host` rule) or via a rule that directly * matches an element with a shadowRoot. In other words, instead of flowing from * parent to child as do native css custom properties, shimmed custom properties * flow only from shadowRoots to nested shadowRoots. * * * When using `@apply` mixing css shorthand property names with * non-shorthand names (for example `border` and `border-width`) is not * supported. */ export const render = (result: unknown, container: Element|DocumentFragment|ShadowRoot, options: ShadyRenderOptions) => { if (!options || typeof options !== 'object' || !options.scopeName) { throw new Error('The `scopeName` option is required.'); } const scopeName = options.scopeName; const hasRendered = parts.has(container); const needsScoping = compatibleShadyCSSVersion && container.nodeType === 11 /* Node.DOCUMENT_FRAGMENT_NODE */ && !!(container as ShadowRoot).host; // Handle first render to a scope specially... const firstScopeRender = needsScoping && !shadyRenderSet.has(scopeName); // On first scope render, render into a fragment; this cannot be a single // fragment that is reused since nested renders can occur synchronously. const renderContainer = firstScopeRender ? document.createDocumentFragment() : container; litRender( result, renderContainer, {templateFactory: shadyTemplateFactory(scopeName), ...options} as RenderOptions); // When performing first scope render, // (1) We've rendered into a fragment so that there's a chance to // `prepareTemplateStyles` before sub-elements hit the DOM // (which might cause them to render based on a common pattern of // rendering in a custom element's `connectedCallback`); // (2) Scope the template with ShadyCSS one time only for this scope. // (3) Render the fragment into the container and make sure the // container knows its `part` is the one we just rendered. This ensures // DOM will be re-used on subsequent renders. if (firstScopeRender) { const part = parts.get(renderContainer)!; parts.delete(renderContainer); // ShadyCSS might have style sheets (e.g. from `prepareAdoptedCssText`) // that should apply to `renderContainer` even if the rendered value is // not a TemplateInstance. However, it will only insert scoped styles // into the document if `prepareTemplateStyles` has already been called // for the given scope name. const template = part.value instanceof TemplateInstance ? part.value.template : undefined; prepareTemplateStyles( scopeName, renderContainer as DocumentFragment, template); removeNodes(container, container.firstChild); container.appendChild(renderContainer); parts.set(container, part); } // After elements have hit the DOM, update styling if this is the // initial render to this container. // This is needed whenever dynamic changes are made so it would be // safest to do every render; however, this would regress performance // so we leave it up to the user to call `ShadyCSS.styleElement` // for dynamic changes. if (!hasRendered && needsScoping) { window.ShadyCSS!.styleElement((container as ShadowRoot).host); } };