lit-html
Advanced tools
Comparing version 1.3.0 to 2.0.0-pre.1
@@ -20,2 +20,26 @@ # Change Log | ||
## [2.0.0-pre.1] - 2020-09-21 | ||
### Changed | ||
* [Breaking] `render()` no longer clears the container it's rendered to. It now appends to the container by default. | ||
* [Breaking] Expressions in comments are not rendered or updated. | ||
* [Breaking] Template caching happens per callsite, not per template-tag/callsize pair. This means some rare forms of highly dynamic template tags are no longer supported. | ||
* [Breaking] Arrays and other iterables passed to attribute bindings are not specially handled. Arrays will be rendered with their default toString representation. This means that ```html`<div class=${['a', 'b']}>``` will render `<div class="a,b">` instead of `<div class="a b">`. To get the old behavior, use `array.join(' ')`. | ||
* Multiple bindings in a single attribute value don't require the attribute value is quoted, as long as there is no whitespace or other attribute-ending character in the attribute value. ```html`<div id=${a}-${b}>``` | ||
* [Breaking] The directive and part APIs are significantly different. See the [README](README.md) for mroe details. | ||
### Added | ||
* Added `renderBefore` to render options. If specified, content is rendered before the node given via render options, e.g. `{renderBefore: node}`. | ||
### Fixed | ||
* All usage of `instanceof` has been removed, making rendering more likely to | ||
work when multiple instances of the library interact. | ||
* Template processing is more robust to expressions in places other than text and attribute values. | ||
### Removed | ||
* [Breaking] The `templateFactory` option of `RenderOptions` has been removed. | ||
* [Breaking] TemplateProcessor has been removed. | ||
* [Breaking] Symbols are not converted to a string before mutating DOM, so passing a Symbol to an attribute or text binding will result in an exception. | ||
* [Breaking] The `until`, `asyncAppend` and `asyncReplace` directives are not implemented. | ||
## [1.3.0] - 2020-08-19 | ||
@@ -22,0 +46,0 @@ |
/** | ||
* @license | ||
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved. | ||
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved. | ||
* This code may only be used under the BSD style license found at | ||
@@ -14,3 +14,4 @@ * http://polymer.github.io/LICENSE.txt | ||
*/ | ||
import { Part } from '../lit-html.js'; | ||
import { TemplateResult, NodePart } from '../lit-html.js'; | ||
import { NodePartState } from '../parts.js'; | ||
/** | ||
@@ -30,3 +31,13 @@ * Enables fast switching between multiple templates by caching the DOM nodes | ||
*/ | ||
export declare const cache: (value: unknown) => (part: Part) => void; | ||
export declare const cache: (v: unknown) => { | ||
_$litDirective$: { | ||
new (): { | ||
templateCache: WeakMap<TemplateStringsArray, NodePartState>; | ||
value?: TemplateResult | undefined; | ||
render(v: unknown): unknown; | ||
update(part: NodePart, [v]: [v: unknown]): unknown; | ||
}; | ||
}; | ||
values: [v: unknown]; | ||
}; | ||
//# sourceMappingURL=cache.d.ts.map |
@@ -0,4 +1,5 @@ | ||
import{directive as t,Directive as s}from"../lit-html.js";import{detachNodePart as i,restoreNodePart as r}from"../parts.js"; | ||
/** | ||
* @license | ||
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved. | ||
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved. | ||
* This code may only be used under the BSD style license found at | ||
@@ -13,66 +14,3 @@ * http://polymer.github.io/LICENSE.txt | ||
* http://polymer.github.io/PATENTS.txt | ||
*/ | ||
import { TemplateInstance } from '../lib/template-instance.js'; | ||
import { directive, NodePart, reparentNodes, TemplateResult } from '../lit-html.js'; | ||
const templateCaches = new WeakMap(); | ||
/** | ||
* Enables fast switching between multiple templates by caching the DOM nodes | ||
* and TemplateInstances produced by the templates. | ||
* | ||
* Example: | ||
* | ||
* ``` | ||
* let checked = false; | ||
* | ||
* html` | ||
* ${cache(checked ? html`input is checked` : html`input is not checked`)} | ||
* ` | ||
* ``` | ||
*/ | ||
export const cache = directive((value) => (part) => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('cache can only be used in text bindings'); | ||
} | ||
let templateCache = templateCaches.get(part); | ||
if (templateCache === undefined) { | ||
templateCache = new WeakMap(); | ||
templateCaches.set(part, templateCache); | ||
} | ||
const previousValue = part.value; | ||
// First, can we update the current TemplateInstance, or do we need to move | ||
// the current nodes into the cache? | ||
if (previousValue instanceof TemplateInstance) { | ||
if (value instanceof TemplateResult && | ||
previousValue.template === part.options.templateFactory(value)) { | ||
// Same Template, just trigger an update of the TemplateInstance | ||
part.setValue(value); | ||
return; | ||
} | ||
else { | ||
// Not the same Template, move the nodes from the DOM into the cache. | ||
let cachedTemplate = templateCache.get(previousValue.template); | ||
if (cachedTemplate === undefined) { | ||
cachedTemplate = { | ||
instance: previousValue, | ||
nodes: document.createDocumentFragment(), | ||
}; | ||
templateCache.set(previousValue.template, cachedTemplate); | ||
} | ||
reparentNodes(cachedTemplate.nodes, part.startNode.nextSibling, part.endNode); | ||
} | ||
} | ||
// Next, can we reuse nodes from the cache? | ||
if (value instanceof TemplateResult) { | ||
const template = part.options.templateFactory(value); | ||
const cachedTemplate = templateCache.get(template); | ||
if (cachedTemplate !== undefined) { | ||
// Move nodes out of cache | ||
part.setValue(cachedTemplate.nodes); | ||
part.commit(); | ||
// Set the Part value to the TemplateInstance so it'll update it. | ||
part.value = cachedTemplate.instance; | ||
} | ||
} | ||
part.setValue(value); | ||
}); | ||
//# sourceMappingURL=cache.js.map | ||
*/const e=t(class extends s{constructor(){super(...arguments),this.templateCache=new WeakMap}render(t){return t}update(t,[s]){if(void 0!==this.value&&this.value.strings!==s.strings&&this.templateCache.set(this.value.strings,i(t)),void 0!==s._$litType$){let i=this.templateCache.get(s.strings);void 0!==i&&r(t,i),this.value=s}else this.value=void 0;return this.render(s)}});export{e as cache}; | ||
//# sourceMappingURL=cache.js.map |
@@ -14,16 +14,38 @@ /** | ||
*/ | ||
import { Part } from '../lit-html.js'; | ||
import { AttributePart, Directive, PartInfo } from '../lit-html.js'; | ||
/** | ||
* A key-value set of class names to truthy values. | ||
*/ | ||
export interface ClassInfo { | ||
readonly [name: string]: string | boolean | number; | ||
} | ||
declare class ClassMap extends Directive { | ||
/** | ||
* Stores the ClassInfo object applied to a given AttributePart. | ||
* Used to unset existing values when a new ClassInfo object is applied. | ||
*/ | ||
previousClasses?: Set<string>; | ||
constructor(part: PartInfo); | ||
render(classInfo: ClassInfo): string; | ||
update(part: AttributePart, [classInfo]: [ClassInfo]): {}; | ||
} | ||
/** | ||
* A directive that applies CSS classes. This must be used in the `class` | ||
* attribute and must be the only part used in the attribute. It takes each | ||
* property in the `classInfo` argument and adds the property name to the | ||
* element's `class` if the property value is truthy; if the property value is | ||
* falsey, the property name is removed from the element's `class`. For example | ||
* `{foo: bar}` applies the class `foo` if the value of `bar` is truthy. | ||
* A directive that applies dynamic CSS classes. | ||
* | ||
* This must be used in the `class` attribute and must be the only part used in | ||
* the attribute. It takes each property in the `classInfo` argument and adds | ||
* the property name to the element's `classList` if the property value is | ||
* truthy; if the property value is falsey, the property name is removed from | ||
* the element's `class`. | ||
* | ||
* For example `{foo: bar}` applies the class `foo` if the value of `bar` is | ||
* truthy. | ||
* | ||
* @param classInfo {ClassInfo} | ||
*/ | ||
export declare const classMap: (classInfo: ClassInfo) => (part: Part) => void; | ||
export declare const classMap: (classInfo: ClassInfo) => { | ||
_$litDirective$: typeof ClassMap; | ||
values: [classInfo: ClassInfo]; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=class-map.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import{directive as t,Directive as s,ATTRIBUTE_PART as e,noChange as i}from"../lit-html.js"; | ||
/** | ||
@@ -13,90 +14,3 @@ * @license | ||
* http://polymer.github.io/PATENTS.txt | ||
*/ | ||
import { AttributePart, directive, PropertyPart } from '../lit-html.js'; | ||
// IE11 doesn't support classList on SVG elements, so we emulate it with a Set | ||
class ClassList { | ||
constructor(element) { | ||
this.classes = new Set(); | ||
this.changed = false; | ||
this.element = element; | ||
const classList = (element.getAttribute('class') || '').split(/\s+/); | ||
for (const cls of classList) { | ||
this.classes.add(cls); | ||
} | ||
} | ||
add(cls) { | ||
this.classes.add(cls); | ||
this.changed = true; | ||
} | ||
remove(cls) { | ||
this.classes.delete(cls); | ||
this.changed = true; | ||
} | ||
commit() { | ||
if (this.changed) { | ||
let classString = ''; | ||
this.classes.forEach((cls) => classString += cls + ' '); | ||
this.element.setAttribute('class', classString); | ||
} | ||
} | ||
} | ||
/** | ||
* Stores the ClassInfo object applied to a given AttributePart. | ||
* Used to unset existing values when a new ClassInfo object is applied. | ||
*/ | ||
const previousClassesCache = new WeakMap(); | ||
/** | ||
* A directive that applies CSS classes. This must be used in the `class` | ||
* attribute and must be the only part used in the attribute. It takes each | ||
* property in the `classInfo` argument and adds the property name to the | ||
* element's `class` if the property value is truthy; if the property value is | ||
* falsey, the property name is removed from the element's `class`. For example | ||
* `{foo: bar}` applies the class `foo` if the value of `bar` is truthy. | ||
* @param classInfo {ClassInfo} | ||
*/ | ||
export const classMap = directive((classInfo) => (part) => { | ||
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) || | ||
part.committer.name !== 'class' || part.committer.parts.length > 1) { | ||
throw new Error('The `classMap` directive must be used in the `class` attribute ' + | ||
'and must be the only part in the attribute.'); | ||
} | ||
const { committer } = part; | ||
const { element } = committer; | ||
let previousClasses = previousClassesCache.get(part); | ||
if (previousClasses === undefined) { | ||
// Write static classes once | ||
// Use setAttribute() because className isn't a string on SVG elements | ||
element.setAttribute('class', committer.strings.join(' ')); | ||
previousClassesCache.set(part, previousClasses = new Set()); | ||
} | ||
const classList = (element.classList || new ClassList(element)); | ||
// Remove old classes that no longer apply | ||
// We use forEach() instead of for-of so that re don't require down-level | ||
// iteration. | ||
previousClasses.forEach((name) => { | ||
if (!(name in classInfo)) { | ||
classList.remove(name); | ||
previousClasses.delete(name); | ||
} | ||
}); | ||
// Add or remove classes based on their classMap value | ||
for (const name in classInfo) { | ||
const value = classInfo[name]; | ||
if (value != previousClasses.has(name)) { | ||
// We explicitly want a loose truthy check of `value` because it seems | ||
// more convenient that '' and 0 are skipped. | ||
if (value) { | ||
classList.add(name); | ||
previousClasses.add(name); | ||
} | ||
else { | ||
classList.remove(name); | ||
previousClasses.delete(name); | ||
} | ||
} | ||
} | ||
if (typeof classList.commit === 'function') { | ||
classList.commit(); | ||
} | ||
}); | ||
//# sourceMappingURL=class-map.js.map | ||
*/const r=t(class extends s{constructor(t){if(super(),t.type!==e||"class"!==t.name||void 0!==t.strings&&t.strings.length>2)throw Error("The `classMap` directive must be used in the `class` attribute and must be the only part in the attribute.")}render(t){return Object.keys(t).filter(s=>t[s]).join(" ")}update(t,[s]){if(void 0===this.previousClasses){this.previousClasses=new Set;for(const t in s)s[t]&&this.previousClasses.add(t);return this.render(s)}const e=t.element.classList;this.previousClasses.forEach(t=>{t in s||(e.remove(t),this.previousClasses.delete(t))});for(const t in s){const i=!!s[t];i!==this.previousClasses.has(t)&&(i?(e.add(t),this.previousClasses.add(t)):(e.remove(t),this.previousClasses.delete(t)))}return i}});export{r as classMap}; | ||
//# sourceMappingURL=class-map.js.map |
@@ -14,3 +14,8 @@ /** | ||
*/ | ||
import { Part } from '../lit-html.js'; | ||
import { Directive, Part } from '../lit-html.js'; | ||
declare class Guard extends Directive { | ||
previousValue: unknown; | ||
render(_value: unknown, f: () => unknown): unknown; | ||
update(_part: Part, [value, f]: Parameters<this['render']>): unknown; | ||
} | ||
/** | ||
@@ -20,2 +25,8 @@ * Prevents re-render of a template function until a single value or an array of | ||
* | ||
* Values are checked against previous values with strict equality (`===`), and | ||
* so the check won't detect nested property changes inside objects or arrays. | ||
* Arrays values have each item checked against the previous value at the same | ||
* index with strict equality. Nested arrays are also checked only by strict | ||
* equality. | ||
* | ||
* Example: | ||
@@ -30,3 +41,3 @@ * | ||
* | ||
* In this case, the template only renders if either `user.id` or `company.id` | ||
* In this case, the template only rerenders if either `user.id` or `company.id` | ||
* changes. | ||
@@ -51,3 +62,7 @@ * | ||
*/ | ||
export declare const guard: (value: unknown, f: () => unknown) => (part: Part) => void; | ||
export declare const guard: (_value: unknown, f: () => unknown) => { | ||
_$litDirective$: typeof Guard; | ||
values: [_value: unknown, f: () => unknown]; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=guard.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import{directive as r,Directive as t,noChange as s}from"../lit-html.js"; | ||
/** | ||
@@ -14,57 +15,3 @@ * @license | ||
*/ | ||
import { directive } from '../lit-html.js'; | ||
const previousValues = new WeakMap(); | ||
/** | ||
* Prevents re-render of a template function until a single value or an array of | ||
* values changes. | ||
* | ||
* Example: | ||
* | ||
* ```js | ||
* html` | ||
* <div> | ||
* ${guard([user.id, company.id], () => html`...`)} | ||
* </div> | ||
* ``` | ||
* | ||
* In this case, the template only renders if either `user.id` or `company.id` | ||
* changes. | ||
* | ||
* guard() is useful with immutable data patterns, by preventing expensive work | ||
* until data updates. | ||
* | ||
* Example: | ||
* | ||
* ```js | ||
* html` | ||
* <div> | ||
* ${guard([immutableItems], () => immutableItems.map(i => html`${i}`))} | ||
* </div> | ||
* ``` | ||
* | ||
* In this case, items are mapped over only when the array reference changes. | ||
* | ||
* @param value the value to check before re-rendering | ||
* @param f the template function | ||
*/ | ||
export const guard = directive((value, f) => (part) => { | ||
const previousValue = previousValues.get(part); | ||
if (Array.isArray(value)) { | ||
// Dirty-check arrays by item | ||
if (Array.isArray(previousValue) && | ||
previousValue.length === value.length && | ||
value.every((v, i) => v === previousValue[i])) { | ||
return; | ||
} | ||
} | ||
else if (previousValue === value && | ||
(value !== undefined || previousValues.has(part))) { | ||
// Dirty-check non-arrays by identity | ||
return; | ||
} | ||
part.setValue(f()); | ||
// Copy the value if it's an array so that if it's mutated we don't forget | ||
// what the previous values were. | ||
previousValues.set(part, Array.isArray(value) ? Array.from(value) : value); | ||
}); | ||
//# sourceMappingURL=guard.js.map | ||
const e={},i=r(class extends t{constructor(){super(...arguments),this.previousValue=e}render(r,t){return t()}update(r,[t,e]){if(Array.isArray(t)){if(Array.isArray(this.previousValue)&&this.previousValue.length===t.length&&t.every((r,t)=>r===this.previousValue[t]))return s}else if(this.previousValue===t)return s;return this.previousValue=Array.isArray(t)?Array.from(t):t,this.render(t,e)}});export{i as guard}; | ||
//# sourceMappingURL=guard.js.map |
@@ -14,3 +14,2 @@ /** | ||
*/ | ||
import { Part } from '../lit-html.js'; | ||
/** | ||
@@ -22,3 +21,3 @@ * For AttributeParts, sets the attribute if the value is defined and removes | ||
*/ | ||
export declare const ifDefined: (value: unknown) => (part: Part) => void; | ||
export declare const ifDefined: (value: unknown) => unknown; | ||
//# sourceMappingURL=if-defined.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import{nothing as t}from"../lit-html.js"; | ||
/** | ||
@@ -13,26 +14,3 @@ * @license | ||
* http://polymer.github.io/PATENTS.txt | ||
*/ | ||
import { AttributePart, directive } from '../lit-html.js'; | ||
const previousValues = new WeakMap(); | ||
/** | ||
* For AttributeParts, sets the attribute if the value is defined and removes | ||
* the attribute if the value is undefined. | ||
* | ||
* For other part types, this directive is a no-op. | ||
*/ | ||
export const ifDefined = directive((value) => (part) => { | ||
const previousValue = previousValues.get(part); | ||
if (value === undefined && part instanceof AttributePart) { | ||
// If the value is undefined, remove the attribute, but only if the value | ||
// was previously defined. | ||
if (previousValue !== undefined || !previousValues.has(part)) { | ||
const name = part.committer.name; | ||
part.committer.element.removeAttribute(name); | ||
} | ||
} | ||
else if (value !== previousValue) { | ||
part.setValue(value); | ||
} | ||
previousValues.set(part, value); | ||
}); | ||
//# sourceMappingURL=if-defined.js.map | ||
*/const l=l=>null!=l?l:t;export{l as ifDefined}; | ||
//# sourceMappingURL=if-defined.js.map |
@@ -14,3 +14,8 @@ /** | ||
*/ | ||
import { AttributePart, BooleanAttributePart, PropertyPart } from '../lit-html.js'; | ||
import { Directive, AttributePart, PartInfo } from '../lit-html.js'; | ||
declare class LiveDirective extends Directive { | ||
constructor(part: PartInfo); | ||
render(value: unknown): unknown; | ||
update(part: AttributePart, [value]: Parameters<this['render']>): unknown; | ||
} | ||
/** | ||
@@ -38,3 +43,7 @@ * Checks binding values against live DOM values, instead of previously bound | ||
*/ | ||
export declare const live: (value: unknown) => (part: AttributePart | BooleanAttributePart | PropertyPart) => void; | ||
export declare const live: (value: unknown) => { | ||
_$litDirective$: typeof LiveDirective; | ||
values: [value: unknown]; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=live.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import{directive as r,Directive as e,EVENT_PART as n,NODE_PART as t,noChange as i,PROPERTY_PART as o,BOOLEAN_ATTRIBUTE_PART as s,nothing as l,ATTRIBUTE_PART as f}from"../lit-html.js"; | ||
/** | ||
@@ -14,61 +15,3 @@ * @license | ||
*/ | ||
import { AttributePart, BooleanAttributePart, directive, EventPart, NodePart, PropertyPart } from '../lit-html.js'; | ||
/** | ||
* Checks binding values against live DOM values, instead of previously bound | ||
* values, when determining whether to update the value. | ||
* | ||
* This is useful for cases where the DOM value may change from outside of | ||
* lit-html, such as with a binding to an `<input>` element's `value` property, | ||
* a content editable elements text, or to a custom element that changes it's | ||
* own properties or attributes. | ||
* | ||
* In these cases if the DOM value changes, but the value set through lit-html | ||
* bindings hasn't, lit-html won't know to update the DOM value and will leave | ||
* it alone. If this is not what you want—if you want to overwrite the DOM | ||
* value with the bound value no matter what—use the `live()` directive: | ||
* | ||
* html`<input .value=${live(x)}>` | ||
* | ||
* `live()` performs a strict equality check agains the live DOM value, and if | ||
* the new value is equal to the live value, does nothing. This means that | ||
* `live()` should not be used when the binding will cause a type conversion. If | ||
* you use `live()` with an attribute binding, make sure that only strings are | ||
* passed in, or the binding will update every render. | ||
*/ | ||
export const live = directive((value) => (part) => { | ||
let previousValue; | ||
if (part instanceof EventPart || part instanceof NodePart) { | ||
throw new Error('The `live` directive is not allowed on text or event bindings'); | ||
} | ||
if (part instanceof BooleanAttributePart) { | ||
checkStrings(part.strings); | ||
previousValue = part.element.hasAttribute(part.name); | ||
// This is a hack needed because BooleanAttributePart doesn't have a | ||
// committer and does its own dirty checking after directives | ||
part.value = previousValue; | ||
} | ||
else { | ||
const { element, name, strings } = part.committer; | ||
checkStrings(strings); | ||
if (part instanceof PropertyPart) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
previousValue = element[name]; | ||
if (previousValue === value) { | ||
return; | ||
} | ||
} | ||
else if (part instanceof AttributePart) { | ||
previousValue = element.getAttribute(name); | ||
} | ||
if (previousValue === String(value)) { | ||
return; | ||
} | ||
} | ||
part.setValue(value); | ||
}); | ||
const checkStrings = (strings) => { | ||
if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') { | ||
throw new Error('`live` bindings can only contain a single expression'); | ||
} | ||
}; | ||
//# sourceMappingURL=live.js.map | ||
const u={},c=r(class extends e{constructor(r){if(super(),r.type===n||r.type===t)throw Error("The `live` directive is not allowed on text or event bindings");if(void 0!==r.strings)throw Error("`live` bindings can only contain a single expression")}render(r){return r}update(r,[e]){if(e===i)return e;const n=r.element,t=r.name;if(r.type===o){if(e===n[t])return i}else if(r.type===s){if((e!==l&&!!e)===n.hasAttribute(t))return i}else if(r.type===f&&n.getAttribute(t)===e+"")return i;return r._value=u,e}});export{c as live}; | ||
//# sourceMappingURL=live.js.map |
@@ -14,25 +14,6 @@ /** | ||
*/ | ||
import { DirectiveFn } from '../lib/directive.js'; | ||
export declare type KeyFn<T> = (item: T, index: number) => unknown; | ||
export declare type ItemTemplate<T> = (item: T, index: number) => unknown; | ||
/** | ||
* A directive that repeats a series of values (usually `TemplateResults`) | ||
* generated from an iterable, and updates those items efficiently when the | ||
* iterable changes based on user-provided `keys` associated with each item. | ||
* | ||
* Note that if a `keyFn` is provided, strict key-to-DOM mapping is maintained, | ||
* meaning previous DOM for a given key is moved into the new position if | ||
* needed, and DOM will never be reused with values for different keys (new DOM | ||
* will always be created for new keys). This is generally the most efficient | ||
* way to use `repeat` since it performs minimum unnecessary work for insertions | ||
* and removals. | ||
* | ||
* IMPORTANT: If providing a `keyFn`, keys *must* be unique for all items in a | ||
* given call to `repeat`. The behavior when two or more items have the same key | ||
* is undefined. | ||
* | ||
* If no `keyFn` is provided, this directive will perform similar to mapping | ||
* items to values, and DOM will be reused against potentially different items. | ||
*/ | ||
export declare const repeat: <T>(items: Iterable<T>, keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>, template?: ItemTemplate<T> | undefined) => DirectiveFn; | ||
export declare type RepeatDirectiveFn = <T>(items: Iterable<T>, keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>, template?: ItemTemplate<T>) => unknown; | ||
export declare const repeat: RepeatDirectiveFn; | ||
//# sourceMappingURL=repeat.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import{directive as e,Directive as s,NodePart as t,noChange as l}from"../lit-html.js";import{getPartValue as n,setPartValue as o,insertPartBefore as r,removePart as i,createAndInsertPart as f}from"../parts.js"; | ||
/** | ||
@@ -14,403 +15,3 @@ * @license | ||
*/ | ||
import { createMarker, directive, NodePart, removeNodes, reparentNodes } from '../lit-html.js'; | ||
// Helper functions for manipulating parts | ||
// TODO(kschaaf): Refactor into Part API? | ||
const createAndInsertPart = (containerPart, beforePart) => { | ||
const container = containerPart.startNode.parentNode; | ||
const beforeNode = beforePart === undefined ? containerPart.endNode : | ||
beforePart.startNode; | ||
const startNode = container.insertBefore(createMarker(), beforeNode); | ||
container.insertBefore(createMarker(), beforeNode); | ||
const newPart = new NodePart(containerPart.options); | ||
newPart.insertAfterNode(startNode); | ||
return newPart; | ||
}; | ||
const updatePart = (part, value) => { | ||
part.setValue(value); | ||
part.commit(); | ||
return part; | ||
}; | ||
const insertPartBefore = (containerPart, part, ref) => { | ||
const container = containerPart.startNode.parentNode; | ||
const beforeNode = ref ? ref.startNode : containerPart.endNode; | ||
const endNode = part.endNode.nextSibling; | ||
if (endNode !== beforeNode) { | ||
reparentNodes(container, part.startNode, endNode, beforeNode); | ||
} | ||
}; | ||
const removePart = (part) => { | ||
removeNodes(part.startNode.parentNode, part.startNode, part.endNode.nextSibling); | ||
}; | ||
// Helper for generating a map of array item to its index over a subset | ||
// of an array (used to lazily generate `newKeyToIndexMap` and | ||
// `oldKeyToIndexMap`) | ||
const generateMap = (list, start, end) => { | ||
const map = new Map(); | ||
for (let i = start; i <= end; i++) { | ||
map.set(list[i], i); | ||
} | ||
return map; | ||
}; | ||
// Stores previous ordered list of parts and map of key to index | ||
const partListCache = new WeakMap(); | ||
const keyListCache = new WeakMap(); | ||
/** | ||
* A directive that repeats a series of values (usually `TemplateResults`) | ||
* generated from an iterable, and updates those items efficiently when the | ||
* iterable changes based on user-provided `keys` associated with each item. | ||
* | ||
* Note that if a `keyFn` is provided, strict key-to-DOM mapping is maintained, | ||
* meaning previous DOM for a given key is moved into the new position if | ||
* needed, and DOM will never be reused with values for different keys (new DOM | ||
* will always be created for new keys). This is generally the most efficient | ||
* way to use `repeat` since it performs minimum unnecessary work for insertions | ||
* and removals. | ||
* | ||
* IMPORTANT: If providing a `keyFn`, keys *must* be unique for all items in a | ||
* given call to `repeat`. The behavior when two or more items have the same key | ||
* is undefined. | ||
* | ||
* If no `keyFn` is provided, this directive will perform similar to mapping | ||
* items to values, and DOM will be reused against potentially different items. | ||
*/ | ||
export const repeat = directive((items, keyFnOrTemplate, template) => { | ||
let keyFn; | ||
if (template === undefined) { | ||
template = keyFnOrTemplate; | ||
} | ||
else if (keyFnOrTemplate !== undefined) { | ||
keyFn = keyFnOrTemplate; | ||
} | ||
return (containerPart) => { | ||
if (!(containerPart instanceof NodePart)) { | ||
throw new Error('repeat can only be used in text bindings'); | ||
} | ||
// Old part & key lists are retrieved from the last update | ||
// (associated with the part for this instance of the directive) | ||
const oldParts = partListCache.get(containerPart) || []; | ||
const oldKeys = keyListCache.get(containerPart) || []; | ||
// New part list will be built up as we go (either reused from | ||
// old parts or created for new keys in this update). This is | ||
// saved in the above cache at the end of the update. | ||
const newParts = []; | ||
// New value list is eagerly generated from items along with a | ||
// parallel array indicating its key. | ||
const newValues = []; | ||
const newKeys = []; | ||
let index = 0; | ||
for (const item of items) { | ||
newKeys[index] = keyFn ? keyFn(item, index) : index; | ||
newValues[index] = template(item, index); | ||
index++; | ||
} | ||
// Maps from key to index for current and previous update; these | ||
// are generated lazily only when needed as a performance | ||
// optimization, since they are only required for multiple | ||
// non-contiguous changes in the list, which are less common. | ||
let newKeyToIndexMap; | ||
let oldKeyToIndexMap; | ||
// Head and tail pointers to old parts and new values | ||
let oldHead = 0; | ||
let oldTail = oldParts.length - 1; | ||
let newHead = 0; | ||
let newTail = newValues.length - 1; | ||
// Overview of O(n) reconciliation algorithm (general approach | ||
// based on ideas found in ivi, vue, snabbdom, etc.): | ||
// | ||
// * We start with the list of old parts and new values (and | ||
// arrays of their respective keys), head/tail pointers into | ||
// each, and we build up the new list of parts by updating | ||
// (and when needed, moving) old parts or creating new ones. | ||
// The initial scenario might look like this (for brevity of | ||
// the diagrams, the numbers in the array reflect keys | ||
// associated with the old parts or new values, although keys | ||
// and parts/values are actually stored in parallel arrays | ||
// indexed using the same head/tail pointers): | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [ , , , , , , ] | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new | ||
// item order | ||
// newHead ^ ^ newTail | ||
// | ||
// * Iterate old & new lists from both sides, updating, | ||
// swapping, or removing parts at the head/tail locations | ||
// until neither head nor tail can move. | ||
// | ||
// * Example below: keys at head pointers match, so update old | ||
// part 0 in-place (no need to move it) and record part 0 in | ||
// the `newParts` list. The last thing we do is advance the | ||
// `oldHead` and `newHead` pointers (will be reflected in the | ||
// next diagram). | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , ] <- heads matched: update 0 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead | ||
// & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Example below: head pointers don't match, but tail | ||
// pointers do, so update part 6 in place (no need to move | ||
// it), and record part 6 in the `newParts` list. Last, | ||
// advance the `oldTail` and `oldHead` pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , 6] <- tails matched: update 6 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldTail | ||
// & newTail | ||
// newHead ^ ^ newTail | ||
// | ||
// * If neither head nor tail match; next check if one of the | ||
// old head/tail items was removed. We first need to generate | ||
// the reverse map of new keys to index (`newKeyToIndexMap`), | ||
// which is done once lazily as a performance optimization, | ||
// since we only hit this case if multiple non-contiguous | ||
// changes were made. Note that for contiguous removal | ||
// anywhere in the list, the head and tails would advance | ||
// from either end and pass each other before we get to this | ||
// case and removals would be handled in the final while loop | ||
// without needing to generate the map. | ||
// | ||
// * Example below: The key at `oldTail` was removed (no longer | ||
// in the `newKeyToIndexMap`), so remove that part from the | ||
// DOM and advance just the `oldTail` pointer. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , 6] <- 5 not in new map: remove | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] 5 and advance oldTail | ||
// newHead ^ ^ newTail | ||
// | ||
// * Once head and tail cannot move, any mismatches are due to | ||
// either new or moved items; if a new key is in the previous | ||
// "old key to old index" map, move the old part to the new | ||
// location, otherwise create and insert a new part. Note | ||
// that when moving an old part we null its position in the | ||
// oldParts array if it lies between the head and tail so we | ||
// know to skip it when the pointers get there. | ||
// | ||
// * Example below: neither head nor tail match, and neither | ||
// were removed; so find the `newHead` key in the | ||
// `oldKeyToIndexMap`, and move that old part's DOM into the | ||
// next head position (before `oldParts[oldHead]`). Last, | ||
// null the part in the `oldPart` array since it was | ||
// somewhere in the remaining oldParts still to be scanned | ||
// (between the head and tail pointers) so that we know to | ||
// skip that old part on future iterations. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, , , , , 6] <- stuck: update & move 2 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] into place and advance | ||
// newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Note that for moves/insertions like the one above, a part | ||
// inserted at the head pointer is inserted before the | ||
// current `oldParts[oldHead]`, and a part inserted at the | ||
// tail pointer is inserted before `newParts[newTail+1]`. The | ||
// seeming asymmetry lies in the fact that new parts are | ||
// moved into place outside in, so to the right of the head | ||
// pointer are old parts, and to the right of the tail | ||
// pointer are new parts. | ||
// | ||
// * We always restart back from the top of the algorithm, | ||
// allowing matching and simple updates in place to | ||
// continue... | ||
// | ||
// * Example below: the head pointers once again match, so | ||
// simply update part 1 and record it in the `newParts` | ||
// array. Last, advance both head pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, , , , 6] <- heads matched: update 1 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead | ||
// & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * As mentioned above, items that were moved as a result of | ||
// being stuck (the final else clause in the code below) are | ||
// marked with null, so we always advance old pointers over | ||
// these so we're comparing the next actual old value on | ||
// either end. | ||
// | ||
// * Example below: `oldHead` is null (already placed in | ||
// newParts), so advance `oldHead`. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] <- old head already used: | ||
// newParts: [0, 2, 1, , , , 6] advance oldHead | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] | ||
// newHead ^ ^ newTail | ||
// | ||
// * Note it's not critical to mark old parts as null when they | ||
// are moved from head to tail or tail to head, since they | ||
// will be outside the pointer range and never visited again. | ||
// | ||
// * Example below: Here the old tail key matches the new head | ||
// key, so the part at the `oldTail` position and move its | ||
// DOM to the new head position (before `oldParts[oldHead]`). | ||
// Last, advance `oldTail` and `newHead` pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, , , 6] <- old tail matches new | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] head: update & move 4, | ||
// advance oldTail & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Example below: Old and new head keys match, so update the | ||
// old head part in place, and advance the `oldHead` and | ||
// `newHead` pointers. | ||
// | ||
// oldHead v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance oldHead & | ||
// newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Once the new or old pointers move past each other then all | ||
// we have left is additions (if old list exhausted) or | ||
// removals (if new list exhausted). Those are handled in the | ||
// final while loops at the end. | ||
// | ||
// * Example below: `oldHead` exceeded `oldTail`, so we're done | ||
// with the main loop. Create the remaining part and insert | ||
// it at the new head position, and the update is complete. | ||
// | ||
// (oldHead > oldTail) | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, 3, 7 ,6] <- create and insert 7 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] | ||
// newHead ^ newTail | ||
// | ||
// * Note that the order of the if/else clauses is not | ||
// important to the algorithm, as long as the null checks | ||
// come first (to ensure we're always working on valid old | ||
// parts) and that the final else clause comes last (since | ||
// that's where the expensive moves occur). The order of | ||
// remaining clauses is is just a simple guess at which cases | ||
// will be most common. | ||
// | ||
// * TODO(kschaaf) Note, we could calculate the longest | ||
// increasing subsequence (LIS) of old items in new position, | ||
// and only move those not in the LIS set. However that costs | ||
// O(nlogn) time and adds a bit more code, and only helps | ||
// make rare types of mutations require fewer moves. The | ||
// above handles removes, adds, reversal, swaps, and single | ||
// moves of contiguous items in linear time, in the minimum | ||
// number of moves. As the number of multiple moves where LIS | ||
// might help approaches a random shuffle, the LIS | ||
// optimization becomes less helpful, so it seems not worth | ||
// the code at this point. Could reconsider if a compelling | ||
// case arises. | ||
while (oldHead <= oldTail && newHead <= newTail) { | ||
if (oldParts[oldHead] === null) { | ||
// `null` means old part at head has already been used | ||
// below; skip | ||
oldHead++; | ||
} | ||
else if (oldParts[oldTail] === null) { | ||
// `null` means old part at tail has already been used | ||
// below; skip | ||
oldTail--; | ||
} | ||
else if (oldKeys[oldHead] === newKeys[newHead]) { | ||
// Old head matches new head; update in place | ||
newParts[newHead] = | ||
updatePart(oldParts[oldHead], newValues[newHead]); | ||
oldHead++; | ||
newHead++; | ||
} | ||
else if (oldKeys[oldTail] === newKeys[newTail]) { | ||
// Old tail matches new tail; update in place | ||
newParts[newTail] = | ||
updatePart(oldParts[oldTail], newValues[newTail]); | ||
oldTail--; | ||
newTail--; | ||
} | ||
else if (oldKeys[oldHead] === newKeys[newTail]) { | ||
// Old head matches new tail; update and move to new tail | ||
newParts[newTail] = | ||
updatePart(oldParts[oldHead], newValues[newTail]); | ||
insertPartBefore(containerPart, oldParts[oldHead], newParts[newTail + 1]); | ||
oldHead++; | ||
newTail--; | ||
} | ||
else if (oldKeys[oldTail] === newKeys[newHead]) { | ||
// Old tail matches new head; update and move to new head | ||
newParts[newHead] = | ||
updatePart(oldParts[oldTail], newValues[newHead]); | ||
insertPartBefore(containerPart, oldParts[oldTail], oldParts[oldHead]); | ||
oldTail--; | ||
newHead++; | ||
} | ||
else { | ||
if (newKeyToIndexMap === undefined) { | ||
// Lazily generate key-to-index maps, used for removals & | ||
// moves below | ||
newKeyToIndexMap = generateMap(newKeys, newHead, newTail); | ||
oldKeyToIndexMap = generateMap(oldKeys, oldHead, oldTail); | ||
} | ||
if (!newKeyToIndexMap.has(oldKeys[oldHead])) { | ||
// Old head is no longer in new list; remove | ||
removePart(oldParts[oldHead]); | ||
oldHead++; | ||
} | ||
else if (!newKeyToIndexMap.has(oldKeys[oldTail])) { | ||
// Old tail is no longer in new list; remove | ||
removePart(oldParts[oldTail]); | ||
oldTail--; | ||
} | ||
else { | ||
// Any mismatches at this point are due to additions or | ||
// moves; see if we have an old part we can reuse and move | ||
// into place | ||
const oldIndex = oldKeyToIndexMap.get(newKeys[newHead]); | ||
const oldPart = oldIndex !== undefined ? oldParts[oldIndex] : null; | ||
if (oldPart === null) { | ||
// No old part for this value; create a new one and | ||
// insert it | ||
const newPart = createAndInsertPart(containerPart, oldParts[oldHead]); | ||
updatePart(newPart, newValues[newHead]); | ||
newParts[newHead] = newPart; | ||
} | ||
else { | ||
// Reuse old part | ||
newParts[newHead] = | ||
updatePart(oldPart, newValues[newHead]); | ||
insertPartBefore(containerPart, oldPart, oldParts[oldHead]); | ||
// This marks the old part as having been used, so that | ||
// it will be skipped in the first two checks above | ||
oldParts[oldIndex] = null; | ||
} | ||
newHead++; | ||
} | ||
} | ||
} | ||
// Add parts for any remaining new values | ||
while (newHead <= newTail) { | ||
// For all remaining additions, we insert before last new | ||
// tail, since old pointers are no longer valid | ||
const newPart = createAndInsertPart(containerPart, newParts[newTail + 1]); | ||
updatePart(newPart, newValues[newHead]); | ||
newParts[newHead++] = newPart; | ||
} | ||
// Remove any remaining unused old parts | ||
while (oldHead <= oldTail) { | ||
const oldPart = oldParts[oldHead++]; | ||
if (oldPart !== null) { | ||
removePart(oldPart); | ||
} | ||
} | ||
// Save order of new parts for next round | ||
partListCache.set(containerPart, newParts); | ||
keyListCache.set(containerPart, newKeys); | ||
}; | ||
}); | ||
//# sourceMappingURL=repeat.js.map | ||
const u=(e,s,t)=>{const l=new Map;for(let n=s;n<=t;n++)l.set(e[n],n);return l},c=e(class extends s{constructor(e){if(super(),!(e instanceof t))throw Error("repeat can only be used in text bindings")}_getValuesAndKeys(e,s,t){let l;void 0===t?t=s:void 0!==s&&(l=s);const n=[],o=[];let r=0;for(const s of e)n[r]=l?l(s,r):r,o[r]=t(s,r),r++;return{values:o,keys:n}}render(e,s,t){return this._getValuesAndKeys(e,s,t).values}update(e,[s,t,c]){var a;let d=n(e);const{values:h,keys:p}=this._getValuesAndKeys(s,t,c);if(!d)return this.itemKeys=p,h;const v=null!==(a=this.itemKeys)&&void 0!==a?a:this.itemKeys=[],m=[];let y,x,b=0,g=d.length-1,j=0,k=h.length-1;for(;b<=g&&j<=k;)if(null===d[b])b++;else if(null===d[g])g--;else if(v[b]===p[j])m[j]=o(d[b],h[j]),b++,j++;else if(v[g]===p[k])m[k]=o(d[g],h[k]),g--,k--;else if(v[b]===p[k])m[k]=o(d[b],h[k]),r(e,d[b],m[k+1]),b++,k--;else if(v[g]===p[j])m[j]=o(d[g],h[j]),r(e,d[g],d[b]),g--,j++;else if(void 0===y&&(y=u(p,j,k),x=u(v,b,g)),y.has(v[b]))if(y.has(v[g])){const s=x.get(p[j]),t=void 0!==s?d[s]:null;if(null===t){const s=f(e,d[b]);o(s,h[j]),m[j]=s}else m[j]=o(t,h[j]),r(e,t,d[b]),d[s]=null;j++}else i(d[g]),g--;else i(d[b]),b++;for(;j<=k;){const s=f(e,m[k+1]);o(s,h[j]),m[j++]=s}for(;b<=g;){const e=d[b++];null!==e&&i(e)}return this.itemKeys=p,e._value=m,l}});export{c as repeat}; | ||
//# sourceMappingURL=repeat.js.map |
@@ -14,6 +14,19 @@ /** | ||
*/ | ||
import { Part } from '../lit-html.js'; | ||
import { AttributePart, Directive, PartInfo } from '../lit-html.js'; | ||
/** | ||
* A key-value set of CSS properties and values. | ||
* | ||
* The key should be either a valid CSS property name string, like | ||
* `'background-color'`, or a valid JavaScript camel case property name | ||
* for CSSStyleDeclaration like `backgroundColor`. | ||
*/ | ||
export interface StyleInfo { | ||
readonly [name: string]: string; | ||
} | ||
declare class StyleMap extends Directive { | ||
previousStyleProperties?: Set<string>; | ||
constructor(part: PartInfo); | ||
render(styleInfo: StyleInfo): string; | ||
update(part: AttributePart, [styleInfo]: Parameters<this['render']>): {}; | ||
} | ||
/** | ||
@@ -36,3 +49,7 @@ * A directive that applies CSS properties to an element. | ||
*/ | ||
export declare const styleMap: (styleInfo: StyleInfo) => (part: Part) => void; | ||
export declare const styleMap: (styleInfo: StyleInfo) => { | ||
_$litDirective$: typeof StyleMap; | ||
values: [styleInfo: StyleInfo]; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=style-map.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import{directive as t,Directive as e,ATTRIBUTE_PART as r,noChange as s}from"../lit-html.js"; | ||
/** | ||
@@ -13,67 +14,3 @@ * @license | ||
* http://polymer.github.io/PATENTS.txt | ||
*/ | ||
import { AttributePart, directive, PropertyPart } from '../lit-html.js'; | ||
/** | ||
* Stores the StyleInfo object applied to a given AttributePart. | ||
* Used to unset existing values when a new StyleInfo object is applied. | ||
*/ | ||
const previousStylePropertyCache = new WeakMap(); | ||
/** | ||
* A directive that applies CSS properties to an element. | ||
* | ||
* `styleMap` can only be used in the `style` attribute and must be the only | ||
* expression in the attribute. It takes the property names in the `styleInfo` | ||
* object and adds the property values as CSS properties. Property names with | ||
* dashes (`-`) are assumed to be valid CSS property names and set on the | ||
* element's style object using `setProperty()`. Names without dashes are | ||
* assumed to be camelCased JavaScript property names and set on the element's | ||
* style object using property assignment, allowing the style object to | ||
* translate JavaScript-style names to CSS property names. | ||
* | ||
* For example `styleMap({backgroundColor: 'red', 'border-top': '5px', '--size': | ||
* '0'})` sets the `background-color`, `border-top` and `--size` properties. | ||
* | ||
* @param styleInfo {StyleInfo} | ||
*/ | ||
export const styleMap = directive((styleInfo) => (part) => { | ||
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) || | ||
part.committer.name !== 'style' || part.committer.parts.length > 1) { | ||
throw new Error('The `styleMap` directive must be used in the style attribute ' + | ||
'and must be the only part in the attribute.'); | ||
} | ||
const { committer } = part; | ||
const { style } = committer.element; | ||
let previousStyleProperties = previousStylePropertyCache.get(part); | ||
if (previousStyleProperties === undefined) { | ||
// Write static styles once | ||
style.cssText = committer.strings.join(' '); | ||
previousStylePropertyCache.set(part, previousStyleProperties = new Set()); | ||
} | ||
// Remove old properties that no longer exist in styleInfo | ||
// We use forEach() instead of for-of so that re don't require down-level | ||
// iteration. | ||
previousStyleProperties.forEach((name) => { | ||
if (!(name in styleInfo)) { | ||
previousStyleProperties.delete(name); | ||
if (name.indexOf('-') === -1) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
style[name] = null; | ||
} | ||
else { | ||
style.removeProperty(name); | ||
} | ||
} | ||
}); | ||
// Add or update properties | ||
for (const name in styleInfo) { | ||
previousStyleProperties.add(name); | ||
if (name.indexOf('-') === -1) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
style[name] = styleInfo[name]; | ||
} | ||
else { | ||
style.setProperty(name, styleInfo[name]); | ||
} | ||
} | ||
}); | ||
//# sourceMappingURL=style-map.js.map | ||
*/const i=t(class extends e{constructor(t){if(super(),t.type!==r||"style"!==t.name||void 0!==t.strings&&t.strings.length>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(t){return Object.keys(t).reduce((e,r)=>{const s=t[r];return null===s?e:e+`${r=r.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${s};`},"")}update(t,[e]){const{style:r}=t.element;if(void 0===this.previousStyleProperties){this.previousStyleProperties=new Set;for(const t in e)this.previousStyleProperties.add(t);return this.render(e)}this.previousStyleProperties.forEach(t=>{t in e||(this.previousStyleProperties.delete(t),-1===t.indexOf("-")?r[t]=null:r.removeProperty(t))});for(const t in e)this.previousStyleProperties.add(t),-1===t.indexOf("-")?r[t]=e[t]:r.setProperty(t,e[t]);return s}});export{i as styleMap}; | ||
//# sourceMappingURL=style-map.js.map |
@@ -14,3 +14,8 @@ /** | ||
*/ | ||
import { Part } from '../lit-html.js'; | ||
import { Directive, PartInfo } from '../lit-html.js'; | ||
declare class TemplateContent extends Directive { | ||
private __previousTemplate?; | ||
constructor(part: PartInfo); | ||
render(template: HTMLTemplateElement): {}; | ||
} | ||
/** | ||
@@ -23,3 +28,7 @@ * Renders the content of a template element as HTML. | ||
*/ | ||
export declare const templateContent: (template: HTMLTemplateElement) => (part: Part) => void; | ||
export declare const templateContent: (template: HTMLTemplateElement) => { | ||
_$litDirective$: typeof TemplateContent; | ||
values: [template: HTMLTemplateElement]; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=template-content.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import{directive as t,Directive as e,NODE_PART as r,noChange as n}from"../lit-html.js"; | ||
/** | ||
@@ -13,30 +14,3 @@ * @license | ||
* http://polymer.github.io/PATENTS.txt | ||
*/ | ||
import { directive, NodePart } from '../lit-html.js'; | ||
// For each part, remember the value that was last rendered to the part by the | ||
// templateContent directive, and the DocumentFragment that was last set as a | ||
// value. The DocumentFragment is used as a unique key to check if the last | ||
// value rendered to the part was with templateContent. If not, we'll always | ||
// re-render the value passed to templateContent. | ||
const previousValues = new WeakMap(); | ||
/** | ||
* Renders the content of a template element as HTML. | ||
* | ||
* Note, the template should be developer controlled and not user controlled. | ||
* Rendering a user-controlled template with this directive | ||
* could lead to cross-site-scripting vulnerabilities. | ||
*/ | ||
export const templateContent = directive((template) => (part) => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('templateContent can only be used in text bindings'); | ||
} | ||
const previousValue = previousValues.get(part); | ||
if (previousValue !== undefined && template === previousValue.template && | ||
part.value === previousValue.fragment) { | ||
return; | ||
} | ||
const fragment = document.importNode(template.content, true); | ||
part.setValue(fragment); | ||
previousValues.set(part, { template, fragment }); | ||
}); | ||
//# sourceMappingURL=template-content.js.map | ||
*/const o=t(class extends e{constructor(t){if(super(),t.type!==r)throw Error("templateContent can only be used in text bindings")}render(t){return this.t===t?n:(this.t=t,document.importNode(t.content,!0))}});export{o as templateContent}; | ||
//# sourceMappingURL=template-content.js.map |
@@ -14,3 +14,11 @@ /** | ||
*/ | ||
import { Part } from '../lit-html.js'; | ||
import { Directive, TemplateResult, PartInfo } from '../lit-html.js'; | ||
export declare class UnsafeHTML extends Directive { | ||
static directiveName: string; | ||
static resultType: number; | ||
value: unknown; | ||
templateResult?: TemplateResult; | ||
constructor(part: PartInfo); | ||
render(value: string): string | TemplateResult | undefined; | ||
} | ||
/** | ||
@@ -23,3 +31,6 @@ * Renders the result as HTML, rather than text. | ||
*/ | ||
export declare const unsafeHTML: (value: unknown) => (part: Part) => void; | ||
export declare const unsafeHTML: (value: string) => { | ||
_$litDirective$: typeof UnsafeHTML; | ||
values: [value: string]; | ||
}; | ||
//# sourceMappingURL=unsafe-html.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import{Directive as t,nothing as r,NODE_PART as i,noChange as s,directive as n}from"../lit-html.js"; | ||
/** | ||
@@ -13,33 +14,3 @@ * @license | ||
* http://polymer.github.io/PATENTS.txt | ||
*/ | ||
import { isPrimitive } from '../lib/parts.js'; | ||
import { directive, NodePart } from '../lit-html.js'; | ||
// For each part, remember the value that was last rendered to the part by the | ||
// unsafeHTML directive, and the DocumentFragment that was last set as a value. | ||
// The DocumentFragment is used as a unique key to check if the last value | ||
// rendered to the part was with unsafeHTML. If not, we'll always re-render the | ||
// value passed to unsafeHTML. | ||
const previousValues = new WeakMap(); | ||
/** | ||
* Renders the result as HTML, rather than text. | ||
* | ||
* Note, this is unsafe to use with any user-provided input that hasn't been | ||
* sanitized or escaped, as it may lead to cross-site-scripting | ||
* vulnerabilities. | ||
*/ | ||
export const unsafeHTML = directive((value) => (part) => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('unsafeHTML can only be used in text bindings'); | ||
} | ||
const previousValue = previousValues.get(part); | ||
if (previousValue !== undefined && isPrimitive(value) && | ||
value === previousValue.value && part.value === previousValue.fragment) { | ||
return; | ||
} | ||
const template = document.createElement('template'); | ||
template.innerHTML = value; // innerHTML casts to string internally | ||
const fragment = document.importNode(template.content, true); | ||
part.setValue(fragment); | ||
previousValues.set(part, { value, fragment }); | ||
}); | ||
//# sourceMappingURL=unsafe-html.js.map | ||
*/class e extends t{constructor(t){if(super(),this.value=r,t.type!==i)throw Error(this.constructor.directiveName+"() can only be used in text bindings")}render(t){if(t===r)return this.templateResult=void 0,this.value=t;if(t===s)return t;if("string"!=typeof t)throw Error(this.constructor.directiveName+"() called with a non-string value");if(t===this.value)return this.templateResult;this.value=t;const i=[t];return i.raw=i,this.templateResult={_$litType$:this.constructor.resultType,strings:i,values:[]}}}e.directiveName="unsafeHTML",e.resultType=1;const o=n(e);export{e as UnsafeHTML,o as unsafeHTML}; | ||
//# sourceMappingURL=unsafe-html.js.map |
@@ -14,3 +14,7 @@ /** | ||
*/ | ||
import { Part } from '../lit-html.js'; | ||
import { UnsafeHTML } from './unsafe-html.js'; | ||
declare class UnsafeSVG extends UnsafeHTML { | ||
static directiveName: string; | ||
static resultType: number; | ||
} | ||
/** | ||
@@ -23,3 +27,7 @@ * Renders the result as SVG, rather than text. | ||
*/ | ||
export declare const unsafeSVG: (value: unknown) => (part: Part) => void; | ||
export declare const unsafeSVG: (value: string) => { | ||
_$litDirective$: typeof UnsafeSVG; | ||
values: [value: string]; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=unsafe-svg.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import{directive as s}from"../lit-html.js";import{UnsafeHTML as t}from"./unsafe-html.js"; | ||
/** | ||
@@ -13,50 +14,3 @@ * @license | ||
* http://polymer.github.io/PATENTS.txt | ||
*/ | ||
import { reparentNodes } from '../lib/dom.js'; | ||
import { isPrimitive } from '../lib/parts.js'; | ||
import { directive, NodePart } from '../lit-html.js'; | ||
// For each part, remember the value that was last rendered to the part by the | ||
// unsafeSVG directive, and the DocumentFragment that was last set as a value. | ||
// The DocumentFragment is used as a unique key to check if the last value | ||
// rendered to the part was with unsafeSVG. If not, we'll always re-render the | ||
// value passed to unsafeSVG. | ||
const previousValues = new WeakMap(); | ||
const isIe = window.navigator.userAgent.indexOf('Trident/') > 0; | ||
/** | ||
* Renders the result as SVG, rather than text. | ||
* | ||
* Note, this is unsafe to use with any user-provided input that hasn't been | ||
* sanitized or escaped, as it may lead to cross-site-scripting | ||
* vulnerabilities. | ||
*/ | ||
export const unsafeSVG = directive((value) => (part) => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('unsafeSVG can only be used in text bindings'); | ||
} | ||
const previousValue = previousValues.get(part); | ||
if (previousValue !== undefined && isPrimitive(value) && | ||
value === previousValue.value && part.value === previousValue.fragment) { | ||
return; | ||
} | ||
const template = document.createElement('template'); | ||
const content = template.content; | ||
let svgElement; | ||
if (isIe) { | ||
// IE can't set innerHTML of an svg element. However, it also doesn't | ||
// support Trusted Types, so it's ok for us to use a string when setting | ||
// innerHTML. | ||
template.innerHTML = `<svg>${value}</svg>`; | ||
svgElement = content.firstChild; | ||
} | ||
else { | ||
svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | ||
content.appendChild(svgElement); | ||
svgElement.innerHTML = value; | ||
} | ||
content.removeChild(svgElement); | ||
reparentNodes(content, svgElement.firstChild); | ||
const fragment = document.importNode(content, true); | ||
part.setValue(fragment); | ||
previousValues.set(part, { value, fragment }); | ||
}); | ||
//# sourceMappingURL=unsafe-svg.js.map | ||
*/class m extends t{}m.directiveName="unsafeSVG",m.resultType=2;const o=s(m);export{o as unsafeSVG}; | ||
//# sourceMappingURL=unsafe-svg.js.map |
@@ -14,21 +14,21 @@ /** | ||
*/ | ||
import { SVGTemplateResult, TemplateResult } from './lib/template-result.js'; | ||
export { DefaultTemplateProcessor, defaultTemplateProcessor } from './lib/default-template-processor.js'; | ||
export { directive, DirectiveFn, isDirective } from './lib/directive.js'; | ||
export { removeNodes, reparentNodes } from './lib/dom.js'; | ||
export { noChange, nothing, Part } from './lib/part.js'; | ||
export { AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isIterable, isPrimitive, NodePart, PropertyCommitter, PropertyPart } from './lib/parts.js'; | ||
export { RenderOptions } from './lib/render-options.js'; | ||
export { parts, render } from './lib/render.js'; | ||
export { templateCaches, templateFactory } from './lib/template-factory.js'; | ||
export { TemplateInstance } from './lib/template-instance.js'; | ||
export { TemplateProcessor } from './lib/template-processor.js'; | ||
export { SVGTemplateResult, TemplateResult } from './lib/template-result.js'; | ||
export { createMarker, isTemplatePartActive, Template } from './lib/template.js'; | ||
declare global { | ||
interface Window { | ||
litHtmlVersions: string[]; | ||
} | ||
} | ||
/** TemplateResult types */ | ||
declare const HTML_RESULT = 1; | ||
declare const SVG_RESULT = 2; | ||
/** TemplatePart types */ | ||
export declare const ATTRIBUTE_PART = 1; | ||
export declare const NODE_PART = 2; | ||
export declare const PROPERTY_PART = 3; | ||
export declare const BOOLEAN_ATTRIBUTE_PART = 4; | ||
export declare const EVENT_PART = 5; | ||
declare type ResultType = typeof HTML_RESULT | typeof SVG_RESULT; | ||
/** | ||
* The return type of the template tag functions. | ||
*/ | ||
export declare type TemplateResult = { | ||
_$litType$: ResultType; | ||
strings: TemplateStringsArray; | ||
values: unknown[]; | ||
}; | ||
/** | ||
* Interprets a template literal as an HTML template that can efficiently | ||
@@ -42,3 +42,175 @@ * render to and update a container. | ||
*/ | ||
export declare const svg: (strings: TemplateStringsArray, ...values: unknown[]) => SVGTemplateResult; | ||
export declare const svg: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult; | ||
/** | ||
* A sentinel value that signals that a value was handled by a directive and | ||
* should not be written to the DOM. | ||
*/ | ||
export declare const noChange: {}; | ||
/** | ||
* A sentinel value that signals a NodePart to fully clear its content. | ||
*/ | ||
export declare const nothing: {}; | ||
export declare type NodePartInfo = { | ||
readonly type: typeof NODE_PART; | ||
}; | ||
export declare type AttributePartInfo = { | ||
readonly type: typeof ATTRIBUTE_PART | typeof PROPERTY_PART | typeof BOOLEAN_ATTRIBUTE_PART | typeof EVENT_PART; | ||
strings?: ReadonlyArray<string>; | ||
name: string; | ||
tagName: string; | ||
}; | ||
/** | ||
* Information about the part a directive is bound to. | ||
* | ||
* This is useful for checking that a directive is attached to a valid part, | ||
* such as with directive that can only be used on attribute bindings. | ||
*/ | ||
export declare type PartInfo = NodePartInfo | AttributePartInfo; | ||
export declare type DirectiveClass = { | ||
new (part: PartInfo): Directive; | ||
}; | ||
/** | ||
* This utility type extracts the signature of a directive class's render() | ||
* method so we can use it for the type of the generated directive function. | ||
*/ | ||
export declare type DirectiveParameters<C extends DirectiveClass> = Parameters<InstanceType<C>['render']>; | ||
/** | ||
* A generated directive function doesn't evaluate the directive, but just | ||
* returns a DirectiveResult object that captures the arguments. | ||
*/ | ||
declare type DirectiveResult<C extends DirectiveClass = DirectiveClass> = { | ||
_$litDirective$: C; | ||
values: DirectiveParameters<C>; | ||
}; | ||
/** | ||
* Creates a user-facing directive function from a Directive class. This | ||
* function has the same parameters as the directive's render() method. | ||
* | ||
* WARNING: The directive and part API changes are in progress and subject to | ||
* change in future pre-releases. | ||
*/ | ||
export declare const directive: <C extends DirectiveClass>(c: C) => (...values: Parameters<InstanceType<C>["render"]>) => DirectiveResult<C>; | ||
export interface RenderOptions { | ||
/** | ||
* An object to use as the `this` value for event listeners. It's often | ||
* useful to set this to the host component rendering a template. | ||
*/ | ||
readonly eventContext?: EventTarget; | ||
/** | ||
* A DOM node before which to render content in the container. | ||
*/ | ||
readonly renderBefore?: ChildNode | null; | ||
} | ||
/** | ||
* Renders a value, usually a lit-html TemplateResult, to the container. | ||
* @param value | ||
* @param container | ||
* @param options | ||
*/ | ||
export declare const render: (value: unknown, container: HTMLElement | DocumentFragment, options?: RenderOptions | undefined) => void; | ||
/** | ||
* Base class for creating custom directives. Users should extend this class, | ||
* implement `render` and/or `update`, and then pass their subclass to | ||
* `directive`. | ||
* | ||
* WARNING: The directive and part API changes are in progress and subject to | ||
* change in future pre-releases. | ||
*/ | ||
export declare abstract class Directive { | ||
abstract render(...props: Array<unknown>): unknown; | ||
update(_part: Part, props: Array<unknown>): unknown; | ||
} | ||
export declare type Part = NodePart | AttributePart | PropertyPart | BooleanAttributePart; | ||
export declare class NodePart { | ||
_startNode: ChildNode; | ||
_endNode: ChildNode | null; | ||
options: RenderOptions | undefined; | ||
readonly type = 2; | ||
_value: unknown; | ||
protected __directive?: Directive; | ||
constructor(_startNode: ChildNode, _endNode: ChildNode | null, options: RenderOptions | undefined); | ||
_setValue(value: unknown): void; | ||
private __insert; | ||
private __commitDirective; | ||
private _commitNode; | ||
private __commitText; | ||
private __commitTemplateResult; | ||
private __commitIterable; | ||
__clear(start?: ChildNode | null): void; | ||
} | ||
export declare class AttributePart { | ||
readonly type: 1 | 5 | 4 | 3; | ||
readonly element: HTMLElement; | ||
readonly name: string; | ||
/** | ||
* If this attribute part represents an interpolation, this contains the | ||
* static strings of the interpolation. For single-value, complete bindings, | ||
* this is undefined. | ||
*/ | ||
readonly strings?: ReadonlyArray<string>; | ||
_value: unknown | Array<unknown>; | ||
private __directives?; | ||
get tagName(): string; | ||
constructor(element: HTMLElement, name: string, strings: ReadonlyArray<string>, _options?: RenderOptions); | ||
/** | ||
* Normalizes a user-provided value before writing it to the DOM. In the | ||
* near future this will include invoking a directive if the value is | ||
* a DirectiveResult. | ||
* | ||
* @param value the raw input value to normalize | ||
* @param _i the index in the values array this value was read from | ||
*/ | ||
__resolveValue(value: unknown, i: number): unknown; | ||
/** | ||
* Sets the value of this part. | ||
* | ||
* If this part is single-valued, `this.__strings` will be undefined, and the | ||
* method will be called with a single value argument. If this part is | ||
* multi-value, `this.__strings` will be defined, and the method is called | ||
* with the value array of the part's owning TemplateInstance, and an offset | ||
* into the value array from which the values should be read. | ||
* | ||
* This method is overloaded this way to eliminate short-lived array slices | ||
* of the template instance values, and allow a fast-path for single-valued | ||
* parts. | ||
* | ||
* @param value The part value, or an array of values for multi-valued parts | ||
* @param from the index to start reading values from. `undefined` for | ||
* single-valued parts | ||
*/ | ||
_setValue(value: unknown): void; | ||
_setValue(value: Array<unknown>, from: number): void; | ||
/** | ||
* Writes the value to the DOM. An override point for PropertyPart and | ||
* BooleanAttributePart. | ||
*/ | ||
__commitValue(value: unknown): void; | ||
} | ||
export declare class PropertyPart extends AttributePart { | ||
readonly type = 3; | ||
__commitValue(value: unknown): void; | ||
} | ||
export declare class BooleanAttributePart extends AttributePart { | ||
readonly type = 4; | ||
__commitValue(value: unknown): void; | ||
} | ||
/** | ||
* An AttributePart that manages an event listener via add/removeEventListener. | ||
* | ||
* This part works by adding itself as the event listener on an element, then | ||
* delegating to the value passed to it. This reduces the number of calls to | ||
* add/removeEventListener if the listener changes frequently, such as when an | ||
* inline function is used as a listener. | ||
* | ||
* Because event options are passed when adding listeners, we must take case | ||
* to add and remove the part as a listener when the event options change. | ||
*/ | ||
export declare class EventPart extends AttributePart { | ||
readonly type = 5; | ||
__eventContext?: unknown; | ||
constructor(...args: ConstructorParameters<typeof AttributePart>); | ||
_setValue(newListener: unknown): void; | ||
handleEvent(event: Event): void; | ||
} | ||
export {}; | ||
//# sourceMappingURL=lit-html.d.ts.map |
@@ -14,47 +14,3 @@ /** | ||
*/ | ||
/** | ||
* | ||
* Main lit-html module. | ||
* | ||
* Main exports: | ||
* | ||
* - [[html]] | ||
* - [[svg]] | ||
* - [[render]] | ||
* | ||
* @packageDocumentation | ||
*/ | ||
/** | ||
* Do not remove this comment; it keeps typedoc from misplacing the module | ||
* docs. | ||
*/ | ||
import { defaultTemplateProcessor } from './lib/default-template-processor.js'; | ||
import { SVGTemplateResult, TemplateResult } from './lib/template-result.js'; | ||
export { DefaultTemplateProcessor, defaultTemplateProcessor } from './lib/default-template-processor.js'; | ||
export { directive, isDirective } from './lib/directive.js'; | ||
// TODO(justinfagnani): remove line when we get NodePart moving methods | ||
export { removeNodes, reparentNodes } from './lib/dom.js'; | ||
export { noChange, nothing } from './lib/part.js'; | ||
export { AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isIterable, isPrimitive, NodePart, PropertyCommitter, PropertyPart } from './lib/parts.js'; | ||
export { parts, render } from './lib/render.js'; | ||
export { templateCaches, templateFactory } from './lib/template-factory.js'; | ||
export { TemplateInstance } from './lib/template-instance.js'; | ||
export { SVGTemplateResult, TemplateResult } from './lib/template-result.js'; | ||
export { createMarker, isTemplatePartActive, Template } from './lib/template.js'; | ||
// IMPORTANT: do not change the property name or the assignment expression. | ||
// This line will be used in regexes to search for lit-html usage. | ||
// TODO(justinfagnani): inject version number at build time | ||
if (typeof window !== 'undefined') { | ||
(window['litHtmlVersions'] || (window['litHtmlVersions'] = [])).push('1.3.0'); | ||
} | ||
/** | ||
* Interprets a template literal as an HTML template that can efficiently | ||
* render to and update a container. | ||
*/ | ||
export const html = (strings, ...values) => new TemplateResult(strings, values, 'html', defaultTemplateProcessor); | ||
/** | ||
* Interprets a template literal as an SVG template that can efficiently | ||
* render to and update a container. | ||
*/ | ||
export const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg', defaultTemplateProcessor); | ||
//# sourceMappingURL=lit-html.js.map | ||
var t,s;const i=`lit$${(Math.random()+"").slice(9)}$`,e="?"+i,h=`<${e}>`,o=document,l=(t="")=>o.createComment(t),n=t=>null===t||"object"!=typeof t&&"function"!=typeof t,r=Array.isArray,c=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,_=/-->/g,u=/>/g,a=/>|[ \n\r]([^ -- "'>=/]+)([ \n\r]*=[ \n\r]*(?:[^ \n\r"'`<>=]|("|')|))/g,v=/'/g,d=/"/g,f=/^(?:script|style|textarea)$/i,m=1,p=2,g=3,y=4,$=5,x=t=>(s,...i)=>({_$litType$:t,strings:s,values:i}),w=x(1),V=x(2),b={},T={},A=new Map,z=t=>(...s)=>({_$litDirective$:t,values:s}),D=(t,s,i)=>{var e,h;const o=null!==(e=null==i?void 0:i.renderBefore)&&void 0!==e?e:s;let n=o.$lit$;if(void 0===n){const t=null!==(h=null==i?void 0:i.renderBefore)&&void 0!==h?h:null;o.$lit$=n=new Z(s.insertBefore(l(),t),t,i)}n._setValue(t)},E=o.createTreeWalker(o);class M{update(t,s){return this.render(...s)}}class N{constructor({strings:t,_$litType$:s}){this.t=[],E.currentNode=(this.i=o.createElement("template")).content;const n=(this.h=t).length-1,r=[];let m,p,g=2===s?"<svg>":"",y=0,$=0,x=0,w=c;for(let s=0;s<n;s++){const e=t[s];let o,l,n=-1,m=0;for(;m<e.length;){if(w.lastIndex=m,l=w.exec(e),null===l){w===a&&(n=-1);break}m=w.lastIndex,w===c?"!--"===l[1]?w=_:void 0!==l[1]?w=u:void 0!==l[2]?(f.test(l[2])&&(p=RegExp("</"+l[2],"g")),w=a):void 0!==l[3]&&(w=a):w===a?">"===l[0]?(w=null!=p?p:c,n=-1):(n=w.lastIndex-l[2].length,o=l[1],w=void 0===l[3]?a:'"'===l[3]?d:v):w===d||w===v?w=a:w===_||w===u?w=c:(w=a,p=void 0)}g+=w===c?e+h:(-1!==n?(r.push(o),e.slice(0,n)+"$lit$"+e.slice(n)):e)+i}if(this.i.innerHTML=g+this.h[n],2===s){const t=this.i.content,s=t.firstChild;s.remove(),t.append(...s.childNodes)}for(;null!==(m=E.nextNode())&&$<n;){if(1===m.nodeType){if(m.hasAttributes()){const{attributes:t}=m;for(let s=0;s<t.length;s++){const{name:e,value:h}=t[s];if(e.endsWith("$lit$")){s--,m.removeAttribute(e);const t=h.split(i),o=/([.?@])?(.*)/.exec(r[x++]);this.t.push({o:1,l:y,_:o[2],h:t,u:"."===o[1]?k:"?"===o[1]?I:"@"===o[1]?S:j}),$+=t.length-1}else e===i&&(m.removeAttribute(e),s--,this.t.push({o:6,l:y}))}}if(f.test(m.tagName)){const t=m.textContent.split(i),s=t.length-1;if(s>0){m.textContent="";for(let i=0;i<s;i++)m.append(t[i]||l()),this.t.push({o:2,l:++y}),$++;m.append(t[s]||l())}}}else if(8===m.nodeType)if(m.data===e)$++,this.t.push({o:2,l:y});else{let t=-1;for(;-1!==(t=m.data.indexOf(i,t+1));)this.t.push({o:7,l:y}),$++,t+=i.length-1}y++}}}class R{constructor(t){this.t=[],this.v=t}m(t){const{i:{content:s},t:i}=this.v,e=o.importNode(s,!0);E.currentNode=e;let h=E.nextNode(),l=0,n=0,r=i[0];for(;void 0!==r&&null!==h;){if(l===r.l){let s;2===r.o?s=new Z(h,h.nextSibling,t):1===r.o&&(s=new r.u(h,r._,r.h,t)),this.t.push(s),r=i[++n]}void 0!==r&&l!==r.l&&(h=E.nextNode(),l++)}return e}p(t){let s=0;for(const i of this.t)void 0!==i?void 0!==i.strings?(i._setValue(t,s),s+=i.strings.length-1):i._setValue(t[s++]):s++}}class Z{constructor(t,s,i){this._startNode=t,this._endNode=s,this.options=i,this.type=2}_setValue(t){n(t)?t!==this._value&&this.g(t):void 0!==t._$litType$?this.$(t):void 0!==t._$litDirective$?this.V(t):void 0!==t.nodeType?this._commitNode(t):(t=>r(t)||t&&"function"==typeof t[Symbol.iterator])(t)?this.T(t):t===T?(this._value=T,this.A()):t!==b&&this.g(t)}D(t,s=this._endNode){return this._startNode.parentNode.insertBefore(t,s)}V(t){var s;const i=t._$litDirective$;(null===(s=this.M)||void 0===s?void 0:s.constructor)!==i&&(this.A(),this.M=new i(this)),this._setValue(this.M.update(this,t.values))}_commitNode(t){this._value!==t&&(this.A(),this._value=this.D(t))}g(t){const s=this._startNode.nextSibling;null!=t||(t=""),null!==s&&3===s.nodeType&&(null===this._endNode?null===s.nextSibling:s===this._endNode.previousSibling)?s.data=t:this._commitNode(new Text(t)),this._value=t}$(t){const{strings:s,values:i}=t;let e=A.get(s);if(void 0===e&&A.set(s,e=new N(t)),null!=this._value&&this._value.v===e)this._value.p(i);else{const t=new R(e),s=t.m(this.options);t.p(i),this._commitNode(s),this._value=t}}T(t){r(this._value)||(this._value=[],this.A());const s=this._value;let i,e=0;for(const h of t)e===s.length?s.push(i=new Z(this.D(l()),this.D(l()),this.options)):i=s[e],i._setValue(h),e++;e<s.length&&(s.length=e,this.A(null==i?void 0:i._endNode.nextSibling))}A(t=this._startNode.nextSibling){for(;t&&t!==this._endNode;){const s=t.nextSibling;t.remove(),t=s}}}class j{constructor(t,s,i,e){this.type=1,this._value=T,this.element=t,this.name=s,i.length>2||""!==i[0]||""!==i[1]?(this._value=Array(i.length-1).fill(T),this.strings=i):this._value=T}get tagName(){return this.element.tagName}N(t,s){var i,e;const h=null===(i=t)||void 0===i?void 0:i._$litDirective$;if(void 0!==h){let i=(null!==(e=this.R)&&void 0!==e?e:this.R=[])[s];(null==i?void 0:i.constructor)!==h&&(i=this.R[s]=new h(this)),t=i.update(this,t.values)}return null!=t?t:""}_setValue(t,s){const i=this.strings;if(void 0===i){const s=this.N(t,0);(n(s)||s===T)&&s===this._value||s===b||this.Z(this._value=s)}else{let e,h,o=i[0],l=!1,r=!1;for(e=0;e<i.length-1;e++)h=this.N(t[s+e],e),h===b?h=this._value[e]:(r=r||h===T,l=l||!((n(h)||h===T)&&h===this._value[e]),this._value[e]=h),o+=("string"==typeof h?h:h+"")+i[e+1];l&&this.Z(r?T:o)}}Z(t){t===T?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t)}}class k extends j{constructor(){super(...arguments),this.type=3}Z(t){this.element[this.name]=t===T?void 0:t}}class I extends j{constructor(){super(...arguments),this.type=4}Z(t){t&&t!==T?this.element.setAttribute(this.name,""):this.element.removeAttribute(this.name)}}class S extends j{constructor(...t){var s;super(...t),this.type=5,this.j=null===(s=t[3])||void 0===s?void 0:s.eventContext}_setValue(t){null!=t||(t=T);const s=this._value,i=t===T&&s!==T||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,e=t!==T&&(s===T||i);i&&this.element.removeEventListener(this.name,this,s),e&&this.element.addEventListener(this.name,this,t),this._value=t}handleEvent(t){var s;"function"==typeof this._value?this._value.call(null!==(s=this.j)&&void 0!==s?s:this.element,t):this._value.handleEvent(t)}}(null!==(t=(s=globalThis).litHtmlVersions)&&void 0!==t?t:s.litHtmlVersions=[]).push("1.3.0");export{m as ATTRIBUTE_PART,j as AttributePart,y as BOOLEAN_ATTRIBUTE_PART,I as BooleanAttributePart,M as Directive,$ as EVENT_PART,S as EventPart,p as NODE_PART,Z as NodePart,g as PROPERTY_PART,k as PropertyPart,z as directive,w as html,b as noChange,T as nothing,D as render,V as svg}; | ||
//# sourceMappingURL=lit-html.js.map |
{ | ||
"name": "lit-html", | ||
"version": "1.3.0", | ||
"description": "HTML template literals in JavaScript", | ||
"version": "2.0.0-pre.1", | ||
"description": "HTML templates literals in JavaScript", | ||
"license": "BSD-3-Clause", | ||
@@ -10,11 +10,3 @@ "homepage": "https://lit-html.polymer-project.org/", | ||
"main": "lit-html.js", | ||
"module": "lit-html.js", | ||
"typings": "lit-html.d.ts", | ||
"typesVersions": { | ||
"<3.8": { | ||
"*": [ | ||
"ts3.4/*" | ||
] | ||
} | ||
}, | ||
"directories": { | ||
@@ -28,20 +20,21 @@ "test": "test" | ||
"/lit-html.d.ts.map", | ||
"/lib/", | ||
"/directives/", | ||
"/polyfills", | ||
"/src/", | ||
"/ts3.4/", | ||
"!/src/test/" | ||
"!/src/test/", | ||
"/development/", | ||
"!/development/test/" | ||
], | ||
"scripts": { | ||
"build": "tsc && rm -rf ./ts3.4 && downlevel-dts . ts3.4 && cp tsconfig.json ./ts3.4/", | ||
"checksize": "rollup -c ; cat lit-html.bundled.js | gzip -9 | wc -c ; rm lit-html.bundled.js", | ||
"test": "npm run build && npm run lint && wct --npm", | ||
"quicktest": "wct -l chrome -p --npm", | ||
"format": "clang-format --version; find src test | grep '\\.js$\\|\\.ts$' | xargs clang-format --style=file -i", | ||
"lint": "npm run lint:eslint", | ||
"lint:eslint": "eslint 'src/**/*.{js,ts}'", | ||
"prepublishOnly": "node check-version-tracker.cjs && npm run lint && npm test", | ||
"prepare": "npm run build", | ||
"publish-dev": "npm test && VERSION=${npm_package_version%-*}-dev.`git rev-parse --short HEAD` && npm version --no-git-tag-version $VERSION && npm publish --tag dev" | ||
"build": "npm run clean && tsc && rollup -c", | ||
"clean": "rm -rf lit-html.{js,js.map,d.ts} directives/ development/", | ||
"build:ts": "tsc", | ||
"build:ts:watch": "tsc --watch", | ||
"dev": "scripts/dev.sh", | ||
"test": "npm run test:dev && npm run test:prod", | ||
"test:dev": "cd ../tests && npx wtr '../lit-html/development/**/*_test.js'", | ||
"test:prod": "TEST_PROD_BUILD=true npm run test:dev", | ||
"test:watch": "npm run test:dev -- --watch", | ||
"format": "prettier src/* --write", | ||
"checksize": "rollup -c --environment=CHECKSIZE", | ||
"check-version": "node check-version-tracker.js" | ||
}, | ||
@@ -51,37 +44,17 @@ "author": "The Polymer Authors", | ||
"devDependencies": { | ||
"@types/chai": "^4.1.0", | ||
"@types/mocha": "^7.0.1", | ||
"@types/trusted-types": "^1.0.1", | ||
"@typescript-eslint/eslint-plugin": "^2.26.0", | ||
"@typescript-eslint/parser": "^2.26.0", | ||
"@webcomponents/shadycss": "^1.8.0", | ||
"@webcomponents/webcomponentsjs": "^2.4.2", | ||
"chai": "^4.1.2", | ||
"chromedriver": "^84.0.1", | ||
"clang-format": "~1.2.4", | ||
"downlevel-dts": "^0.4.0", | ||
"eslint": "^6.8.0", | ||
"husky": "^4.2.0", | ||
"lint-staged": "^10.1.0", | ||
"lit-html-benchmarks": "^0.2.1", | ||
"mocha": "^7.0.1", | ||
"rollup": "^1.19.0", | ||
"rollup-plugin-filesize": "^6.2.0", | ||
"rollup-plugin-terser": "^5.2.0", | ||
"tachometer": "^0.5.0", | ||
"typescript": "~3.8.0", | ||
"uglify-es": "^3.3.5", | ||
"wct-mocha": "^1.1.0", | ||
"web-component-tester": "^6.9.0" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "lint-staged" | ||
} | ||
}, | ||
"lint-staged": { | ||
"src/**/*.{js,ts}": [ | ||
"eslint --fix" | ||
] | ||
"@esm-bundle/chai": "^4.1.5", | ||
"@rollup/plugin-replace": "^2.3.3", | ||
"@types/mocha": "^8.0.3", | ||
"chokidar-cli": "^2.1.0", | ||
"concurrently": "^5.3.0", | ||
"mocha": "^8.1.3", | ||
"prettier": "^2.1.1", | ||
"rollup": "^2.26.9", | ||
"rollup-plugin-copy": "^3.3.0", | ||
"rollup-plugin-filesize": "^9.0.2", | ||
"rollup-plugin-sourcemaps": "^0.6.2", | ||
"rollup-plugin-terser": "^7.0.1", | ||
"terser": "^5.2.1", | ||
"typescript": "^4.0.2" | ||
} | ||
} |
137
README.md
@@ -1,15 +0,138 @@ | ||
# lit-html | ||
# lit-html 2.0 Pre-release | ||
Efficient, Expressive, Extensible HTML templates in JavaScript | ||
[![Build Status](https://travis-ci.org/Polymer/lit-html.svg?branch=master)](https://travis-ci.org/Polymer/lit-html) | ||
[![Published on npm](https://img.shields.io/npm/v/lit-html.svg)](https://www.npmjs.com/package/lit-html) | ||
[![Build Status](https://github.com/polymer/lit-html/workflows/Tests/badge.svg?branch=lit-next | ||
)](https://github.com/Polymer/lit-html/actions?query=workflow%3ATests) | ||
[![Published on npm](https://img.shields.io/npm/v/lit-html/next-major)](https://www.npmjs.com/package/lit-html) | ||
[![Join our Slack](https://img.shields.io/badge/slack-join%20chat-4a154b.svg)](https://www.polymer-project.org/slack-invite) | ||
[![Mentioned in Awesome lit-html](https://awesome.re/mentioned-badge.svg)](https://github.com/web-padawan/awesome-lit-html) | ||
## Documentation | ||
## 🚨 About this pre-release | ||
Full documentation is available at [lit-html.polymer-project.org](https://lit-html.polymer-project.org). | ||
This is a major version pre-release of lit-html 2.0. See issue | ||
[#1182](https://github.com/Polymer/lit-html/issues/1182) for the full list of changes | ||
planned/considered for this release. | ||
Docs source is in the `docs` folder. To build the site yourself, see the instructions in [docs/README.md](docs/README.md). | ||
This pre-release is not yet feature complete or API stable. Please note the | ||
breaking changes, known issues, and limitations below, and use at your risk | ||
until the stable release is available. Issues are welcome | ||
for unexpected changes not noted below or in the changelog. | ||
## 🚨 Breaking changes | ||
While `lit-html` 2.0 is intended to be a backward-compatible change for the | ||
majority of 1.x users, please be aware of the following notable breaking | ||
changes: | ||
* New `directive` and `Part` API (see below for migration info) | ||
* `render()` no longer clears its container on first render | ||
* Custom `templateFactory`, `TemplateProcessor`, and custom tag functions are no | ||
longer supported | ||
See the full [changelog](CHANGELOG.md#200-pre1---2020-09-21) for more details on | ||
these and other minor breaking changes. | ||
## 🚨 Known issues/limitations | ||
* **Browser support**: This pre-release should run on modern browsers, however a | ||
change to factor legacy browser support (IE11, etc.) into an opt-in package is | ||
ongoing. As such, this release will not run on some older browsers. This is a | ||
temporary state. | ||
* **Limited directive implementation**: The following directives are not yet | ||
implemented. This is a temporary state: | ||
* `asyncAppend` | ||
* `asyncReplace` | ||
* `until` | ||
## 🚨 Migrating directives | ||
While the API for _using_ directives should be 100% backward-compatible with | ||
1.x, there is a breaking change to how custom directives are _authored_. The API | ||
change improves ergonomics around making stateful directives while providing a | ||
clear pattern for SSR-compatible directives: only `render` will be called on the | ||
server, while `update` will not be. | ||
**⚠️ WARNING: The directive and part API changes are in progress and subject to | ||
change in future pre-releases.** | ||
<details> | ||
<summary>Expand here for details on migrating directives.</summary> | ||
### Overview of directive API changes | ||
| | 1.x API | 2.0 API | | ||
|-|-----|-----| | ||
| Code idiom for directive | function that takes directive arguments, and returns function that takes `part` and returns value | class with `update` & `render` methods which accept directive arguments | | ||
| Where to do declarative rendering | pass value to `part.setValue()` | return value from `render()` method | | ||
| Where to do imperative DOM/part manipulation | directive function | `update()` method | | ||
| Where state is stored between renders | `WeakMap` keyed on `part` | class instance fields | | ||
| How part validation is done | `instanceof` check on `part` in every render | `part.type` check in constructor | ||
### Example directive migration | ||
Below is an example of a lit-html 1.x directive, and how to migrate it to the | ||
new API: | ||
1.x Directive API: | ||
```js | ||
import {directive, NodePart, html} from 'lit-html'; | ||
// State stored in WeakMap | ||
const previousState = new WeakMap(); | ||
// Functional-based directive API | ||
export const renderCounter = directive((initialValue) => (part) => { | ||
// When necessary, validate part type each render using `instanceof` | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('renderCounter only supports NodePart'); | ||
} | ||
// Retrieve value from previous state | ||
let value = previousState.get(part); | ||
// Update state | ||
if (previous === undefined) { | ||
value = initialValue; | ||
} else { | ||
value++; | ||
} | ||
// Store state | ||
previousState.set(part, value); | ||
// Update part with new rendering | ||
part.setValue(html`<p>${value}</p>`); | ||
}); | ||
``` | ||
2.0 Directive API: | ||
```js | ||
import {directive, Directive, NODE_PART, html} from 'lit-html'; | ||
// Class-based directive API | ||
export const renderCounter = directive(class extends Directive { | ||
// State stored in class field | ||
value = undefined; | ||
constructor(part) { | ||
super(); | ||
// When necessary, validate part in constructor using `part.type` | ||
if (part.type !== NODE_PART) { | ||
throw new Error('renderCounter only supports NodePart'); | ||
} | ||
} | ||
// Any imperative updates to DOM/parts would go here | ||
update(part, [initialValue]) { | ||
// ... | ||
} | ||
// Do SSR-compatible rendering (arguments are passed from call site) | ||
render(initialValue) { | ||
// Previous state available on class field | ||
if (this.value === undefined) { | ||
this.value = initialValue; | ||
} else { | ||
this.value++; | ||
} | ||
return html`<p>${this.value}</p>`; | ||
} | ||
}); | ||
``` | ||
</details> | ||
<hr> | ||
# lit-html | ||
## Overview | ||
@@ -47,2 +170,2 @@ | ||
Please see [CONTRIBUTING.md](./CONTRIBUTING.md). | ||
Please see [CONTRIBUTING.md](../../CONTRIBUTING.md). |
/** | ||
* @license | ||
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved. | ||
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved. | ||
* This code may only be used under the BSD style license found at | ||
@@ -15,13 +15,5 @@ * http://polymer.github.io/LICENSE.txt | ||
import {TemplateInstance} from '../lib/template-instance.js'; | ||
import {Template} from '../lib/template.js'; | ||
import {directive, NodePart, Part, reparentNodes, TemplateResult} from '../lit-html.js'; | ||
import {directive, TemplateResult, NodePart, Directive} from '../lit-html.js'; | ||
import {detachNodePart, restoreNodePart, NodePartState} from '../parts.js'; | ||
interface CachedTemplate { | ||
readonly instance: TemplateInstance; | ||
readonly nodes: DocumentFragment; | ||
} | ||
const templateCaches = | ||
new WeakMap<NodePart, WeakMap<Template, CachedTemplate>>(); | ||
/** | ||
@@ -41,52 +33,36 @@ * Enables fast switching between multiple templates by caching the DOM nodes | ||
*/ | ||
export const cache = directive((value: unknown) => (part: Part) => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('cache can only be used in text bindings'); | ||
} | ||
export const cache = directive( | ||
class extends Directive { | ||
templateCache = new WeakMap<TemplateStringsArray, NodePartState>(); | ||
value?: TemplateResult; | ||
let templateCache = templateCaches.get(part); | ||
render(v: unknown) { | ||
return v; | ||
} | ||
if (templateCache === undefined) { | ||
templateCache = new WeakMap(); | ||
templateCaches.set(part, templateCache); | ||
} | ||
update(part: NodePart, [v]: Parameters<this['render']>) { | ||
// If the new value is not a TemplateResult from the same Template as the | ||
// previous value, move the nodes from the DOM into the cache. | ||
if ( | ||
this.value !== undefined && | ||
this.value.strings !== (v as TemplateResult).strings | ||
) { | ||
this.templateCache.set(this.value.strings, detachNodePart(part)); | ||
} | ||
const previousValue = part.value; | ||
// First, can we update the current TemplateInstance, or do we need to move | ||
// the current nodes into the cache? | ||
if (previousValue instanceof TemplateInstance) { | ||
if (value instanceof TemplateResult && | ||
previousValue.template === part.options.templateFactory(value)) { | ||
// Same Template, just trigger an update of the TemplateInstance | ||
part.setValue(value); | ||
return; | ||
} else { | ||
// Not the same Template, move the nodes from the DOM into the cache. | ||
let cachedTemplate = templateCache.get(previousValue.template); | ||
if (cachedTemplate === undefined) { | ||
cachedTemplate = { | ||
instance: previousValue, | ||
nodes: document.createDocumentFragment(), | ||
}; | ||
templateCache.set(previousValue.template, cachedTemplate); | ||
// If the new value is a TemplateResult, try to restore it from cache | ||
if ((v as TemplateResult)._$litType$ !== undefined) { | ||
let cachedTemplate = this.templateCache.get( | ||
(v as TemplateResult).strings | ||
); | ||
if (cachedTemplate !== undefined) { | ||
restoreNodePart(part, cachedTemplate); | ||
} | ||
this.value = v as TemplateResult; | ||
} else { | ||
this.value = undefined; | ||
} | ||
reparentNodes( | ||
cachedTemplate.nodes, part.startNode.nextSibling, part.endNode); | ||
return this.render(v); | ||
} | ||
} | ||
// Next, can we reuse nodes from the cache? | ||
if (value instanceof TemplateResult) { | ||
const template = part.options.templateFactory(value); | ||
const cachedTemplate = templateCache.get(template); | ||
if (cachedTemplate !== undefined) { | ||
// Move nodes out of cache | ||
part.setValue(cachedTemplate.nodes); | ||
part.commit(); | ||
// Set the Part value to the TemplateInstance so it'll update it. | ||
part.value = cachedTemplate.instance; | ||
} | ||
} | ||
part.setValue(value); | ||
}); | ||
); |
@@ -15,105 +15,102 @@ /** | ||
import {AttributePart, directive, Part, PropertyPart} from '../lit-html.js'; | ||
import { | ||
AttributePart, | ||
directive, | ||
Directive, | ||
noChange, | ||
PartInfo, | ||
ATTRIBUTE_PART, | ||
} from '../lit-html.js'; | ||
// IE11 doesn't support classList on SVG elements, so we emulate it with a Set | ||
class ClassList { | ||
element: Element; | ||
classes: Set<string> = new Set(); | ||
changed = false; | ||
constructor(element: Element) { | ||
this.element = element; | ||
const classList = (element.getAttribute('class') || '').split(/\s+/); | ||
for (const cls of classList) { | ||
this.classes.add(cls); | ||
} | ||
} | ||
add(cls: string) { | ||
this.classes.add(cls); | ||
this.changed = true; | ||
} | ||
remove(cls: string) { | ||
this.classes.delete(cls); | ||
this.changed = true; | ||
} | ||
commit() { | ||
if (this.changed) { | ||
let classString = ''; | ||
this.classes.forEach((cls) => classString += cls + ' '); | ||
this.element.setAttribute('class', classString); | ||
} | ||
} | ||
} | ||
/** | ||
* A key-value set of class names to truthy values. | ||
*/ | ||
export interface ClassInfo { | ||
readonly [name: string]: string|boolean|number; | ||
readonly [name: string]: string | boolean | number; | ||
} | ||
/** | ||
* Stores the ClassInfo object applied to a given AttributePart. | ||
* Used to unset existing values when a new ClassInfo object is applied. | ||
*/ | ||
const previousClassesCache = new WeakMap<Part, Set<string>>(); | ||
class ClassMap extends Directive { | ||
/** | ||
* Stores the ClassInfo object applied to a given AttributePart. | ||
* Used to unset existing values when a new ClassInfo object is applied. | ||
*/ | ||
previousClasses?: Set<string>; | ||
/** | ||
* A directive that applies CSS classes. This must be used in the `class` | ||
* attribute and must be the only part used in the attribute. It takes each | ||
* property in the `classInfo` argument and adds the property name to the | ||
* element's `class` if the property value is truthy; if the property value is | ||
* falsey, the property name is removed from the element's `class`. For example | ||
* `{foo: bar}` applies the class `foo` if the value of `bar` is truthy. | ||
* @param classInfo {ClassInfo} | ||
*/ | ||
export const classMap = directive((classInfo: ClassInfo) => (part: Part) => { | ||
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) || | ||
part.committer.name !== 'class' || part.committer.parts.length > 1) { | ||
throw new Error( | ||
constructor(part: PartInfo) { | ||
super(); | ||
if ( | ||
part.type !== ATTRIBUTE_PART || | ||
part.name !== 'class' || | ||
(part.strings !== undefined && part.strings.length > 2) | ||
) { | ||
throw new Error( | ||
'The `classMap` directive must be used in the `class` attribute ' + | ||
'and must be the only part in the attribute.'); | ||
'and must be the only part in the attribute.' | ||
); | ||
} | ||
} | ||
const {committer} = part; | ||
const {element} = committer; | ||
let previousClasses = previousClassesCache.get(part); | ||
if (previousClasses === undefined) { | ||
// Write static classes once | ||
// Use setAttribute() because className isn't a string on SVG elements | ||
element.setAttribute('class', committer.strings.join(' ')); | ||
previousClassesCache.set(part, previousClasses = new Set()); | ||
render(classInfo: ClassInfo) { | ||
return Object.keys(classInfo) | ||
.filter((key) => classInfo[key]) | ||
.join(' '); | ||
} | ||
const classList = | ||
(element.classList || new ClassList(element)) as DOMTokenList | ClassList; | ||
// Remove old classes that no longer apply | ||
// We use forEach() instead of for-of so that re don't require down-level | ||
// iteration. | ||
previousClasses.forEach((name) => { | ||
if (!(name in classInfo)) { | ||
classList.remove(name); | ||
previousClasses!.delete(name); | ||
update(part: AttributePart, [classInfo]: [ClassInfo]) { | ||
// Remember dynamic classes on the first render | ||
if (this.previousClasses === undefined) { | ||
this.previousClasses = new Set(); | ||
for (const name in classInfo) { | ||
if (classInfo[name]) { | ||
this.previousClasses.add(name); | ||
} | ||
} | ||
return this.render(classInfo); | ||
} | ||
}); | ||
// Add or remove classes based on their classMap value | ||
for (const name in classInfo) { | ||
const value = classInfo[name]; | ||
if (value != previousClasses.has(name)) { | ||
const classList = part.element.classList; | ||
// Remove old classes that no longer apply | ||
// We use forEach() instead of for-of so that we don't require down-level | ||
// iteration. | ||
this.previousClasses.forEach((name) => { | ||
if (!(name in classInfo)) { | ||
classList.remove(name); | ||
this.previousClasses!.delete(name); | ||
} | ||
}); | ||
// Add or remove classes based on their classMap value | ||
for (const name in classInfo) { | ||
// We explicitly want a loose truthy check of `value` because it seems | ||
// more convenient that '' and 0 are skipped. | ||
if (value) { | ||
classList.add(name); | ||
previousClasses.add(name); | ||
} else { | ||
classList.remove(name); | ||
previousClasses.delete(name); | ||
const value = !!classInfo[name]; | ||
if (value !== this.previousClasses.has(name)) { | ||
if (value) { | ||
classList.add(name); | ||
this.previousClasses.add(name); | ||
} else { | ||
classList.remove(name); | ||
this.previousClasses.delete(name); | ||
} | ||
} | ||
} | ||
return noChange; | ||
} | ||
if (typeof (classList as ClassList).commit === 'function') { | ||
(classList as ClassList).commit(); | ||
} | ||
}); | ||
} | ||
/** | ||
* A directive that applies dynamic CSS classes. | ||
* | ||
* This must be used in the `class` attribute and must be the only part used in | ||
* the attribute. It takes each property in the `classInfo` argument and adds | ||
* the property name to the element's `classList` if the property value is | ||
* truthy; if the property value is falsey, the property name is removed from | ||
* the element's `class`. | ||
* | ||
* For example `{foo: bar}` applies the class `foo` if the value of `bar` is | ||
* truthy. | ||
* | ||
* @param classInfo {ClassInfo} | ||
*/ | ||
export const classMap = directive(ClassMap); |
@@ -15,6 +15,37 @@ /** | ||
import {directive, Part} from '../lit-html.js'; | ||
import {directive, Directive, noChange, Part} from '../lit-html.js'; | ||
const previousValues = new WeakMap<Part, unknown>(); | ||
// A sentinal that indicates guard() hasn't rendered anything yet | ||
const initialValue = {}; | ||
class Guard extends Directive { | ||
previousValue: unknown = initialValue; | ||
render(_value: unknown, f: () => unknown) { | ||
return f(); | ||
} | ||
update(_part: Part, [value, f]: Parameters<this['render']>) { | ||
if (Array.isArray(value)) { | ||
// Dirty-check arrays by item | ||
if ( | ||
Array.isArray(this.previousValue) && | ||
this.previousValue.length === value.length && | ||
value.every((v, i) => v === (this.previousValue as Array<unknown>)[i]) | ||
) { | ||
return noChange; | ||
} | ||
} else if (this.previousValue === value) { | ||
// Dirty-check non-arrays by identity | ||
return noChange; | ||
} | ||
// Copy the value if it's an array so that if it's mutated we don't forget | ||
// what the previous values were. | ||
this.previousValue = Array.isArray(value) ? Array.from(value) : value; | ||
const r = this.render(value, f); | ||
return r; | ||
} | ||
} | ||
/** | ||
@@ -24,2 +55,8 @@ * Prevents re-render of a template function until a single value or an array of | ||
* | ||
* Values are checked against previous values with strict equality (`===`), and | ||
* so the check won't detect nested property changes inside objects or arrays. | ||
* Arrays values have each item checked against the previous value at the same | ||
* index with strict equality. Nested arrays are also checked only by strict | ||
* equality. | ||
* | ||
* Example: | ||
@@ -34,3 +71,3 @@ * | ||
* | ||
* In this case, the template only renders if either `user.id` or `company.id` | ||
* In this case, the template only rerenders if either `user.id` or `company.id` | ||
* changes. | ||
@@ -55,24 +92,2 @@ * | ||
*/ | ||
export const guard = | ||
directive((value: unknown, f: () => unknown) => (part: Part): void => { | ||
const previousValue = previousValues.get(part); | ||
if (Array.isArray(value)) { | ||
// Dirty-check arrays by item | ||
if (Array.isArray(previousValue) && | ||
previousValue.length === value.length && | ||
value.every((v, i) => v === previousValue[i])) { | ||
return; | ||
} | ||
} else if ( | ||
previousValue === value && | ||
(value !== undefined || previousValues.has(part))) { | ||
// Dirty-check non-arrays by identity | ||
return; | ||
} | ||
part.setValue(f()); | ||
// Copy the value if it's an array so that if it's mutated we don't forget | ||
// what the previous values were. | ||
previousValues.set( | ||
part, Array.isArray(value) ? Array.from(value) : value); | ||
}); | ||
export const guard = directive(Guard); |
@@ -15,6 +15,4 @@ /** | ||
import {AttributePart, directive, Part} from '../lit-html.js'; | ||
import {nothing} from '../lit-html.js'; | ||
const previousValues = new WeakMap<Part, unknown>(); | ||
/** | ||
@@ -26,17 +24,2 @@ * For AttributeParts, sets the attribute if the value is defined and removes | ||
*/ | ||
export const ifDefined = directive((value: unknown) => (part: Part) => { | ||
const previousValue = previousValues.get(part); | ||
if (value === undefined && part instanceof AttributePart) { | ||
// If the value is undefined, remove the attribute, but only if the value | ||
// was previously defined. | ||
if (previousValue !== undefined || !previousValues.has(part)) { | ||
const name = part.committer.name; | ||
part.committer.element.removeAttribute(name); | ||
} | ||
} else if (value !== previousValue) { | ||
part.setValue(value); | ||
} | ||
previousValues.set(part, value); | ||
}); | ||
export const ifDefined = (value: unknown) => value ?? nothing; |
@@ -15,4 +15,70 @@ /** | ||
import {AttributePart, BooleanAttributePart, directive, EventPart, NodePart, PropertyPart} from '../lit-html.js'; | ||
import { | ||
directive, | ||
Directive, | ||
AttributePart, | ||
noChange, | ||
nothing, | ||
PartInfo, | ||
NODE_PART, | ||
EVENT_PART, | ||
PROPERTY_PART, | ||
BOOLEAN_ATTRIBUTE_PART, | ||
ATTRIBUTE_PART, | ||
} from '../lit-html.js'; | ||
// A sentinal value that can never appear as a part value except when set by | ||
// live(). Used to force a dirty-check to fail and cause a re-render. | ||
const RESET_VALUE = {}; | ||
class LiveDirective extends Directive { | ||
constructor(part: PartInfo) { | ||
super(); | ||
if (part.type === EVENT_PART || part.type === NODE_PART) { | ||
throw new Error( | ||
'The `live` directive is not allowed on text or event bindings' | ||
); | ||
} | ||
if (part.strings !== undefined) { | ||
throw new Error('`live` bindings can only contain a single expression'); | ||
} | ||
} | ||
render(value: unknown) { | ||
return value; | ||
} | ||
update(part: AttributePart, [value]: Parameters<this['render']>) { | ||
if (value === noChange) { | ||
return value; | ||
} | ||
const element = part.element; | ||
const name = part.name; | ||
// TODO (justinfagnani): This is essentially implementing a getLiveValue() | ||
// method for each part type. Should that be moved into the AttributePart | ||
// interface? | ||
if (part.type === PROPERTY_PART) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
if (value === (element as any)[name]) { | ||
return noChange; | ||
} | ||
} else if (part.type === BOOLEAN_ATTRIBUTE_PART) { | ||
if ( | ||
(value === nothing ? false : !!value) === element.hasAttribute(name) | ||
) { | ||
return noChange; | ||
} | ||
} else if (part.type === ATTRIBUTE_PART) { | ||
if (element.getAttribute(name) === String(value)) { | ||
return noChange; | ||
} | ||
} | ||
// Setting the part's value to RESET_VALUE causes its dirty-check to fail | ||
// so that it always sets the value. | ||
part._value = RESET_VALUE; | ||
return value; | ||
} | ||
} | ||
/** | ||
@@ -40,39 +106,2 @@ * Checks binding values against live DOM values, instead of previously bound | ||
*/ | ||
export const live = directive( | ||
(value: unknown) => (part: AttributePart|PropertyPart| | ||
BooleanAttributePart) => { | ||
let previousValue: unknown; | ||
if (part instanceof EventPart || part instanceof NodePart) { | ||
throw new Error( | ||
'The `live` directive is not allowed on text or event bindings'); | ||
} | ||
if (part instanceof BooleanAttributePart) { | ||
checkStrings(part.strings); | ||
previousValue = part.element.hasAttribute(part.name); | ||
// This is a hack needed because BooleanAttributePart doesn't have a | ||
// committer and does its own dirty checking after directives | ||
part.value = previousValue; | ||
} else { | ||
const {element, name, strings} = part.committer; | ||
checkStrings(strings); | ||
if (part instanceof PropertyPart) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
previousValue = (element as any)[name]; | ||
if (previousValue === value) { | ||
return; | ||
} | ||
} else if (part instanceof AttributePart) { | ||
previousValue = element.getAttribute(name); | ||
} | ||
if (previousValue === String(value)) { | ||
return; | ||
} | ||
} | ||
part.setValue(value); | ||
}); | ||
const checkStrings = (strings: readonly string[]) => { | ||
if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') { | ||
throw new Error('`live` bindings can only contain a single expression'); | ||
} | ||
}; | ||
export const live = directive(LiveDirective); |
@@ -15,4 +15,16 @@ /** | ||
import {DirectiveFn} from '../lib/directive.js'; | ||
import {createMarker, directive, NodePart, Part, removeNodes, reparentNodes} from '../lit-html.js'; | ||
import { | ||
directive, | ||
NodePart, | ||
Directive, | ||
noChange, | ||
PartInfo, | ||
} from '../lit-html.js'; | ||
import { | ||
createAndInsertPart, | ||
getPartValue, | ||
insertPartBefore, | ||
removePart, | ||
setPartValue, | ||
} from '../parts.js'; | ||
@@ -22,37 +34,2 @@ export type KeyFn<T> = (item: T, index: number) => unknown; | ||
// Helper functions for manipulating parts | ||
// TODO(kschaaf): Refactor into Part API? | ||
const createAndInsertPart = | ||
(containerPart: NodePart, beforePart?: NodePart): NodePart => { | ||
const container = containerPart.startNode.parentNode as Node; | ||
const beforeNode = beforePart === undefined ? containerPart.endNode : | ||
beforePart.startNode; | ||
const startNode = container.insertBefore(createMarker(), beforeNode); | ||
container.insertBefore(createMarker(), beforeNode); | ||
const newPart = new NodePart(containerPart.options); | ||
newPart.insertAfterNode(startNode); | ||
return newPart; | ||
}; | ||
const updatePart = (part: NodePart, value: unknown) => { | ||
part.setValue(value); | ||
part.commit(); | ||
return part; | ||
}; | ||
const insertPartBefore = | ||
(containerPart: NodePart, part: NodePart, ref?: NodePart) => { | ||
const container = containerPart.startNode.parentNode as Node; | ||
const beforeNode = ref ? ref.startNode : containerPart.endNode; | ||
const endNode = part.endNode.nextSibling; | ||
if (endNode !== beforeNode) { | ||
reparentNodes(container, part.startNode, endNode, beforeNode); | ||
} | ||
}; | ||
const removePart = (part: NodePart) => { | ||
removeNodes( | ||
part.startNode.parentNode!, part.startNode, part.endNode.nextSibling); | ||
}; | ||
// Helper for generating a map of array item to its index over a subset | ||
@@ -69,6 +46,2 @@ // of an array (used to lazily generate `newKeyToIndexMap` and | ||
// Stores previous ordered list of parts and map of key to index | ||
const partListCache = new WeakMap<NodePart, (NodePart | null)[]>(); | ||
const keyListCache = new WeakMap<NodePart, unknown[]>(); | ||
/** | ||
@@ -93,354 +66,404 @@ * A directive that repeats a series of values (usually `TemplateResults`) | ||
*/ | ||
export const repeat = | ||
directive( | ||
<T>(items: Iterable<T>, | ||
keyFnOrTemplate: KeyFn<T>|ItemTemplate<T>, | ||
template?: ItemTemplate<T>): | ||
DirectiveFn => { | ||
let keyFn: KeyFn<T>; | ||
if (template === undefined) { | ||
template = keyFnOrTemplate; | ||
} else if (keyFnOrTemplate !== undefined) { | ||
keyFn = keyFnOrTemplate as KeyFn<T>; | ||
} | ||
return (containerPart: Part): void => { | ||
if (!(containerPart instanceof NodePart)) { | ||
throw new Error('repeat can only be used in text bindings'); | ||
} | ||
// Old part & key lists are retrieved from the last update | ||
// (associated with the part for this instance of the directive) | ||
const oldParts = partListCache.get(containerPart) || []; | ||
const oldKeys = keyListCache.get(containerPart) || []; | ||
class RepeatDirective extends Directive { | ||
itemKeys?: unknown[]; | ||
// New part list will be built up as we go (either reused from | ||
// old parts or created for new keys in this update). This is | ||
// saved in the above cache at the end of the update. | ||
const newParts: NodePart[] = []; | ||
constructor(part: PartInfo) { | ||
super(); | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('repeat can only be used in text bindings'); | ||
} | ||
} | ||
// New value list is eagerly generated from items along with a | ||
// parallel array indicating its key. | ||
const newValues: unknown[] = []; | ||
const newKeys: unknown[] = []; | ||
let index = 0; | ||
for (const item of items) { | ||
newKeys[index] = keyFn ? keyFn(item, index) : index; | ||
newValues[index] = template !(item, index); | ||
index++; | ||
} | ||
_getValuesAndKeys<T>( | ||
items: Iterable<T>, | ||
keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>, | ||
template?: ItemTemplate<T> | ||
) { | ||
let keyFn: KeyFn<T> | undefined; | ||
if (template === undefined) { | ||
template = keyFnOrTemplate; | ||
} else if (keyFnOrTemplate !== undefined) { | ||
keyFn = keyFnOrTemplate as KeyFn<T>; | ||
} | ||
const keys = []; | ||
const values = []; | ||
let index = 0; | ||
for (const item of items) { | ||
keys[index] = keyFn ? keyFn(item, index) : index; | ||
values[index] = template!(item, index); | ||
index++; | ||
} | ||
return { | ||
values, | ||
keys, | ||
}; | ||
} | ||
// Maps from key to index for current and previous update; these | ||
// are generated lazily only when needed as a performance | ||
// optimization, since they are only required for multiple | ||
// non-contiguous changes in the list, which are less common. | ||
let newKeyToIndexMap!: Map<unknown, number>; | ||
let oldKeyToIndexMap!: Map<unknown, number>; | ||
render<T>( | ||
items: Iterable<T>, | ||
keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>, | ||
template?: ItemTemplate<T> | ||
) { | ||
return this._getValuesAndKeys(items, keyFnOrTemplate, template).values; | ||
} | ||
// Head and tail pointers to old parts and new values | ||
let oldHead = 0; | ||
let oldTail = oldParts.length - 1; | ||
let newHead = 0; | ||
let newTail = newValues.length - 1; | ||
update<T>( | ||
containerPart: NodePart, | ||
[items, keyFnOrTemplate, template]: [ | ||
Iterable<T>, | ||
KeyFn<T> | ItemTemplate<T>, | ||
ItemTemplate<T> | ||
] | ||
) { | ||
// Old part & key lists are retrieved from the last update | ||
// TODO: deal with directive being swapped out? | ||
let oldParts = getPartValue(containerPart) as Array<NodePart | null>; | ||
const {values: newValues, keys: newKeys} = this._getValuesAndKeys( | ||
items, | ||
keyFnOrTemplate, | ||
template | ||
); | ||
// Overview of O(n) reconciliation algorithm (general approach | ||
// based on ideas found in ivi, vue, snabbdom, etc.): | ||
// | ||
// * We start with the list of old parts and new values (and | ||
// arrays of their respective keys), head/tail pointers into | ||
// each, and we build up the new list of parts by updating | ||
// (and when needed, moving) old parts or creating new ones. | ||
// The initial scenario might look like this (for brevity of | ||
// the diagrams, the numbers in the array reflect keys | ||
// associated with the old parts or new values, although keys | ||
// and parts/values are actually stored in parallel arrays | ||
// indexed using the same head/tail pointers): | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [ , , , , , , ] | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new | ||
// item order | ||
// newHead ^ ^ newTail | ||
// | ||
// * Iterate old & new lists from both sides, updating, | ||
// swapping, or removing parts at the head/tail locations | ||
// until neither head nor tail can move. | ||
// | ||
// * Example below: keys at head pointers match, so update old | ||
// part 0 in-place (no need to move it) and record part 0 in | ||
// the `newParts` list. The last thing we do is advance the | ||
// `oldHead` and `newHead` pointers (will be reflected in the | ||
// next diagram). | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , ] <- heads matched: update 0 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead | ||
// & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Example below: head pointers don't match, but tail | ||
// pointers do, so update part 6 in place (no need to move | ||
// it), and record part 6 in the `newParts` list. Last, | ||
// advance the `oldTail` and `oldHead` pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , 6] <- tails matched: update 6 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldTail | ||
// & newTail | ||
// newHead ^ ^ newTail | ||
// | ||
// * If neither head nor tail match; next check if one of the | ||
// old head/tail items was removed. We first need to generate | ||
// the reverse map of new keys to index (`newKeyToIndexMap`), | ||
// which is done once lazily as a performance optimization, | ||
// since we only hit this case if multiple non-contiguous | ||
// changes were made. Note that for contiguous removal | ||
// anywhere in the list, the head and tails would advance | ||
// from either end and pass each other before we get to this | ||
// case and removals would be handled in the final while loop | ||
// without needing to generate the map. | ||
// | ||
// * Example below: The key at `oldTail` was removed (no longer | ||
// in the `newKeyToIndexMap`), so remove that part from the | ||
// DOM and advance just the `oldTail` pointer. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , 6] <- 5 not in new map: remove | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] 5 and advance oldTail | ||
// newHead ^ ^ newTail | ||
// | ||
// * Once head and tail cannot move, any mismatches are due to | ||
// either new or moved items; if a new key is in the previous | ||
// "old key to old index" map, move the old part to the new | ||
// location, otherwise create and insert a new part. Note | ||
// that when moving an old part we null its position in the | ||
// oldParts array if it lies between the head and tail so we | ||
// know to skip it when the pointers get there. | ||
// | ||
// * Example below: neither head nor tail match, and neither | ||
// were removed; so find the `newHead` key in the | ||
// `oldKeyToIndexMap`, and move that old part's DOM into the | ||
// next head position (before `oldParts[oldHead]`). Last, | ||
// null the part in the `oldPart` array since it was | ||
// somewhere in the remaining oldParts still to be scanned | ||
// (between the head and tail pointers) so that we know to | ||
// skip that old part on future iterations. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, , , , , 6] <- stuck: update & move 2 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] into place and advance | ||
// newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Note that for moves/insertions like the one above, a part | ||
// inserted at the head pointer is inserted before the | ||
// current `oldParts[oldHead]`, and a part inserted at the | ||
// tail pointer is inserted before `newParts[newTail+1]`. The | ||
// seeming asymmetry lies in the fact that new parts are | ||
// moved into place outside in, so to the right of the head | ||
// pointer are old parts, and to the right of the tail | ||
// pointer are new parts. | ||
// | ||
// * We always restart back from the top of the algorithm, | ||
// allowing matching and simple updates in place to | ||
// continue... | ||
// | ||
// * Example below: the head pointers once again match, so | ||
// simply update part 1 and record it in the `newParts` | ||
// array. Last, advance both head pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, , , , 6] <- heads matched: update 1 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead | ||
// & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * As mentioned above, items that were moved as a result of | ||
// being stuck (the final else clause in the code below) are | ||
// marked with null, so we always advance old pointers over | ||
// these so we're comparing the next actual old value on | ||
// either end. | ||
// | ||
// * Example below: `oldHead` is null (already placed in | ||
// newParts), so advance `oldHead`. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] <- old head already used: | ||
// newParts: [0, 2, 1, , , , 6] advance oldHead | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] | ||
// newHead ^ ^ newTail | ||
// | ||
// * Note it's not critical to mark old parts as null when they | ||
// are moved from head to tail or tail to head, since they | ||
// will be outside the pointer range and never visited again. | ||
// | ||
// * Example below: Here the old tail key matches the new head | ||
// key, so the part at the `oldTail` position and move its | ||
// DOM to the new head position (before `oldParts[oldHead]`). | ||
// Last, advance `oldTail` and `newHead` pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, , , 6] <- old tail matches new | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] head: update & move 4, | ||
// advance oldTail & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Example below: Old and new head keys match, so update the | ||
// old head part in place, and advance the `oldHead` and | ||
// `newHead` pointers. | ||
// | ||
// oldHead v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance oldHead & | ||
// newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Once the new or old pointers move past each other then all | ||
// we have left is additions (if old list exhausted) or | ||
// removals (if new list exhausted). Those are handled in the | ||
// final while loops at the end. | ||
// | ||
// * Example below: `oldHead` exceeded `oldTail`, so we're done | ||
// with the main loop. Create the remaining part and insert | ||
// it at the new head position, and the update is complete. | ||
// | ||
// (oldHead > oldTail) | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, 3, 7 ,6] <- create and insert 7 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] | ||
// newHead ^ newTail | ||
// | ||
// * Note that the order of the if/else clauses is not | ||
// important to the algorithm, as long as the null checks | ||
// come first (to ensure we're always working on valid old | ||
// parts) and that the final else clause comes last (since | ||
// that's where the expensive moves occur). The order of | ||
// remaining clauses is is just a simple guess at which cases | ||
// will be most common. | ||
// | ||
// * TODO(kschaaf) Note, we could calculate the longest | ||
// increasing subsequence (LIS) of old items in new position, | ||
// and only move those not in the LIS set. However that costs | ||
// O(nlogn) time and adds a bit more code, and only helps | ||
// make rare types of mutations require fewer moves. The | ||
// above handles removes, adds, reversal, swaps, and single | ||
// moves of contiguous items in linear time, in the minimum | ||
// number of moves. As the number of multiple moves where LIS | ||
// might help approaches a random shuffle, the LIS | ||
// optimization becomes less helpful, so it seems not worth | ||
// the code at this point. Could reconsider if a compelling | ||
// case arises. | ||
if (!oldParts) { | ||
this.itemKeys = newKeys; | ||
return newValues; | ||
} | ||
while (oldHead <= oldTail && newHead <= newTail) { | ||
if (oldParts[oldHead] === null) { | ||
// `null` means old part at head has already been used | ||
// below; skip | ||
oldHead++; | ||
} else if (oldParts[oldTail] === null) { | ||
// `null` means old part at tail has already been used | ||
// below; skip | ||
oldTail--; | ||
} else if (oldKeys[oldHead] === newKeys[newHead]) { | ||
// Old head matches new head; update in place | ||
newParts[newHead] = | ||
updatePart(oldParts[oldHead]!, newValues[newHead]); | ||
oldHead++; | ||
newHead++; | ||
} else if (oldKeys[oldTail] === newKeys[newTail]) { | ||
// Old tail matches new tail; update in place | ||
newParts[newTail] = | ||
updatePart(oldParts[oldTail]!, newValues[newTail]); | ||
oldTail--; | ||
newTail--; | ||
} else if (oldKeys[oldHead] === newKeys[newTail]) { | ||
// Old head matches new tail; update and move to new tail | ||
newParts[newTail] = | ||
updatePart(oldParts[oldHead]!, newValues[newTail]); | ||
insertPartBefore( | ||
containerPart, | ||
oldParts[oldHead]!, | ||
newParts[newTail + 1]); | ||
oldHead++; | ||
newTail--; | ||
} else if (oldKeys[oldTail] === newKeys[newHead]) { | ||
// Old tail matches new head; update and move to new head | ||
newParts[newHead] = | ||
updatePart(oldParts[oldTail]!, newValues[newHead]); | ||
insertPartBefore( | ||
containerPart, oldParts[oldTail]!, oldParts[oldHead]!); | ||
oldTail--; | ||
newHead++; | ||
} else { | ||
if (newKeyToIndexMap === undefined) { | ||
// Lazily generate key-to-index maps, used for removals & | ||
// moves below | ||
newKeyToIndexMap = generateMap(newKeys, newHead, newTail); | ||
oldKeyToIndexMap = generateMap(oldKeys, oldHead, oldTail); | ||
} | ||
if (!newKeyToIndexMap.has(oldKeys[oldHead])) { | ||
// Old head is no longer in new list; remove | ||
removePart(oldParts[oldHead]!); | ||
oldHead++; | ||
} else if (!newKeyToIndexMap.has(oldKeys[oldTail])) { | ||
// Old tail is no longer in new list; remove | ||
removePart(oldParts[oldTail]!); | ||
oldTail--; | ||
} else { | ||
// Any mismatches at this point are due to additions or | ||
// moves; see if we have an old part we can reuse and move | ||
// into place | ||
const oldIndex = oldKeyToIndexMap.get(newKeys[newHead]); | ||
const oldPart = | ||
oldIndex !== undefined ? oldParts[oldIndex] : null; | ||
if (oldPart === null) { | ||
// No old part for this value; create a new one and | ||
// insert it | ||
const newPart = createAndInsertPart( | ||
containerPart, oldParts[oldHead]!); | ||
updatePart(newPart, newValues[newHead]); | ||
newParts[newHead] = newPart; | ||
} else { | ||
// Reuse old part | ||
newParts[newHead] = | ||
updatePart(oldPart, newValues[newHead]); | ||
insertPartBefore( | ||
containerPart, oldPart, oldParts[oldHead]!); | ||
// This marks the old part as having been used, so that | ||
// it will be skipped in the first two checks above | ||
oldParts[oldIndex as number] = null; | ||
} | ||
newHead++; | ||
} | ||
} | ||
} | ||
// Add parts for any remaining new values | ||
while (newHead <= newTail) { | ||
// For all remaining additions, we insert before last new | ||
// tail, since old pointers are no longer valid | ||
const newPart = | ||
createAndInsertPart(containerPart, newParts[newTail + 1]); | ||
updatePart(newPart, newValues[newHead]); | ||
newParts[newHead++] = newPart; | ||
} | ||
// Remove any remaining unused old parts | ||
while (oldHead <= oldTail) { | ||
const oldPart = oldParts[oldHead++]; | ||
if (oldPart !== null) { | ||
removePart(oldPart); | ||
} | ||
} | ||
// Save order of new parts for next round | ||
partListCache.set(containerPart, newParts); | ||
keyListCache.set(containerPart, newKeys); | ||
}; | ||
}) as | ||
<T>(items: Iterable<T>, | ||
keyFnOrTemplate: KeyFn<T>|ItemTemplate<T>, | ||
template?: ItemTemplate<T>) => DirectiveFn; | ||
const oldKeys = (this.itemKeys ??= []); | ||
// New part list will be built up as we go (either reused from | ||
// old parts or created for new keys in this update). This is | ||
// saved in the above cache at the end of the update. | ||
const newParts: NodePart[] = []; | ||
// Maps from key to index for current and previous update; these | ||
// are generated lazily only when needed as a performance | ||
// optimization, since they are only required for multiple | ||
// non-contiguous changes in the list, which are less common. | ||
let newKeyToIndexMap!: Map<unknown, number>; | ||
let oldKeyToIndexMap!: Map<unknown, number>; | ||
// Head and tail pointers to old parts and new values | ||
let oldHead = 0; | ||
let oldTail = oldParts.length - 1; | ||
let newHead = 0; | ||
let newTail = newValues.length - 1; | ||
// Overview of O(n) reconciliation algorithm (general approach | ||
// based on ideas found in ivi, vue, snabbdom, etc.): | ||
// | ||
// * We start with the list of old parts and new values (and | ||
// arrays of their respective keys), head/tail pointers into | ||
// each, and we build up the new list of parts by updating | ||
// (and when needed, moving) old parts or creating new ones. | ||
// The initial scenario might look like this (for brevity of | ||
// the diagrams, the numbers in the array reflect keys | ||
// associated with the old parts or new values, although keys | ||
// and parts/values are actually stored in parallel arrays | ||
// indexed using the same head/tail pointers): | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [ , , , , , , ] | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new | ||
// item order | ||
// newHead ^ ^ newTail | ||
// | ||
// * Iterate old & new lists from both sides, updating, | ||
// swapping, or removing parts at the head/tail locations | ||
// until neither head nor tail can move. | ||
// | ||
// * Example below: keys at head pointers match, so update old | ||
// part 0 in-place (no need to move it) and record part 0 in | ||
// the `newParts` list. The last thing we do is advance the | ||
// `oldHead` and `newHead` pointers (will be reflected in the | ||
// next diagram). | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , ] <- heads matched: update 0 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead | ||
// & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Example below: head pointers don't match, but tail | ||
// pointers do, so update part 6 in place (no need to move | ||
// it), and record part 6 in the `newParts` list. Last, | ||
// advance the `oldTail` and `oldHead` pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , 6] <- tails matched: update 6 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldTail | ||
// & newTail | ||
// newHead ^ ^ newTail | ||
// | ||
// * If neither head nor tail match; next check if one of the | ||
// old head/tail items was removed. We first need to generate | ||
// the reverse map of new keys to index (`newKeyToIndexMap`), | ||
// which is done once lazily as a performance optimization, | ||
// since we only hit this case if multiple non-contiguous | ||
// changes were made. Note that for contiguous removal | ||
// anywhere in the list, the head and tails would advance | ||
// from either end and pass each other before we get to this | ||
// case and removals would be handled in the final while loop | ||
// without needing to generate the map. | ||
// | ||
// * Example below: The key at `oldTail` was removed (no longer | ||
// in the `newKeyToIndexMap`), so remove that part from the | ||
// DOM and advance just the `oldTail` pointer. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , 6] <- 5 not in new map: remove | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] 5 and advance oldTail | ||
// newHead ^ ^ newTail | ||
// | ||
// * Once head and tail cannot move, any mismatches are due to | ||
// either new or moved items; if a new key is in the previous | ||
// "old key to old index" map, move the old part to the new | ||
// location, otherwise create and insert a new part. Note | ||
// that when moving an old part we null its position in the | ||
// oldParts array if it lies between the head and tail so we | ||
// know to skip it when the pointers get there. | ||
// | ||
// * Example below: neither head nor tail match, and neither | ||
// were removed; so find the `newHead` key in the | ||
// `oldKeyToIndexMap`, and move that old part's DOM into the | ||
// next head position (before `oldParts[oldHead]`). Last, | ||
// null the part in the `oldPart` array since it was | ||
// somewhere in the remaining oldParts still to be scanned | ||
// (between the head and tail pointers) so that we know to | ||
// skip that old part on future iterations. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, , , , , 6] <- stuck: update & move 2 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] into place and advance | ||
// newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Note that for moves/insertions like the one above, a part | ||
// inserted at the head pointer is inserted before the | ||
// current `oldParts[oldHead]`, and a part inserted at the | ||
// tail pointer is inserted before `newParts[newTail+1]`. The | ||
// seeming asymmetry lies in the fact that new parts are | ||
// moved into place outside in, so to the right of the head | ||
// pointer are old parts, and to the right of the tail | ||
// pointer are new parts. | ||
// | ||
// * We always restart back from the top of the algorithm, | ||
// allowing matching and simple updates in place to | ||
// continue... | ||
// | ||
// * Example below: the head pointers once again match, so | ||
// simply update part 1 and record it in the `newParts` | ||
// array. Last, advance both head pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, , , , 6] <- heads matched: update 1 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead | ||
// & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * As mentioned above, items that were moved as a result of | ||
// being stuck (the final else clause in the code below) are | ||
// marked with null, so we always advance old pointers over | ||
// these so we're comparing the next actual old value on | ||
// either end. | ||
// | ||
// * Example below: `oldHead` is null (already placed in | ||
// newParts), so advance `oldHead`. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] <- old head already used: | ||
// newParts: [0, 2, 1, , , , 6] advance oldHead | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] | ||
// newHead ^ ^ newTail | ||
// | ||
// * Note it's not critical to mark old parts as null when they | ||
// are moved from head to tail or tail to head, since they | ||
// will be outside the pointer range and never visited again. | ||
// | ||
// * Example below: Here the old tail key matches the new head | ||
// key, so the part at the `oldTail` position and move its | ||
// DOM to the new head position (before `oldParts[oldHead]`). | ||
// Last, advance `oldTail` and `newHead` pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, , , 6] <- old tail matches new | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] head: update & move 4, | ||
// advance oldTail & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Example below: Old and new head keys match, so update the | ||
// old head part in place, and advance the `oldHead` and | ||
// `newHead` pointers. | ||
// | ||
// oldHead v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance oldHead & | ||
// newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Once the new or old pointers move past each other then all | ||
// we have left is additions (if old list exhausted) or | ||
// removals (if new list exhausted). Those are handled in the | ||
// final while loops at the end. | ||
// | ||
// * Example below: `oldHead` exceeded `oldTail`, so we're done | ||
// with the main loop. Create the remaining part and insert | ||
// it at the new head position, and the update is complete. | ||
// | ||
// (oldHead > oldTail) | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, 3, 7 ,6] <- create and insert 7 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] | ||
// newHead ^ newTail | ||
// | ||
// * Note that the order of the if/else clauses is not | ||
// important to the algorithm, as long as the null checks | ||
// come first (to ensure we're always working on valid old | ||
// parts) and that the final else clause comes last (since | ||
// that's where the expensive moves occur). The order of | ||
// remaining clauses is is just a simple guess at which cases | ||
// will be most common. | ||
// | ||
// * TODO(kschaaf) Note, we could calculate the longest | ||
// increasing subsequence (LIS) of old items in new position, | ||
// and only move those not in the LIS set. However that costs | ||
// O(nlogn) time and adds a bit more code, and only helps | ||
// make rare types of mutations require fewer moves. The | ||
// above handles removes, adds, reversal, swaps, and single | ||
// moves of contiguous items in linear time, in the minimum | ||
// number of moves. As the number of multiple moves where LIS | ||
// might help approaches a random shuffle, the LIS | ||
// optimization becomes less helpful, so it seems not worth | ||
// the code at this point. Could reconsider if a compelling | ||
// case arises. | ||
while (oldHead <= oldTail && newHead <= newTail) { | ||
if (oldParts[oldHead] === null) { | ||
// `null` means old part at head has already been used | ||
// below; skip | ||
oldHead++; | ||
} else if (oldParts[oldTail] === null) { | ||
// `null` means old part at tail has already been used | ||
// below; skip | ||
oldTail--; | ||
} else if (oldKeys[oldHead] === newKeys[newHead]) { | ||
// Old head matches new head; update in place | ||
newParts[newHead] = setPartValue( | ||
oldParts[oldHead]!, | ||
newValues[newHead] | ||
); | ||
oldHead++; | ||
newHead++; | ||
} else if (oldKeys[oldTail] === newKeys[newTail]) { | ||
// Old tail matches new tail; update in place | ||
newParts[newTail] = setPartValue( | ||
oldParts[oldTail]!, | ||
newValues[newTail] | ||
); | ||
oldTail--; | ||
newTail--; | ||
} else if (oldKeys[oldHead] === newKeys[newTail]) { | ||
// Old head matches new tail; update and move to new tail | ||
newParts[newTail] = setPartValue( | ||
oldParts[oldHead]!, | ||
newValues[newTail] | ||
); | ||
insertPartBefore( | ||
containerPart, | ||
oldParts[oldHead]!, | ||
newParts[newTail + 1] | ||
); | ||
oldHead++; | ||
newTail--; | ||
} else if (oldKeys[oldTail] === newKeys[newHead]) { | ||
// Old tail matches new head; update and move to new head | ||
newParts[newHead] = setPartValue( | ||
oldParts[oldTail]!, | ||
newValues[newHead] | ||
); | ||
insertPartBefore(containerPart, oldParts[oldTail]!, oldParts[oldHead]!); | ||
oldTail--; | ||
newHead++; | ||
} else { | ||
if (newKeyToIndexMap === undefined) { | ||
// Lazily generate key-to-index maps, used for removals & | ||
// moves below | ||
newKeyToIndexMap = generateMap(newKeys, newHead, newTail); | ||
oldKeyToIndexMap = generateMap(oldKeys, oldHead, oldTail); | ||
} | ||
if (!newKeyToIndexMap.has(oldKeys[oldHead])) { | ||
// Old head is no longer in new list; remove | ||
removePart(oldParts[oldHead]!); | ||
oldHead++; | ||
} else if (!newKeyToIndexMap.has(oldKeys[oldTail])) { | ||
// Old tail is no longer in new list; remove | ||
removePart(oldParts[oldTail]!); | ||
oldTail--; | ||
} else { | ||
// Any mismatches at this point are due to additions or | ||
// moves; see if we have an old part we can reuse and move | ||
// into place | ||
const oldIndex = oldKeyToIndexMap.get(newKeys[newHead]); | ||
const oldPart = oldIndex !== undefined ? oldParts[oldIndex] : null; | ||
if (oldPart === null) { | ||
// No old part for this value; create a new one and | ||
// insert it | ||
const newPart = createAndInsertPart( | ||
containerPart, | ||
oldParts[oldHead]! | ||
); | ||
setPartValue(newPart, newValues[newHead]); | ||
newParts[newHead] = newPart; | ||
} else { | ||
// Reuse old part | ||
newParts[newHead] = setPartValue(oldPart, newValues[newHead]); | ||
insertPartBefore(containerPart, oldPart, oldParts[oldHead]!); | ||
// This marks the old part as having been used, so that | ||
// it will be skipped in the first two checks above | ||
oldParts[oldIndex as number] = null; | ||
} | ||
newHead++; | ||
} | ||
} | ||
} | ||
// Add parts for any remaining new values | ||
while (newHead <= newTail) { | ||
// For all remaining additions, we insert before last new | ||
// tail, since old pointers are no longer valid | ||
const newPart = createAndInsertPart(containerPart, newParts[newTail + 1]); | ||
setPartValue(newPart, newValues[newHead]); | ||
newParts[newHead++] = newPart; | ||
} | ||
// Remove any remaining unused old parts | ||
while (oldHead <= oldTail) { | ||
const oldPart = oldParts[oldHead++]; | ||
if (oldPart !== null) { | ||
removePart(oldPart); | ||
} | ||
} | ||
// Save order of new parts for next round | ||
this.itemKeys = newKeys; | ||
// Directly set part value, bypassing it's dirty-checking | ||
// TODO (justinfagnani): resolve with https://github.com/Polymer/lit-html/issues/1261 | ||
containerPart._value = newParts; | ||
return noChange; | ||
} | ||
} | ||
export type RepeatDirectiveFn = <T>( | ||
items: Iterable<T>, | ||
keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>, | ||
template?: ItemTemplate<T> | ||
) => unknown; | ||
export const repeat = directive(RepeatDirective) as RepeatDirectiveFn; |
@@ -15,4 +15,18 @@ /** | ||
import {AttributePart, directive, Part, PropertyPart} from '../lit-html.js'; | ||
import { | ||
AttributePart, | ||
directive, | ||
Directive, | ||
noChange, | ||
PartInfo, | ||
ATTRIBUTE_PART, | ||
} from '../lit-html.js'; | ||
/** | ||
* A key-value set of CSS properties and values. | ||
* | ||
* The key should be either a valid CSS property name string, like | ||
* `'background-color'`, or a valid JavaScript camel case property name | ||
* for CSSStyleDeclaration like `backgroundColor`. | ||
*/ | ||
export interface StyleInfo { | ||
@@ -22,8 +36,79 @@ readonly [name: string]: string; | ||
/** | ||
* Stores the StyleInfo object applied to a given AttributePart. | ||
* Used to unset existing values when a new StyleInfo object is applied. | ||
*/ | ||
const previousStylePropertyCache = new WeakMap<AttributePart, Set<string>>(); | ||
class StyleMap extends Directive { | ||
previousStyleProperties?: Set<string>; | ||
constructor(part: PartInfo) { | ||
super(); | ||
if ( | ||
part.type !== ATTRIBUTE_PART || | ||
part.name !== 'style' || | ||
(part.strings !== undefined && part.strings.length > 2) | ||
) { | ||
throw new Error( | ||
'The `styleMap` directive must be used in the `style` attribute ' + | ||
'and must be the only part in the attribute.' | ||
); | ||
} | ||
} | ||
render(styleInfo: StyleInfo) { | ||
return Object.keys(styleInfo).reduce((style, prop) => { | ||
const value = styleInfo[prop]; | ||
if (value === null) { | ||
return style; | ||
} | ||
// Convert property names from camel-case to dash-case, i.e.: | ||
// `backgroundColor` -> `background-color` | ||
// Vendor-prefixed names need an extra `-` appended to front: | ||
// `webkitAppearance` -> `-webkit-appearance` | ||
// Exception is any property name containing a dash, including | ||
// custom properties; we assume these are already dash-cased i.e.: | ||
// `--my-button-color` --> `--my-button-color` | ||
prop = prop | ||
.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, '-$&') | ||
.toLowerCase(); | ||
return style + `${prop}:${value};`; | ||
}, ''); | ||
} | ||
update(part: AttributePart, [styleInfo]: Parameters<this['render']>) { | ||
const {style} = part.element as HTMLElement; | ||
if (this.previousStyleProperties === undefined) { | ||
this.previousStyleProperties = new Set(); | ||
for (const name in styleInfo) { | ||
this.previousStyleProperties.add(name); | ||
} | ||
return this.render(styleInfo); | ||
} | ||
// Remove old properties that no longer exist in styleInfo | ||
// We use forEach() instead of for-of so that re don't require down-level | ||
// iteration. | ||
this.previousStyleProperties!.forEach((name) => { | ||
if (!(name in styleInfo)) { | ||
this.previousStyleProperties!.delete(name); | ||
if (name.indexOf('-') === -1) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(style as any)[name] = null; | ||
} else { | ||
style.removeProperty(name); | ||
} | ||
} | ||
}); | ||
// Add or update properties | ||
for (const name in styleInfo) { | ||
this.previousStyleProperties.add(name); | ||
if (name.indexOf('-') === -1) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(style as any)[name] = styleInfo[name]; | ||
} else { | ||
style.setProperty(name, styleInfo[name]); | ||
} | ||
} | ||
return noChange; | ||
} | ||
} | ||
/** | ||
@@ -46,46 +131,2 @@ * A directive that applies CSS properties to an element. | ||
*/ | ||
export const styleMap = directive((styleInfo: StyleInfo) => (part: Part) => { | ||
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) || | ||
part.committer.name !== 'style' || part.committer.parts.length > 1) { | ||
throw new Error( | ||
'The `styleMap` directive must be used in the style attribute ' + | ||
'and must be the only part in the attribute.'); | ||
} | ||
const {committer} = part; | ||
const {style} = committer.element as HTMLElement; | ||
let previousStyleProperties = previousStylePropertyCache.get(part); | ||
if (previousStyleProperties === undefined) { | ||
// Write static styles once | ||
style.cssText = committer.strings.join(' '); | ||
previousStylePropertyCache.set(part, previousStyleProperties = new Set()); | ||
} | ||
// Remove old properties that no longer exist in styleInfo | ||
// We use forEach() instead of for-of so that re don't require down-level | ||
// iteration. | ||
previousStyleProperties.forEach((name) => { | ||
if (!(name in styleInfo)) { | ||
previousStyleProperties!.delete(name); | ||
if (name.indexOf('-') === -1) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(style as any)[name] = null; | ||
} else { | ||
style.removeProperty(name); | ||
} | ||
} | ||
}); | ||
// Add or update properties | ||
for (const name in styleInfo) { | ||
previousStyleProperties.add(name); | ||
if (name.indexOf('-') === -1) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(style as any)[name] = styleInfo[name]; | ||
} else { | ||
style.setProperty(name, styleInfo[name]); | ||
} | ||
} | ||
}); | ||
export const styleMap = directive(StyleMap); |
@@ -15,16 +15,29 @@ /** | ||
import {directive, NodePart, Part} from '../lit-html.js'; | ||
import { | ||
Directive, | ||
directive, | ||
noChange, | ||
NODE_PART, | ||
PartInfo, | ||
} from '../lit-html.js'; | ||
interface PreviousValue { | ||
readonly template: HTMLTemplateElement; | ||
readonly fragment: DocumentFragment; | ||
class TemplateContent extends Directive { | ||
private __previousTemplate?: HTMLTemplateElement; | ||
constructor(part: PartInfo) { | ||
super(); | ||
if (part.type !== NODE_PART) { | ||
throw new Error('templateContent can only be used in text bindings'); | ||
} | ||
} | ||
render(template: HTMLTemplateElement) { | ||
if (this.__previousTemplate === template) { | ||
return noChange; | ||
} | ||
this.__previousTemplate = template; | ||
return document.importNode(template.content, true); | ||
} | ||
} | ||
// For each part, remember the value that was last rendered to the part by the | ||
// templateContent directive, and the DocumentFragment that was last set as a | ||
// value. The DocumentFragment is used as a unique key to check if the last | ||
// value rendered to the part was with templateContent. If not, we'll always | ||
// re-render the value passed to templateContent. | ||
const previousValues = new WeakMap<NodePart, PreviousValue>(); | ||
/** | ||
@@ -37,18 +50,2 @@ * Renders the content of a template element as HTML. | ||
*/ | ||
export const templateContent = | ||
directive((template: HTMLTemplateElement) => (part: Part): void => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('templateContent can only be used in text bindings'); | ||
} | ||
const previousValue = previousValues.get(part); | ||
if (previousValue !== undefined && template === previousValue.template && | ||
part.value === previousValue.fragment) { | ||
return; | ||
} | ||
const fragment = document.importNode(template.content, true); | ||
part.setValue(fragment); | ||
previousValues.set(part, {template, fragment}); | ||
}); | ||
export const templateContent = directive(TemplateContent); |
@@ -15,17 +15,66 @@ /** | ||
import {isPrimitive} from '../lib/parts.js'; | ||
import {directive, NodePart, Part} from '../lit-html.js'; | ||
import { | ||
directive, | ||
Directive, | ||
nothing, | ||
TemplateResult, | ||
noChange, | ||
PartInfo, | ||
NODE_PART, | ||
} from '../lit-html.js'; | ||
interface PreviousValue { | ||
readonly value: unknown; | ||
readonly fragment: DocumentFragment; | ||
const HTML_RESULT = 1; | ||
export class UnsafeHTML extends Directive { | ||
static directiveName = 'unsafeHTML'; | ||
static resultType = HTML_RESULT; | ||
value: unknown = nothing; | ||
templateResult?: TemplateResult; | ||
constructor(part: PartInfo) { | ||
super(); | ||
if (part.type !== NODE_PART) { | ||
throw new Error( | ||
`${ | ||
(this.constructor as typeof UnsafeHTML).directiveName | ||
}() can only be used in text bindings` | ||
); | ||
} | ||
} | ||
render(value: string) { | ||
// TODO: add tests for nothing and noChange | ||
if (value === nothing) { | ||
this.templateResult = undefined; | ||
return (this.value = value); | ||
} | ||
if (value === noChange) { | ||
return value; | ||
} | ||
if (typeof value != 'string') { | ||
throw new Error( | ||
`${ | ||
(this.constructor as typeof UnsafeHTML).directiveName | ||
}() called with a non-string value` | ||
); | ||
} | ||
if (value === this.value) { | ||
return this.templateResult; | ||
} | ||
this.value = value; | ||
const strings = ([value] as unknown) as TemplateStringsArray; | ||
(strings as any).raw = strings; | ||
// WARNING: impersonating a TemplateResult like this is extremely | ||
// dangerous. Third-party directives should not do this. | ||
return (this.templateResult = { | ||
// Cast to a known set of integers that satisfy ResultType so that we | ||
// don't have to export ResultType and possibly encourage this pattern. | ||
_$litType$: (this.constructor as typeof UnsafeHTML).resultType as 1 | 2, | ||
strings, | ||
values: [], | ||
}); | ||
} | ||
} | ||
// For each part, remember the value that was last rendered to the part by the | ||
// unsafeHTML directive, and the DocumentFragment that was last set as a value. | ||
// The DocumentFragment is used as a unique key to check if the last value | ||
// rendered to the part was with unsafeHTML. If not, we'll always re-render the | ||
// value passed to unsafeHTML. | ||
const previousValues = new WeakMap<NodePart, PreviousValue>(); | ||
/** | ||
@@ -38,19 +87,2 @@ * Renders the result as HTML, rather than text. | ||
*/ | ||
export const unsafeHTML = directive((value: unknown) => (part: Part): void => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('unsafeHTML can only be used in text bindings'); | ||
} | ||
const previousValue = previousValues.get(part); | ||
if (previousValue !== undefined && isPrimitive(value) && | ||
value === previousValue.value && part.value === previousValue.fragment) { | ||
return; | ||
} | ||
const template = document.createElement('template'); | ||
template.innerHTML = value as string; // innerHTML casts to string internally | ||
const fragment = document.importNode(template.content, true); | ||
part.setValue(fragment); | ||
previousValues.set(part, {value, fragment}); | ||
}); | ||
export const unsafeHTML = directive(UnsafeHTML); |
@@ -15,20 +15,12 @@ /** | ||
import {reparentNodes} from '../lib/dom.js'; | ||
import {isPrimitive} from '../lib/parts.js'; | ||
import {directive, NodePart, Part} from '../lit-html.js'; | ||
import {directive} from '../lit-html.js'; | ||
import {UnsafeHTML} from './unsafe-html.js'; | ||
interface PreviousValue { | ||
readonly value: unknown; | ||
readonly fragment: DocumentFragment; | ||
const SVG_RESULT = 2; | ||
class UnsafeSVG extends UnsafeHTML { | ||
static directiveName = 'unsafeSVG'; | ||
static resultType = SVG_RESULT; | ||
} | ||
// For each part, remember the value that was last rendered to the part by the | ||
// unsafeSVG directive, and the DocumentFragment that was last set as a value. | ||
// The DocumentFragment is used as a unique key to check if the last value | ||
// rendered to the part was with unsafeSVG. If not, we'll always re-render the | ||
// value passed to unsafeSVG. | ||
const previousValues = new WeakMap<NodePart, PreviousValue>(); | ||
const isIe = window.navigator.userAgent.indexOf('Trident/') > 0; | ||
/** | ||
@@ -41,33 +33,2 @@ * Renders the result as SVG, rather than text. | ||
*/ | ||
export const unsafeSVG = directive((value: unknown) => (part: Part): void => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('unsafeSVG can only be used in text bindings'); | ||
} | ||
const previousValue = previousValues.get(part); | ||
if (previousValue !== undefined && isPrimitive(value) && | ||
value === previousValue.value && part.value === previousValue.fragment) { | ||
return; | ||
} | ||
const template = document.createElement('template'); | ||
const content = template.content; | ||
let svgElement; | ||
if (isIe) { | ||
// IE can't set innerHTML of an svg element. However, it also doesn't | ||
// support Trusted Types, so it's ok for us to use a string when setting | ||
// innerHTML. | ||
template.innerHTML = `<svg>${value}</svg>`; | ||
svgElement = content.firstChild!; | ||
} else { | ||
svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | ||
content.appendChild(svgElement); | ||
svgElement.innerHTML = value as string; | ||
} | ||
content.removeChild(svgElement); | ||
reparentNodes(content, svgElement.firstChild); | ||
const fragment = document.importNode(content, true); | ||
part.setValue(fragment); | ||
previousValues.set(part, {value, fragment}); | ||
}); | ||
export const unsafeSVG = directive(UnsafeSVG); |
1077
src/lit-html.ts
@@ -15,55 +15,164 @@ /** | ||
const DEV_MODE = true; | ||
if (DEV_MODE) { | ||
console.warn('lit-html is in dev mode. Not recommended for production!'); | ||
} | ||
// Added to an attribute name to mark the attribute as bound so we can find | ||
// it easily. | ||
const boundAttributeSuffix = '$lit$'; | ||
// This marker is used in many syntactic positions in HTML, so it must be | ||
// a valid element name and attribute name. We don't support dynamic names (yet) | ||
// but this at least ensures that the parse tree is closer to the template | ||
// intention. | ||
const marker = `lit$${String(Math.random()).slice(9)}$`; | ||
// String used to tell if a comment is a marker comment | ||
const markerMatch = '?' + marker; | ||
// Text used to insert a comment marker node. We use processing instruction | ||
// syntax because it's slightly smaller, but parses as a comment node. | ||
const nodeMarker = `<${markerMatch}>`; | ||
const d = document; | ||
// Creates a dynamic marker. We never have to search for these in the DOM. | ||
const createMarker = (v = '') => d.createComment(v); | ||
// https://tc39.github.io/ecma262/#sec-typeof-operator | ||
type Primitive = null | undefined | boolean | number | string | symbol | bigint; | ||
const isPrimitive = (value: unknown): value is Primitive => | ||
value === null || (typeof value != 'object' && typeof value != 'function'); | ||
const isArray = Array.isArray; | ||
const isIterable = (value: unknown): value is Iterable<unknown> => | ||
isArray(value) || | ||
(value && typeof (value as any)[Symbol.iterator] === 'function'); | ||
// TODO (justinfagnani): can we get away with `\s`? | ||
const SPACE_CHAR = `[ \t\n\f\r]`; | ||
const ATTR_VALUE_CHAR = `[^ \t\n\f\r"'\`<>=]`; | ||
const NAME_CHAR = `[^\0-\x1F\x7F-\x9F "'>=/]`; | ||
// These regexes represent the five parsing states that we care about in the | ||
// Template's HTML scanner. They match the *end* of the state they're named | ||
// after. | ||
// Depending on the match, we transition to a new state. If there's no match, | ||
// we stay in the same state. | ||
// Note that the regexes are stateful. We utilize lastIndex and sync it | ||
// across the multiple regexes used. In addition to the five regexes below | ||
// we also dynamically create a regex to find the matching end tags for raw | ||
// text elements. | ||
// TODO (justinfagnani): we detect many more parsing edge-cases than we | ||
// used to, and many of those are of dubious value. Decide and document | ||
// how to relax correctness to simplify the regexes and states. | ||
/** | ||
* End of text is: `<` followed by: | ||
* (comment start) or (tag) or (dynamic tag binding) | ||
*/ | ||
const textEndRegex = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g; | ||
const COMMENT_START = 1; | ||
const TAG_NAME = 2; | ||
const DYNAMIC_TAG_NAME = 3; | ||
const commentEndRegex = /-->/g; | ||
/** | ||
* Comments not started with <!--, like </{, can be ended by a single `>` | ||
*/ | ||
const comment2EndRegex = />/g; | ||
/** | ||
* The tagEnd regex matches the end of the "inside an opening" tag syntax | ||
* position. It either matches a `>` or an attribute. | ||
* | ||
* Main lit-html module. | ||
* See attributes in the HTML spec: | ||
* https://www.w3.org/TR/html5/syntax.html#elements-attributes | ||
* | ||
* Main exports: | ||
* " \t\n\f\r" are HTML space characters: | ||
* https://infra.spec.whatwg.org/#ascii-whitespace | ||
* | ||
* - [[html]] | ||
* - [[svg]] | ||
* - [[render]] | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every | ||
* space character except " ". | ||
* | ||
* @packageDocumentation | ||
* So an attribute is: | ||
* * The name: any character except a control character, space character, ('), | ||
* ("), ">", "=", or "/" | ||
* * Followed by zero or more space characters | ||
* * Followed by "=" | ||
* * Followed by zero or more space characters | ||
* * Followed by: | ||
* * Any character except space, ('), ("), "<", ">", "=", (`), or | ||
* * (") then any non-("), or | ||
* * (') then any non-(') | ||
*/ | ||
const tagEndRegex = new RegExp( | ||
`>|${SPACE_CHAR}(${NAME_CHAR}+)(${SPACE_CHAR}*=${SPACE_CHAR}*(?:${ATTR_VALUE_CHAR}|("|')|))`, | ||
'g' | ||
); | ||
const ENTIRE_MATCH = 0; | ||
const ATTRIBUTE_NAME = 1; | ||
const SPACES_AND_EQUALS = 2; | ||
const QUOTE_CHAR = 3; | ||
const singleQuoteAttrEndRegex = /'/g; | ||
const doubleQuoteAttrEndRegex = /"/g; | ||
/** | ||
* Do not remove this comment; it keeps typedoc from misplacing the module | ||
* docs. | ||
* Matches the raw text elements. | ||
* | ||
* Comments are not parsed within raw text elements, so we need to search their | ||
* text content for marker strings. | ||
*/ | ||
import {defaultTemplateProcessor} from './lib/default-template-processor.js'; | ||
import {SVGTemplateResult, TemplateResult} from './lib/template-result.js'; | ||
const rawTextElement = /^(?:script|style|textarea)$/i; | ||
export {DefaultTemplateProcessor, defaultTemplateProcessor} from './lib/default-template-processor.js'; | ||
export {directive, DirectiveFn, isDirective} from './lib/directive.js'; | ||
// TODO(justinfagnani): remove line when we get NodePart moving methods | ||
export {removeNodes, reparentNodes} from './lib/dom.js'; | ||
export {noChange, nothing, Part} from './lib/part.js'; | ||
export {AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isIterable, isPrimitive, NodePart, PropertyCommitter, PropertyPart} from './lib/parts.js'; | ||
export {RenderOptions} from './lib/render-options.js'; | ||
export {parts, render} from './lib/render.js'; | ||
export {templateCaches, templateFactory} from './lib/template-factory.js'; | ||
export {TemplateInstance} from './lib/template-instance.js'; | ||
export {TemplateProcessor} from './lib/template-processor.js'; | ||
export {SVGTemplateResult, TemplateResult} from './lib/template-result.js'; | ||
export {createMarker, isTemplatePartActive, Template} from './lib/template.js'; | ||
/** TemplateResult types */ | ||
const HTML_RESULT = 1; | ||
const SVG_RESULT = 2; | ||
declare global { | ||
interface Window { | ||
litHtmlVersions: string[]; | ||
} | ||
} | ||
/** TemplatePart types */ | ||
// TODO (justinfagnani): since these are exported, consider shorter names, | ||
// like just `ATTRIBUTE`. | ||
export const ATTRIBUTE_PART = 1; | ||
export const NODE_PART = 2; | ||
export const PROPERTY_PART = 3; | ||
export const BOOLEAN_ATTRIBUTE_PART = 4; | ||
export const EVENT_PART = 5; | ||
const ELEMENT_PART = 6; | ||
const COMMENT_PART = 7; | ||
// IMPORTANT: do not change the property name or the assignment expression. | ||
// This line will be used in regexes to search for lit-html usage. | ||
// TODO(justinfagnani): inject version number at build time | ||
if (typeof window !== 'undefined') { | ||
(window['litHtmlVersions'] || (window['litHtmlVersions'] = [])).push('1.3.0'); | ||
} | ||
type ResultType = typeof HTML_RESULT | typeof SVG_RESULT; | ||
/** | ||
* The return type of the template tag functions. | ||
*/ | ||
export type TemplateResult = { | ||
_$litType$: ResultType; | ||
// TODO (justinfagnani): consider shorter names, like `s` and `v`. This is a | ||
// semi-public API though. We can't just let Terser rename them for us, | ||
// because we need TemplateResults to work between compatible versions of | ||
// lit-html. | ||
strings: TemplateStringsArray; | ||
values: unknown[]; | ||
}; | ||
/** | ||
* Generates a template literal tag function that returns a TemplateResult with | ||
* the given result type. | ||
*/ | ||
const tag = (_$litType$: ResultType) => ( | ||
strings: TemplateStringsArray, | ||
...values: unknown[] | ||
): TemplateResult => ({ | ||
_$litType$, | ||
strings, | ||
values, | ||
}); | ||
/** | ||
* Interprets a template literal as an HTML template that can efficiently | ||
* render to and update a container. | ||
*/ | ||
export const html = (strings: TemplateStringsArray, ...values: unknown[]) => | ||
new TemplateResult(strings, values, 'html', defaultTemplateProcessor); | ||
export const html = tag(HTML_RESULT); | ||
@@ -74,3 +183,895 @@ /** | ||
*/ | ||
export const svg = (strings: TemplateStringsArray, ...values: unknown[]) => | ||
new SVGTemplateResult(strings, values, 'svg', defaultTemplateProcessor); | ||
export const svg = tag(SVG_RESULT); | ||
/** | ||
* A sentinel value that signals that a value was handled by a directive and | ||
* should not be written to the DOM. | ||
*/ | ||
export const noChange = {}; | ||
/** | ||
* A sentinel value that signals a NodePart to fully clear its content. | ||
*/ | ||
export const nothing = {}; | ||
/** | ||
* The cache of prepared templates, keyed by the tagged TemplateStringsArray | ||
* and _not_ accounting for the specific template tag used. This means that | ||
* template tags cannot be dynamic - the must statically be one of html, svg, | ||
* or attr. This restriction simplifies the cache lookup, which is on the hot | ||
* path for rendering. | ||
*/ | ||
const templateCache = new Map<TemplateStringsArray, Template>(); | ||
export type NodePartInfo = { | ||
readonly type: typeof NODE_PART; | ||
}; | ||
export type AttributePartInfo = { | ||
readonly type: | ||
| typeof ATTRIBUTE_PART | ||
| typeof PROPERTY_PART | ||
| typeof BOOLEAN_ATTRIBUTE_PART | ||
| typeof EVENT_PART; | ||
strings?: ReadonlyArray<string>; | ||
name: string; | ||
tagName: string; | ||
}; | ||
/** | ||
* Information about the part a directive is bound to. | ||
* | ||
* This is useful for checking that a directive is attached to a valid part, | ||
* such as with directive that can only be used on attribute bindings. | ||
*/ | ||
export type PartInfo = NodePartInfo | AttributePartInfo; | ||
export type DirectiveClass = {new (part: PartInfo): Directive}; | ||
/** | ||
* This utility type extracts the signature of a directive class's render() | ||
* method so we can use it for the type of the generated directive function. | ||
*/ | ||
export type DirectiveParameters<C extends DirectiveClass> = Parameters< | ||
InstanceType<C>['render'] | ||
>; | ||
/** | ||
* A generated directive function doesn't evaluate the directive, but just | ||
* returns a DirectiveResult object that captures the arguments. | ||
*/ | ||
type DirectiveResult<C extends DirectiveClass = DirectiveClass> = { | ||
_$litDirective$: C; | ||
values: DirectiveParameters<C>; | ||
}; | ||
/** | ||
* Creates a user-facing directive function from a Directive class. This | ||
* function has the same parameters as the directive's render() method. | ||
* | ||
* WARNING: The directive and part API changes are in progress and subject to | ||
* change in future pre-releases. | ||
*/ | ||
export const directive = <C extends DirectiveClass>(c: C) => ( | ||
...values: DirectiveParameters<C> | ||
): DirectiveResult<C> => ({ | ||
_$litDirective$: c, | ||
values, | ||
}); | ||
export interface RenderOptions { | ||
/** | ||
* An object to use as the `this` value for event listeners. It's often | ||
* useful to set this to the host component rendering a template. | ||
*/ | ||
readonly eventContext?: EventTarget; | ||
/** | ||
* A DOM node before which to render content in the container. | ||
*/ | ||
readonly renderBefore?: ChildNode | null; | ||
} | ||
/** | ||
* Renders a value, usually a lit-html TemplateResult, to the container. | ||
* @param value | ||
* @param container | ||
* @param options | ||
*/ | ||
export const render = ( | ||
value: unknown, | ||
container: HTMLElement | DocumentFragment, | ||
options?: RenderOptions | ||
) => { | ||
const partOwnerNode = options?.renderBefore ?? container; | ||
let part: NodePart = (partOwnerNode as any).$lit$; | ||
if (part === undefined) { | ||
const endNode = options?.renderBefore ?? null; | ||
(partOwnerNode as any).$lit$ = part = new NodePart( | ||
container.insertBefore(createMarker(), endNode), | ||
endNode, | ||
options | ||
); | ||
} | ||
part._setValue(value); | ||
}; | ||
const walker = d.createTreeWalker(d); | ||
// | ||
// Classes only below here, const variable declarations only above here... | ||
// | ||
// Keeping variable declarations and classes together improves minification. | ||
// Interfaces and type aliases can be interleaved freely. | ||
// | ||
/** | ||
* Base class for creating custom directives. Users should extend this class, | ||
* implement `render` and/or `update`, and then pass their subclass to | ||
* `directive`. | ||
* | ||
* WARNING: The directive and part API changes are in progress and subject to | ||
* change in future pre-releases. | ||
*/ | ||
export abstract class Directive { | ||
abstract render(...props: Array<unknown>): unknown; | ||
update(_part: Part, props: Array<unknown>): unknown { | ||
return this.render(...props); | ||
} | ||
} | ||
class Template { | ||
private __strings: TemplateStringsArray; | ||
__element: HTMLTemplateElement; | ||
__parts: Array<TemplatePart> = []; | ||
constructor({strings, _$litType$: type}: TemplateResult) { | ||
walker.currentNode = (this.__element = d.createElement('template')).content; | ||
// Insert makers into the template HTML to represent the position of | ||
// bindings. The following code scans the template strings to determine the | ||
// syntactic position of the bindings. They can be in text position, where | ||
// we insert an HTML comment, attribute value position, where we insert a | ||
// sentinel string and re-write the attribute name, or inside a tag where | ||
// we insert the sentinel string. | ||
const l = (this.__strings = strings).length - 1; | ||
const attrNames: Array<string> = []; | ||
let html = type === SVG_RESULT ? '<svg>' : ''; | ||
let node: Node | null; | ||
let nodeIndex = 0; | ||
let bindingIndex = 0; | ||
let attrNameIndex = 0; | ||
// When we're inside a raw text tag (not it's text content), the regex | ||
// will still be tagRegex so we can find attributes, but will switch to | ||
// this regex when the tag ends. | ||
let rawTextEndRegex: RegExp | undefined; | ||
// The current parsing state, represented as a reference to one of the | ||
// regexes | ||
let regex = textEndRegex; | ||
for (let i = 0; i < l; i++) { | ||
const s = strings[i]; | ||
// The index of the end of the last attribute name. When this is | ||
// positive at end of a string, it means we're in an attribute value | ||
// position and need to rewrite the attribute name. | ||
let attrNameEndIndex = -1; | ||
let attrName: string | undefined; | ||
let lastIndex = 0; | ||
let match: RegExpExecArray | null; | ||
// The conditions in this loop handle the current parse state, and the | ||
// assignments to the `regex` variable are the state transitions. | ||
while (lastIndex < s.length) { | ||
// Make sure we start searching from where we previously left off | ||
regex.lastIndex = lastIndex; | ||
match = regex.exec(s); | ||
if (match === null) { | ||
// If the current regex doesn't match we've come to a binding inside | ||
// that state and must break and insert a marker | ||
if (regex === tagEndRegex) { | ||
// When tagEndRegex doesn't match we must have a binding in | ||
// attribute-name position, since tagEndRegex does match static | ||
// attribute names and end-of-tag. We need to clear | ||
// attrNameEndIndex which may have been set by a previous | ||
// tagEndRegex match. | ||
attrNameEndIndex = -1; | ||
} | ||
break; | ||
} | ||
lastIndex = regex.lastIndex; | ||
if (regex === textEndRegex) { | ||
if (match[COMMENT_START] === '!--') { | ||
regex = commentEndRegex; | ||
} else if (match[COMMENT_START] !== undefined) { | ||
// We started a weird comment, like </{ | ||
regex = comment2EndRegex; | ||
} else if (match[TAG_NAME] !== undefined) { | ||
if (rawTextElement.test(match[TAG_NAME])) { | ||
// Record if we encounter a raw-text element. We'll switch to | ||
// this regex at the end of the tag | ||
rawTextEndRegex = new RegExp(`<\/${match[TAG_NAME]}`, 'g'); | ||
} | ||
regex = tagEndRegex; | ||
} else if (match[DYNAMIC_TAG_NAME] !== undefined) { | ||
// dynamic tag name | ||
regex = tagEndRegex; | ||
} | ||
} else if (regex === tagEndRegex) { | ||
if (match[ENTIRE_MATCH] === '>') { | ||
// End of a tag. If we had started a raw-text element, use that | ||
// regex | ||
regex = rawTextEndRegex ?? textEndRegex; | ||
// We may be ending an unquoted attribute value, so make sure we | ||
// clear any pending attrNameEndIndex | ||
attrNameEndIndex = -1; | ||
} else { | ||
attrNameEndIndex = | ||
regex.lastIndex - match[SPACES_AND_EQUALS].length; | ||
attrName = match[ATTRIBUTE_NAME]; | ||
regex = | ||
match[QUOTE_CHAR] === undefined | ||
? tagEndRegex | ||
: match[QUOTE_CHAR] === '"' | ||
? doubleQuoteAttrEndRegex | ||
: singleQuoteAttrEndRegex; | ||
} | ||
} else if ( | ||
regex === doubleQuoteAttrEndRegex || | ||
regex === singleQuoteAttrEndRegex | ||
) { | ||
regex = tagEndRegex; | ||
} else if (regex === commentEndRegex || regex === comment2EndRegex) { | ||
regex = textEndRegex; | ||
} else { | ||
// Not one of the five state regexes, so it must be the dynamically | ||
// created raw text regex and we're at the close of that element. | ||
regex = tagEndRegex; | ||
rawTextEndRegex = undefined; | ||
} | ||
} | ||
if (DEV_MODE) { | ||
// If we have a attrNameEndIndex, which indicates that we should | ||
// rewrite the attribute name, assert that we're in a valid attribute | ||
// position - either in a tag, or a quoted attribute value. | ||
console.assert( | ||
attrNameEndIndex === -1 || | ||
regex === tagEndRegex || | ||
regex === singleQuoteAttrEndRegex || | ||
regex === doubleQuoteAttrEndRegex, | ||
'unexpected parse state B' | ||
); | ||
} | ||
// If we're in text position, and not in a raw text element | ||
// (regex === textEndRegex), we insert a comment marker. Otherwise, we | ||
// insert a plain maker. If we have a attrNameEndIndex, it means we need | ||
// to rewrite the attribute name to add a bound attribute suffix. | ||
html += | ||
regex === textEndRegex | ||
? s + nodeMarker | ||
: (attrNameEndIndex !== -1 | ||
? (attrNames.push(attrName!), | ||
s.slice(0, attrNameEndIndex) + | ||
boundAttributeSuffix + | ||
s.slice(attrNameEndIndex)) | ||
: s) + marker; | ||
} | ||
// TODO (justinfagnani): if regex is not textRegex log a warning for a | ||
// malformed template in dev mode. | ||
// Note, we don't add '</svg>' for SVG result types because the parser | ||
// will close the <svg> tag for us. | ||
this.__element.innerHTML = html + this.__strings[l]; | ||
if (type === SVG_RESULT) { | ||
const content = this.__element.content; | ||
const svgElement = content.firstChild!; | ||
svgElement.remove(); | ||
content.append(...svgElement.childNodes); | ||
} | ||
// Walk the template to find binding markers and create TemplateParts | ||
while ((node = walker.nextNode()) !== null && bindingIndex < l) { | ||
if (node.nodeType === 1) { | ||
// TODO (justinfagnani): for attempted dynamic tag names, we don't | ||
// increment the bindingIndex, and it'll be off by 1 in the element | ||
// and off by two after it. | ||
if ((node as Element).hasAttributes()) { | ||
const {attributes} = node as Element; | ||
for (let i = 0; i < attributes.length; i++) { | ||
const {name, value} = attributes[i]; | ||
if (name.endsWith(boundAttributeSuffix)) { | ||
i--; | ||
(node as Element).removeAttribute(name); | ||
const statics = value.split(marker); | ||
const m = /([.?@])?(.*)/.exec(attrNames[attrNameIndex++])!; | ||
this.__parts.push({ | ||
__type: ATTRIBUTE_PART, | ||
__index: nodeIndex, | ||
__name: m[2], | ||
__strings: statics, | ||
__constructor: | ||
m[1] === '.' | ||
? PropertyPart | ||
: m[1] === '?' | ||
? BooleanAttributePart | ||
: m[1] === '@' | ||
? EventPart | ||
: AttributePart, | ||
}); | ||
bindingIndex += statics.length - 1; | ||
} else if (name === marker) { | ||
(node as Element).removeAttribute(name); | ||
i--; | ||
this.__parts.push({ | ||
__type: ELEMENT_PART, | ||
__index: nodeIndex, | ||
}); | ||
} | ||
} | ||
} | ||
// TODO (justinfagnani): benchmark the regex against testing for each | ||
// of the 3 raw text element names. | ||
if (rawTextElement.test((node as Element).tagName)) { | ||
// For raw text elements we need to split the text content on | ||
// markers, create a Text node for each segment, and create | ||
// a TemplatePart for each marker. | ||
const strings = (node as Element).textContent!.split(marker); | ||
const lastIndex = strings.length - 1; | ||
if (lastIndex > 0) { | ||
(node as Element).textContent = ''; | ||
// Generate a new text node for each literal section | ||
// These nodes are also used as the markers for node parts | ||
// We can't use empty text nodes as markers because they're | ||
// normalized in some browsers (TODO: check) | ||
for (let i = 0; i < lastIndex; i++) { | ||
(node as Element).append(strings[i] || createMarker()); | ||
this.__parts.push({__type: NODE_PART, __index: ++nodeIndex}); | ||
bindingIndex++; | ||
} | ||
(node as Element).append(strings[lastIndex] || createMarker()); | ||
} | ||
} | ||
} else if (node.nodeType === 8) { | ||
const data = (node as Comment).data; | ||
if (data === markerMatch) { | ||
bindingIndex++; | ||
this.__parts.push({__type: NODE_PART, __index: nodeIndex}); | ||
} else { | ||
let i = -1; | ||
while ((i = (node as Comment).data.indexOf(marker, i + 1)) !== -1) { | ||
// Comment node has a binding marker inside, make an inactive part | ||
// The binding won't work, but subsequent bindings will | ||
// TODO (justinfagnani): consider whether it's even worth it to | ||
// make bindings in comments work | ||
this.__parts.push({__type: COMMENT_PART, __index: nodeIndex}); | ||
bindingIndex++; | ||
// Move to the end of the match | ||
i += marker.length - 1; | ||
} | ||
} | ||
} | ||
nodeIndex++; | ||
} | ||
} | ||
} | ||
/** | ||
* An updateable instance of a Template. Holds references to the Parts used to | ||
* update the template instance. | ||
*/ | ||
class TemplateInstance { | ||
__template: Template; | ||
__parts: Array<Part | undefined> = []; | ||
constructor(template: Template) { | ||
this.__template = template; | ||
} | ||
// This method is separate from the constructor because we need to return a | ||
// DocumentFragment and we don't want to hold onto it with an instance field. | ||
__clone(options: RenderOptions | undefined) { | ||
const { | ||
__element: {content}, | ||
__parts: parts, | ||
} = this.__template; | ||
const fragment = d.importNode(content, true); | ||
walker.currentNode = fragment; | ||
let node = walker.nextNode(); | ||
let nodeIndex = 0; | ||
let partIndex = 0; | ||
let templatePart = parts[0]; | ||
while (templatePart !== undefined && node !== null) { | ||
if (nodeIndex === templatePart.__index) { | ||
let part: Part | undefined; | ||
if (templatePart.__type === NODE_PART) { | ||
part = new NodePart(node as HTMLElement, node.nextSibling, options); | ||
} else if (templatePart.__type === ATTRIBUTE_PART) { | ||
part = new templatePart.__constructor( | ||
node as HTMLElement, | ||
templatePart.__name, | ||
templatePart.__strings, | ||
options | ||
); | ||
} | ||
this.__parts.push(part); | ||
templatePart = parts[++partIndex]; | ||
} | ||
if (templatePart !== undefined && nodeIndex !== templatePart.__index) { | ||
node = walker.nextNode(); | ||
nodeIndex++; | ||
} | ||
} | ||
return fragment; | ||
} | ||
__update(values: Array<unknown>) { | ||
let i = 0; | ||
for (const part of this.__parts) { | ||
if (part === undefined) { | ||
i++; | ||
continue; | ||
} | ||
if ((part as AttributePart).strings !== undefined) { | ||
(part as AttributePart)._setValue(values, i); | ||
i += (part as AttributePart).strings!.length - 1; | ||
} else { | ||
(part as NodePart)._setValue(values[i++]); | ||
} | ||
} | ||
} | ||
} | ||
/* | ||
* Parts | ||
*/ | ||
type AttributeTemplatePart = { | ||
readonly __type: typeof ATTRIBUTE_PART; | ||
readonly __index: number; | ||
readonly __name: string; | ||
readonly __constructor: typeof AttributePart; | ||
readonly __strings: ReadonlyArray<string>; | ||
}; | ||
type NodeTemplatePart = { | ||
readonly __type: typeof NODE_PART; | ||
readonly __index: number; | ||
}; | ||
type ElementTemplatePart = { | ||
readonly __type: typeof ELEMENT_PART; | ||
readonly __index: number; | ||
}; | ||
type CommentTemplatePart = { | ||
readonly __type: typeof COMMENT_PART; | ||
readonly __index: number; | ||
}; | ||
/** | ||
* A TemplatePart represents a dynamic part in a template, before the template | ||
* is instantiated. When a template is instantiated Parts are created from | ||
* TemplateParts. | ||
*/ | ||
type TemplatePart = | ||
| NodeTemplatePart | ||
| AttributeTemplatePart | ||
| ElementTemplatePart | ||
| CommentTemplatePart; | ||
export type Part = | ||
| NodePart | ||
| AttributePart | ||
| PropertyPart | ||
| BooleanAttributePart; | ||
export class NodePart { | ||
readonly type = NODE_PART; | ||
_value: unknown; | ||
protected __directive?: Directive; | ||
constructor( | ||
public _startNode: ChildNode, | ||
public _endNode: ChildNode | null, | ||
public options: RenderOptions | undefined | ||
) {} | ||
_setValue(value: unknown): void { | ||
// TODO (justinfagnani): when setting a non-directive over a directive, | ||
// we don't yet clear this.__directive. | ||
// See https://github.com/Polymer/lit-html/issues/1286 | ||
if (isPrimitive(value)) { | ||
if (value !== this._value) { | ||
this.__commitText(value); | ||
} | ||
} else if ((value as TemplateResult)._$litType$ !== undefined) { | ||
this.__commitTemplateResult(value as TemplateResult); | ||
} else if ((value as DirectiveResult)._$litDirective$ !== undefined) { | ||
this.__commitDirective(value as DirectiveResult); | ||
} else if ((value as Node).nodeType !== undefined) { | ||
this._commitNode(value as Node); | ||
} else if (isIterable(value)) { | ||
this.__commitIterable(value); | ||
} else if (value === nothing) { | ||
this._value = nothing; | ||
this.__clear(); | ||
} else if (value !== noChange) { | ||
// Fallback, will render the string representation | ||
this.__commitText(value); | ||
} | ||
} | ||
private __insert<T extends Node>(node: T, ref = this._endNode) { | ||
return this._startNode.parentNode!.insertBefore(node, ref); | ||
} | ||
private __commitDirective(value: DirectiveResult) { | ||
const directive = value._$litDirective$; | ||
if (this.__directive?.constructor !== directive) { | ||
this.__clear(); | ||
this.__directive = new directive(this as NodePartInfo); | ||
} | ||
// TODO (justinfagnani): To support nested directives, we'd need to | ||
// resolve the directive result's values. We may want to offer another | ||
// way of composing directives. | ||
this._setValue(this.__directive.update(this, value.values)); | ||
} | ||
private _commitNode(value: Node): void { | ||
if (this._value !== value) { | ||
this.__clear(); | ||
this._value = this.__insert(value); | ||
} | ||
} | ||
private __commitText(value: unknown): void { | ||
const node = this._startNode.nextSibling; | ||
// Make sure undefined and null render as an empty string | ||
// TODO: use `nothing` to clear the node? | ||
value ??= ''; | ||
// TODO(justinfagnani): Can we just check if this._value is primitive? | ||
if ( | ||
node !== null && | ||
node.nodeType === 3 /* Node.TEXT_NODE */ && | ||
(this._endNode === null | ||
? node.nextSibling === null | ||
: node === this._endNode.previousSibling) | ||
) { | ||
// If we only have a single text node between the markers, we can just | ||
// set its value, rather than replacing it. | ||
(node as Text).data = value as string; | ||
} else { | ||
this._commitNode(new Text(value as string)); | ||
} | ||
this._value = value; | ||
} | ||
private __commitTemplateResult(result: TemplateResult): void { | ||
const {strings, values} = result; | ||
let template = templateCache.get(strings); | ||
if (template === undefined) { | ||
templateCache.set(strings, (template = new Template(result))); | ||
} | ||
if ( | ||
this._value != null && | ||
(this._value as TemplateInstance).__template === template | ||
) { | ||
(this._value as TemplateInstance).__update(values); | ||
} else { | ||
const instance = new TemplateInstance(template!); | ||
const fragment = instance.__clone(this.options); | ||
instance.__update(values); | ||
this._commitNode(fragment); | ||
this._value = instance; | ||
} | ||
} | ||
private __commitIterable(value: Iterable<unknown>): void { | ||
// For an Iterable, we create a new InstancePart per item, then set its | ||
// value to the item. This is a little bit of overhead for every item in | ||
// an Iterable, but it lets us recurse easily and efficiently update Arrays | ||
// of TemplateResults that will be commonly returned from expressions like: | ||
// array.map((i) => html`${i}`), by reusing existing TemplateInstances. | ||
// If value is an array, then the previous render was of an | ||
// iterable and value will contain the NodeParts from the previous | ||
// render. If value is not an array, clear this part and make a new | ||
// array for NodeParts. | ||
if (!isArray(this._value)) { | ||
this._value = []; | ||
this.__clear(); | ||
} | ||
// Lets us keep track of how many items we stamped so we can clear leftover | ||
// items from a previous render | ||
const itemParts = this._value as NodePart[]; | ||
let partIndex = 0; | ||
let itemPart: NodePart | undefined; | ||
for (const item of value) { | ||
if (partIndex === itemParts.length) { | ||
// If no existing part, create a new one | ||
// TODO (justinfagnani): test perf impact of always creating two parts | ||
// instead of sharing parts between nodes | ||
// https://github.com/Polymer/lit-html/issues/1266 | ||
itemParts.push( | ||
(itemPart = new NodePart( | ||
this.__insert(createMarker()), | ||
this.__insert(createMarker()), | ||
this.options | ||
)) | ||
); | ||
} else { | ||
// Reuse an existing part | ||
itemPart = itemParts[partIndex]; | ||
} | ||
itemPart._setValue(item); | ||
partIndex++; | ||
} | ||
if (partIndex < itemParts.length) { | ||
// Truncate the parts array so _value reflects the current state | ||
itemParts.length = partIndex; | ||
// itemParts always have end nodes | ||
this.__clear(itemPart?._endNode!.nextSibling); | ||
} | ||
} | ||
__clear(start: ChildNode | null = this._startNode.nextSibling) { | ||
while (start && start !== this._endNode) { | ||
const n = start!.nextSibling; | ||
start!.remove(); | ||
start = n; | ||
} | ||
} | ||
} | ||
export class AttributePart { | ||
readonly type = ATTRIBUTE_PART as | ||
| typeof ATTRIBUTE_PART | ||
| typeof PROPERTY_PART | ||
| typeof BOOLEAN_ATTRIBUTE_PART | ||
| typeof EVENT_PART; | ||
readonly element: HTMLElement; | ||
readonly name: string; | ||
/** | ||
* If this attribute part represents an interpolation, this contains the | ||
* static strings of the interpolation. For single-value, complete bindings, | ||
* this is undefined. | ||
*/ | ||
readonly strings?: ReadonlyArray<string>; | ||
_value: unknown | Array<unknown> = nothing; | ||
private __directives?: Array<Directive>; | ||
get tagName() { | ||
return this.element.tagName; | ||
} | ||
constructor( | ||
element: HTMLElement, | ||
name: string, | ||
strings: ReadonlyArray<string>, | ||
_options?: RenderOptions | ||
) { | ||
this.element = element; | ||
this.name = name; | ||
if (strings.length > 2 || strings[0] !== '' || strings[1] !== '') { | ||
this._value = new Array(strings.length - 1).fill(nothing); | ||
this.strings = strings; | ||
} else { | ||
this._value = nothing; | ||
} | ||
} | ||
/** | ||
* Normalizes a user-provided value before writing it to the DOM. In the | ||
* near future this will include invoking a directive if the value is | ||
* a DirectiveResult. | ||
* | ||
* @param value the raw input value to normalize | ||
* @param _i the index in the values array this value was read from | ||
*/ | ||
__resolveValue(value: unknown, i: number) { | ||
const directiveCtor = (value as DirectiveResult)?._$litDirective$; | ||
if (directiveCtor !== undefined) { | ||
// TODO (justinfagnani): Initialize array to the correct value, | ||
// or check length. | ||
let directive: Directive = (this.__directives ??= [])[i]; | ||
if (directive?.constructor !== directiveCtor) { | ||
directive = this.__directives[i] = new directiveCtor( | ||
this as AttributePartInfo | ||
); | ||
} | ||
// TODO (justinfagnani): To support nested directives, we'd need to | ||
// resolve the directive result's values. We may want to offer another | ||
// way of composing directives. | ||
value = directive.update(this, (value as DirectiveResult).values); | ||
} | ||
return value ?? ''; | ||
} | ||
/** | ||
* Sets the value of this part. | ||
* | ||
* If this part is single-valued, `this.__strings` will be undefined, and the | ||
* method will be called with a single value argument. If this part is | ||
* multi-value, `this.__strings` will be defined, and the method is called | ||
* with the value array of the part's owning TemplateInstance, and an offset | ||
* into the value array from which the values should be read. | ||
* | ||
* This method is overloaded this way to eliminate short-lived array slices | ||
* of the template instance values, and allow a fast-path for single-valued | ||
* parts. | ||
* | ||
* @param value The part value, or an array of values for multi-valued parts | ||
* @param from the index to start reading values from. `undefined` for | ||
* single-valued parts | ||
*/ | ||
_setValue(value: unknown): void; | ||
_setValue(value: Array<unknown>, from: number): void; | ||
_setValue(value: unknown | Array<unknown>, from?: number) { | ||
const strings = this.strings; | ||
if (strings === undefined) { | ||
// Single-value binding case | ||
const v = this.__resolveValue(value, 0); | ||
// Only dirty-check primitives and `nothing`: | ||
// `(isPrimitive(v) || v === nothing)` limits the clause to primitives and | ||
// `nothing`. `v === this._value` is the dirty-check. | ||
if ( | ||
!((isPrimitive(v) || v === nothing) && v === this._value) && | ||
v !== noChange | ||
) { | ||
this.__commitValue((this._value = v)); | ||
} | ||
} else { | ||
// Interpolation case | ||
let attributeValue = strings[0]; | ||
// Whether any of the values has changed, for dirty-checking | ||
let change = false; | ||
// Whether any of the values is the `nothing` sentinel. If any are, we | ||
// remove the entire attribute. | ||
let remove = false; | ||
let i, v; | ||
for (i = 0; i < strings.length - 1; i++) { | ||
v = this.__resolveValue((value as Array<unknown>)[from! + i], i); | ||
if (v === noChange) { | ||
// If the user-provided value is `noChange`, use the previous value | ||
v = (this._value as Array<unknown>)[i]; | ||
} else { | ||
remove = remove || v === nothing; | ||
change = | ||
change || | ||
!( | ||
(isPrimitive(v) || v === nothing) && | ||
v === (this._value as Array<unknown>)[i] | ||
); | ||
(this._value as Array<unknown>)[i] = v; | ||
} | ||
attributeValue += | ||
(typeof v === 'string' ? v : String(v)) + strings[i + 1]; | ||
} | ||
if (change) { | ||
this.__commitValue(remove ? nothing : attributeValue); | ||
} | ||
} | ||
} | ||
/** | ||
* Writes the value to the DOM. An override point for PropertyPart and | ||
* BooleanAttributePart. | ||
*/ | ||
__commitValue(value: unknown) { | ||
if (value === nothing) { | ||
this.element.removeAttribute(this.name); | ||
} else { | ||
this.element.setAttribute(this.name, value as string); | ||
} | ||
} | ||
} | ||
export class PropertyPart extends AttributePart { | ||
readonly type = PROPERTY_PART; | ||
__commitValue(value: unknown) { | ||
(this.element as any)[this.name] = value === nothing ? undefined : value; | ||
} | ||
} | ||
export class BooleanAttributePart extends AttributePart { | ||
readonly type = BOOLEAN_ATTRIBUTE_PART; | ||
__commitValue(value: unknown) { | ||
if (value && value !== nothing) { | ||
this.element.setAttribute(this.name, ''); | ||
} else { | ||
this.element.removeAttribute(this.name); | ||
} | ||
} | ||
} | ||
type EventListenerWithOptions = EventListenerOrEventListenerObject & | ||
Partial<AddEventListenerOptions>; | ||
/** | ||
* An AttributePart that manages an event listener via add/removeEventListener. | ||
* | ||
* This part works by adding itself as the event listener on an element, then | ||
* delegating to the value passed to it. This reduces the number of calls to | ||
* add/removeEventListener if the listener changes frequently, such as when an | ||
* inline function is used as a listener. | ||
* | ||
* Because event options are passed when adding listeners, we must take case | ||
* to add and remove the part as a listener when the event options change. | ||
*/ | ||
export class EventPart extends AttributePart { | ||
readonly type = EVENT_PART; | ||
__eventContext?: unknown; | ||
constructor(...args: ConstructorParameters<typeof AttributePart>) { | ||
super(...args); | ||
this.__eventContext = args[3]?.eventContext; | ||
} | ||
_setValue(newListener: unknown) { | ||
newListener ??= nothing; | ||
const oldListener = this._value; | ||
// If the new value is nothing or any options change we have to remove the | ||
// part as a listener. | ||
const shouldRemoveListener = | ||
(newListener === nothing && oldListener !== nothing) || | ||
(newListener as EventListenerWithOptions).capture !== | ||
(oldListener as EventListenerWithOptions).capture || | ||
(newListener as EventListenerWithOptions).once !== | ||
(oldListener as EventListenerWithOptions).once || | ||
(newListener as EventListenerWithOptions).passive !== | ||
(oldListener as EventListenerWithOptions).passive; | ||
// If the new value is not nothing and we removed the listener, we have | ||
// to add the part as a listener. | ||
const shouldAddListener = | ||
newListener !== nothing && | ||
(oldListener === nothing || shouldRemoveListener); | ||
if (shouldRemoveListener) { | ||
this.element.removeEventListener( | ||
this.name, | ||
this, | ||
oldListener as EventListenerWithOptions | ||
); | ||
} | ||
if (shouldAddListener) { | ||
// Beware: IE11 and Chrome 41 don't like using the listener as the | ||
// options object. Figure out how to deal w/ this in IE11 - maybe | ||
// patch addEventListener? | ||
this.element.addEventListener( | ||
this.name, | ||
this, | ||
newListener as EventListenerWithOptions | ||
); | ||
} | ||
this._value = newListener; | ||
} | ||
handleEvent(event: Event) { | ||
if (typeof this._value === 'function') { | ||
// TODO (justinfagnani): do we need to default to this.__element? | ||
// It'll always be the same as `e.currentTarget`. | ||
this._value.call(this.__eventContext ?? this.element, event); | ||
} else { | ||
(this._value as EventListenerObject).handleEvent(event); | ||
} | ||
} | ||
} | ||
// IMPORTANT: do not change the property name or the assignment expression. | ||
// This line will be used in regexes to search for lit-html usage. | ||
// TODO(justinfagnani): inject version number at build time | ||
((globalThis as any)['litHtmlVersions'] ??= []).push('1.3.0'); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
14
171
485553
97
5407
1