| /** |
| * @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 { parts, render as litRender } from './render.js'; |
| import { templateCaches } from './template-factory.js'; |
| import { TemplateInstance } from './template-instance.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, scopeName) => `${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) => (result) => { |
| const cacheKey = getTemplateCacheKey(result.type, scopeName); |
| let templateCache = templateCaches.get(cacheKey); |
| if (templateCache === undefined) { |
| templateCache = { |
| stringsArray: new WeakMap(), |
| keyString: new Map() |
| }; |
| 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) => { |
| 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(); |
| Array.from(content.querySelectorAll('style')).forEach((s) => { |
| styles.add(s); |
| }); |
| removeNodesFromTemplate(template, styles); |
| }); |
| } |
| }); |
| }; |
| const shadyRenderSet = new Set(); |
| /** |
| * 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, renderedDOM, 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(); |
| removes.add(condensedStyle); |
| removeNodesFromTemplate(template, removes); |
| } |
| }; |
| /** |
| * 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, container, options) => { |
| 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.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, Object.assign({ templateFactory: shadyTemplateFactory(scopeName) }, options)); |
| // 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, 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.host); |
| } |
| }; |
| //# sourceMappingURL=shady-render.js.map |