287 lines
14 KiB
JavaScript
287 lines
14 KiB
JavaScript
/**
|
|
* @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.
|
|
*
|
|
* @module shady-render
|
|
* @preferred
|
|
*/
|
|
/**
|
|
* 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}
|
|
*/
|
|
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
|