Comparing version 0.7.1 to 0.8.0-pre.1
@@ -13,4 +13,7 @@ # Change Log | ||
<!-- ## Unreleased --> | ||
## Unreleased | ||
* Added the `asyncAppend` and `asyncReplace` directives to handle async iterable values | ||
in expressions. | ||
## [0.7.0] - 2017-10-06 | ||
@@ -17,0 +20,0 @@ |
@@ -55,3 +55,2 @@ /** | ||
setValue(value: any): void; | ||
handleEvent(event: Event): void; | ||
} |
@@ -49,7 +49,7 @@ /** | ||
if (templatePart.rawName.startsWith('on-')) { | ||
const eventName = templatePart.rawName.substring(3); | ||
const eventName = templatePart.rawName.slice(3); | ||
return new EventPart(instance, node, eventName); | ||
} | ||
if (templatePart.name.endsWith('$')) { | ||
const name = templatePart.name.substring(0, templatePart.name.length - 1); | ||
const name = templatePart.name.slice(0, -1); | ||
return new AttributePart(instance, node, name, templatePart.strings); | ||
@@ -65,3 +65,3 @@ } | ||
let value; | ||
if (s.length === 2 && s[0] === '' && s[s.length - 1] === '') { | ||
if (s.length === 2 && s[0] === '' && s[1] === '') { | ||
// An expression that occupies the whole attribute value will leave | ||
@@ -73,9 +73,3 @@ // leading and trailing empty strings. | ||
// Interpolation, so interpolate | ||
value = ''; | ||
for (let i = 0; i < s.length; i++) { | ||
value += s[i]; | ||
if (i < s.length - 1) { | ||
value += getValue(this, values[startIndex + i]); | ||
} | ||
} | ||
value = this._interpolate(values, startIndex); | ||
} | ||
@@ -93,19 +87,12 @@ this.element[this.name] = value; | ||
const listener = getValue(this, value); | ||
if (listener === this._listener) { | ||
const previous = this._listener; | ||
if (listener === previous) { | ||
return; | ||
} | ||
if (listener == null) { | ||
this.element.removeEventListener(this.eventName, this); | ||
} | ||
else if (this._listener == null) { | ||
this.element.addEventListener(this.eventName, this); | ||
} | ||
this._listener = listener; | ||
} | ||
handleEvent(event) { | ||
if (typeof this._listener === 'function') { | ||
this._listener.call(this.element, event); | ||
if (previous != null) { | ||
this.element.removeEventListener(this.eventName, previous); | ||
} | ||
else if (typeof this._listener.handleEvent === 'function') { | ||
this._listener.handleEvent(event); | ||
if (listener != null) { | ||
this.element.addEventListener(this.eventName, listener); | ||
} | ||
@@ -112,0 +99,0 @@ } |
@@ -15,3 +15,8 @@ /** | ||
import { directive, NodePart } from '../lit-html.js'; | ||
const stateCache = new WeakMap(); | ||
const keyMapCache = new WeakMap(); | ||
function cleanMap(part, key, map) { | ||
if (!part.startNode.parentNode) { | ||
map.delete(key); | ||
} | ||
} | ||
export function repeat(items, keyFnOrTemplate, template) { | ||
@@ -26,17 +31,10 @@ let keyFn; | ||
return directive((part) => { | ||
let state = stateCache.get(part); | ||
if (state === undefined) { | ||
state = { | ||
keyMap: keyFn && new Map(), | ||
parts: [], | ||
}; | ||
stateCache.set(part, state); | ||
let keyMap = keyMapCache.get(part); | ||
if (keyMap === undefined) { | ||
keyMap = new Map(); | ||
keyMapCache.set(part, keyMap); | ||
} | ||
const container = part.startNode.parentNode; | ||
const oldParts = state.parts; | ||
const endParts = new Map(oldParts.map((p) => [p.endNode, p])); | ||
const keyMap = state.keyMap; | ||
const itemParts = []; | ||
let index = 0; | ||
let currentMarker; | ||
let index = -1; | ||
let currentMarker = part.startNode.nextSibling; | ||
for (const item of items) { | ||
@@ -46,4 +44,5 @@ let result; | ||
try { | ||
result = template(item, index++); | ||
key = keyFn && keyFn(item); | ||
++index; | ||
result = template(item, index); | ||
key = keyFn ? keyFn(item) : index; | ||
} | ||
@@ -54,83 +53,41 @@ catch (e) { | ||
} | ||
// Try to reuse a part, either keyed or from the list of previous parts | ||
// if there's no keyMap | ||
let itemPart = keyMap === undefined ? oldParts[0] : keyMap.get(key); | ||
// Try to reuse a part | ||
let itemPart = keyMap.get(key); | ||
if (itemPart === undefined) { | ||
// New part, attach it | ||
if (currentMarker === undefined) { | ||
currentMarker = new Text(); | ||
container.insertBefore(currentMarker, part.startNode.nextSibling); | ||
} | ||
const endNode = new Text(); | ||
container.insertBefore(endNode, currentMarker.nextSibling); | ||
itemPart = new NodePart(part.instance, currentMarker, endNode); | ||
if (key !== undefined && keyMap !== undefined) { | ||
const marker = document.createTextNode(''); | ||
const endNode = document.createTextNode(''); | ||
container.insertBefore(marker, currentMarker); | ||
container.insertBefore(endNode, currentMarker); | ||
itemPart = new NodePart(part.instance, marker, endNode); | ||
if (key !== undefined) { | ||
keyMap.set(key, itemPart); | ||
} | ||
} | ||
else { | ||
// Existing part, maybe move it | ||
const range = document.createRange(); | ||
range.setStartBefore(itemPart.startNode); | ||
range.setEndBefore(itemPart.endNode); | ||
if (currentMarker === undefined) { | ||
// this should be the first part, make sure it's first | ||
if (part.startNode.nextSibling !== itemPart.startNode) { | ||
// move the whole part | ||
// get previous and next parts | ||
const previousPart = endParts.get(itemPart.startNode); | ||
if (previousPart) { | ||
previousPart.endNode = itemPart.endNode; | ||
endParts.set(previousPart.endNode, previousPart); | ||
} | ||
const contents = range.extractContents(); | ||
if (part.startNode.nextSibling === part.endNode) { | ||
// The container part was empty, so we need a new endPart | ||
itemPart.endNode = new Text(); | ||
container.insertBefore(itemPart.endNode, part.startNode.nextSibling); | ||
} | ||
else { | ||
// endNode should equal the startNode of the currently first part | ||
itemPart.endNode = part.startNode.nextSibling; | ||
} | ||
container.insertBefore(contents, part.startNode.nextSibling); | ||
} | ||
// else part is in the correct position already | ||
else if (currentMarker !== itemPart.startNode) { | ||
// Existing part in the wrong position | ||
const end = itemPart.endNode.nextSibling; | ||
for (let node = itemPart.startNode; node !== end;) { | ||
const n = node.nextSibling; | ||
container.insertBefore(node, currentMarker); | ||
node = n; | ||
} | ||
else if (currentMarker !== itemPart.startNode) { | ||
// move to correct position | ||
const previousPart = endParts.get(itemPart.startNode); | ||
if (previousPart) { | ||
previousPart.endNode = itemPart.endNode; | ||
endParts.set(previousPart.endNode, previousPart); | ||
} | ||
const contents = range.extractContents(); | ||
container.insertBefore(contents, currentMarker); | ||
} | ||
// remove part from oldParts list so it's not cleaned up | ||
oldParts.splice(oldParts.indexOf(itemPart), 1); | ||
} | ||
// else part is in the correct position already | ||
else { | ||
// else part is in the correct position already | ||
currentMarker = itemPart.endNode.nextSibling; | ||
} | ||
itemPart.setValue(result); | ||
itemParts.push(itemPart); | ||
currentMarker = itemPart.endNode; | ||
} | ||
// Cleanup | ||
if (oldParts.length > 0) { | ||
const clearStart = oldParts[0].startNode; | ||
const clearEnd = oldParts[oldParts.length - 1].endNode; | ||
const clearRange = document.createRange(); | ||
if (itemParts.length === 0) { | ||
clearRange.setStartBefore(clearStart); | ||
if (currentMarker !== part.endNode) { | ||
const end = part.endNode; | ||
for (let node = currentMarker; node !== end;) { | ||
const n = node.nextSibling; | ||
container.removeChild(node); | ||
node = n; | ||
} | ||
else { | ||
clearRange.setStartAfter(clearStart); | ||
} | ||
clearRange.setEndAfter(clearEnd); | ||
clearRange.deleteContents(); | ||
clearRange.detach(); // is this neccessary? | ||
keyMap.forEach(cleanMap); | ||
} | ||
state.parts = itemParts; | ||
}); | ||
} | ||
//# sourceMappingURL=repeat.js.map |
@@ -22,2 +22,2 @@ /** | ||
*/ | ||
export declare const unsafeHTML: (value: any) => (_part: NodePart) => DocumentFragment; | ||
export declare const unsafeHTML: (value: any) => (part: NodePart) => void; |
@@ -22,7 +22,7 @@ /** | ||
*/ | ||
export const unsafeHTML = (value) => directive((_part) => { | ||
export const unsafeHTML = (value) => directive((part) => { | ||
const tmp = document.createElement('template'); | ||
tmp.innerHTML = value; | ||
return document.importNode(tmp.content, true); | ||
part.setValue(document.importNode(tmp.content, true)); | ||
}); | ||
//# sourceMappingURL=unsafe-html.js.map |
@@ -18,2 +18,2 @@ /** | ||
*/ | ||
export declare const until: (promise: Promise<any>, defaultContent: any) => (part: NodePart) => Promise<any>; | ||
export declare const until: (promise: Promise<any>, defaultContent: any) => (part: NodePart) => void; |
@@ -20,4 +20,4 @@ /** | ||
part.setValue(defaultContent); | ||
return promise; | ||
part.setValue(promise); | ||
}); | ||
//# sourceMappingURL=until.js.map |
@@ -54,3 +54,2 @@ /** | ||
element: HTMLTemplateElement; | ||
svg: boolean; | ||
constructor(strings: TemplateStringsArray, svg?: boolean); | ||
@@ -62,2 +61,9 @@ /** | ||
} | ||
/** | ||
* Returns a value ready to be inserted into a Part from a user-provided value. | ||
* | ||
* If the user value is a directive, this invokes the directive with the given | ||
* part. If the value is null, it's converted to undefined to work better | ||
* with certain DOM APIs, like textContent. | ||
*/ | ||
export declare const getValue: (part: Part, value: any) => any; | ||
@@ -83,2 +89,3 @@ export declare type DirectiveFn = (part: Part) => any; | ||
constructor(instance: TemplateInstance, element: Element, name: string, strings: string[]); | ||
protected _interpolate(values: any[], startIndex: number): string; | ||
setValue(values: any[], startIndex: number): void; | ||
@@ -90,3 +97,3 @@ } | ||
endNode: Node; | ||
private _previousValue; | ||
_previousValue: any; | ||
constructor(instance: TemplateInstance, startNode: Node, endNode: Node); | ||
@@ -99,3 +106,3 @@ setValue(value: any): void; | ||
private _setIterable(value); | ||
protected _setPromise(value: Promise<any>): void; | ||
private _setPromise(value); | ||
clear(startNode?: Node): void; | ||
@@ -117,1 +124,13 @@ } | ||
} | ||
/** | ||
* Reparents nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), into another container (could be the same container), before | ||
* `beforeNode`. If `beforeNode` is null, it appends the nodes to the | ||
* container. | ||
*/ | ||
export declare const reparentNodes: (container: Node, start: Node | null, end?: Node | null, before?: Node | null) => void; | ||
/** | ||
* Removes nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), from `container`. | ||
*/ | ||
export declare const removeNodes: (container: Node, startNode: Node | null, endNode?: Node | null) => void; |
252
lit-html.js
@@ -77,6 +77,3 @@ /** | ||
instance.update(result.values); | ||
let child; | ||
while ((child = container.lastChild)) { | ||
container.removeChild(child); | ||
} | ||
removeNodes(container, container.firstChild); | ||
container.appendChild(fragment); | ||
@@ -88,15 +85,19 @@ } | ||
*/ | ||
const attributeMarker = `{{lit-${Math.random()}}}`; | ||
const marker = `{{lit-${String(Math.random()).slice(2)}}}`; | ||
const nodeMarker = `<!--${marker}-->`; | ||
const markerRegex = new RegExp(`${marker}|${nodeMarker}`); | ||
const nonWhitespace = /[^\s]/; | ||
const lastAttributeNameRegex = /([\w\-.$]+)=(?:[^"']*|"[^"]*|'[^']*)$/; | ||
/** | ||
* Regex to scan the string preceding an expression to see if we're in a text | ||
* context, and not an attribute context. | ||
* | ||
* This works by seeing if we have a `>` not followed by a `<`. If there is a | ||
* `<` closer to the end of the strings, then we're inside a tag. | ||
* Finds the closing index of the last closed HTML tag. | ||
* This has 3 possible return values: | ||
* - `-1`, meaning there is no tag in str. | ||
* - `string.length`, meaning the last opened tag is unclosed. | ||
* - Some positive number < str.length, meaning the index of the closing '>'. | ||
*/ | ||
const textRegex = />[^<]*$/; | ||
const hasTagsRegex = /[^<]*/; | ||
const textMarkerContent = '_-lit-html-_'; | ||
const textMarker = `<!--${textMarkerContent}-->`; | ||
const attrOrTextRegex = new RegExp(`${attributeMarker}|${textMarker}`); | ||
function findTagClose(str) { | ||
const close = str.lastIndexOf('>'); | ||
const open = str.indexOf('<', close + 1); | ||
return open > -1 ? str.length : close; | ||
} | ||
/** | ||
@@ -130,7 +131,12 @@ * A placeholder for a dynamic expression in an HTML template. | ||
this.parts = []; | ||
this.svg = svg; | ||
this.element = document.createElement('template'); | ||
this.element.innerHTML = this._getHtml(strings, svg); | ||
const element = this.element = document.createElement('template'); | ||
element.innerHTML = this._getHtml(strings, svg); | ||
const content = element.content; | ||
if (svg) { | ||
const svgElement = content.firstChild; | ||
content.removeChild(svgElement); | ||
reparentNodes(content, svgElement.firstChild); | ||
} | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
const walker = document.createTreeWalker(this.element.content, 133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
const walker = document.createTreeWalker(content, 133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
NodeFilter.SHOW_TEXT */, null, false); | ||
@@ -154,25 +160,31 @@ let index = -1; | ||
const attributes = node.attributes; | ||
// Per https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap, | ||
// attributes are not guaranteed to be returned in document order. In | ||
// particular, Edge/IE can return them out of order, so we cannot assume | ||
// a correspondance between part index and attribute index. | ||
let count = 0; | ||
for (let i = 0; i < attributes.length; i++) { | ||
const attribute = attributes.item(i); | ||
const attributeStrings = attribute.value.split(attrOrTextRegex); | ||
if (attributeStrings.length > 1) { | ||
// Get the template literal section leading up to the first | ||
// expression in this attribute attribute | ||
const attributeString = strings[partIndex]; | ||
// Trim the trailing literal value if this is an interpolation | ||
const rawNameString = attributeString.substring(0, attributeString.length - attributeStrings[0].length); | ||
// Find the attribute name | ||
const rawName = rawNameString.match(/((?:\w|[.\-_$])+)=["']?$/)[1]; | ||
this.parts.push(new TemplatePart('attribute', index, attribute.name, rawName, attributeStrings)); | ||
node.removeAttribute(attribute.name); | ||
partIndex += attributeStrings.length - 1; | ||
i--; | ||
if (attributes[i].value.indexOf(marker) >= 0) { | ||
count++; | ||
} | ||
} | ||
while (count-- > 0) { | ||
// Get the template literal section leading up to the first | ||
// expression in this attribute attribute | ||
const stringForPart = strings[partIndex]; | ||
// Find the attribute name | ||
const attributeNameInPart = lastAttributeNameRegex.exec(stringForPart)[1]; | ||
// Find the corresponding attribute | ||
const attribute = attributes.getNamedItem(attributeNameInPart); | ||
const stringsForAttributeValue = attribute.value.split(markerRegex); | ||
this.parts.push(new TemplatePart('attribute', index, attribute.name, attributeNameInPart, stringsForAttributeValue)); | ||
node.removeAttribute(attribute.name); | ||
partIndex += stringsForAttributeValue.length - 1; | ||
} | ||
} | ||
else if (node.nodeType === 3 /* Node.TEXT_NODE */) { | ||
const nodeValue = node.nodeValue; | ||
const strings = nodeValue.split(attributeMarker); | ||
if (strings.length > 1) { | ||
if (nodeValue.indexOf(marker) > -1) { | ||
const parent = node.parentNode; | ||
const strings = nodeValue.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
@@ -201,3 +213,3 @@ // We have a part for each match found | ||
nextSibling.nodeType === 1 /* Node.ELEMENT_NODE */) && | ||
nodeValue.trim() === '') { | ||
!nonWhitespace.test(nodeValue)) { | ||
nodesToRemove.push(node); | ||
@@ -210,10 +222,18 @@ currentNode = previousNode; | ||
else if (node.nodeType === 8 /* Node.COMMENT_NODE */ && | ||
node.nodeValue === textMarkerContent) { | ||
node.nodeValue === marker) { | ||
const parent = node.parentNode; | ||
// If we don't have a previous node add a marker node. | ||
// If the previousSibling is removed, because it's another part | ||
// placholder, or empty text, add a marker node. | ||
if (node.previousSibling === null || | ||
node.previousSibling !== previousNode) { | ||
parent.insertBefore(new Text(), node); | ||
// Add a new marker node to be the startNode of the Part if any of the | ||
// following are true: | ||
// * We don't have a previousSibling | ||
// * previousSibling is being removed (thus it's not the | ||
// `previousNode`) | ||
// * previousSibling is not a Text node | ||
// | ||
// TODO(justinfagnani): We should be able to use the previousNode here | ||
// as the marker node and reduce the number of extra nodes we add to a | ||
// template. See https://github.com/PolymerLabs/lit-html/issues/147 | ||
const previousSibling = node.previousSibling; | ||
if (previousSibling === null || previousSibling !== previousNode || | ||
previousSibling.nodeType !== Node.TEXT_NODE) { | ||
parent.insertBefore(document.createTextNode(''), node); | ||
} | ||
@@ -225,7 +245,7 @@ else { | ||
nodesToRemove.push(node); | ||
// If we don't have a next node add a marker node. | ||
// If we don't have a nextSibling add a marker node. | ||
// We don't have to check if the next node is going to be removed, | ||
// because that node will induce a marker if so. | ||
// because that node will induce a new marker if so. | ||
if (node.nextSibling === null) { | ||
parent.insertBefore(new Text(), node); | ||
parent.insertBefore(document.createTextNode(''), node); | ||
} | ||
@@ -248,25 +268,32 @@ else { | ||
_getHtml(strings, svg) { | ||
const l = strings.length; | ||
const a = []; | ||
let isTextBinding = false; | ||
for (let i = 0; i < l - 1; i++) { | ||
const l = strings.length - 1; | ||
let html = ''; | ||
let isTextBinding = true; | ||
for (let i = 0; i < l; i++) { | ||
const s = strings[i]; | ||
a.push(s); | ||
// We're in a text position if the previous string matches the | ||
// textRegex. If it doesn't and the previous string has no tags, then | ||
// we use the previous text position state. | ||
isTextBinding = s.match(textRegex) !== null || | ||
(s.match(hasTagsRegex) !== null && isTextBinding); | ||
a.push(isTextBinding ? textMarker : attributeMarker); | ||
html += s; | ||
// We're in a text position if the previous string closed its tags. | ||
// If it doesn't have any tags, then we use the previous text position | ||
// state. | ||
const closing = findTagClose(s); | ||
isTextBinding = closing > -1 ? closing < s.length : isTextBinding; | ||
html += isTextBinding ? nodeMarker : marker; | ||
} | ||
a.push(strings[l - 1]); | ||
const html = a.join(''); | ||
html += strings[l]; | ||
return svg ? `<svg>${html}</svg>` : html; | ||
} | ||
} | ||
/** | ||
* Returns a value ready to be inserted into a Part from a user-provided value. | ||
* | ||
* If the user value is a directive, this invokes the directive with the given | ||
* part. If the value is null, it's converted to undefined to work better | ||
* with certain DOM APIs, like textContent. | ||
*/ | ||
export const getValue = (part, value) => { | ||
// `null` as the value of a Text node will render the string 'null' | ||
// so we convert it to undefined | ||
if (value != null && value.__litDirective === true) { | ||
if (isDirective(value)) { | ||
value = value(part); | ||
return directiveValue; | ||
} | ||
@@ -279,2 +306,4 @@ return value === null ? undefined : value; | ||
}; | ||
const isDirective = (o) => typeof o === 'function' && o.__litDirective === true; | ||
const directiveValue = {}; | ||
export class AttributePart { | ||
@@ -288,21 +317,24 @@ constructor(instance, element, name, strings) { | ||
} | ||
setValue(values, startIndex) { | ||
_interpolate(values, startIndex) { | ||
const strings = this.strings; | ||
const l = strings.length - 1; | ||
let text = ''; | ||
for (let i = 0; i < strings.length; i++) { | ||
for (let i = 0; i < l; i++) { | ||
text += strings[i]; | ||
if (i < strings.length - 1) { | ||
const v = getValue(this, values[startIndex + i]); | ||
if (v && | ||
(Array.isArray(v) || typeof v !== 'string' && v[Symbol.iterator])) { | ||
for (const t of v) { | ||
// TODO: we need to recursively call getValue into iterables... | ||
text += t; | ||
} | ||
const v = getValue(this, values[startIndex + i]); | ||
if (v && v !== directiveValue && | ||
(Array.isArray(v) || typeof v !== 'string' && v[Symbol.iterator])) { | ||
for (const t of v) { | ||
// TODO: we need to recursively call getValue into iterables... | ||
text += t; | ||
} | ||
else { | ||
text += v; | ||
} | ||
} | ||
else { | ||
text += v; | ||
} | ||
} | ||
return text + strings[l]; | ||
} | ||
setValue(values, startIndex) { | ||
const text = this._interpolate(values, startIndex); | ||
this.element.setAttribute(this.name, text); | ||
@@ -320,2 +352,5 @@ } | ||
value = getValue(this, value); | ||
if (value === directiveValue) { | ||
return; | ||
} | ||
if (value === null || | ||
@@ -351,2 +386,5 @@ !(typeof value === 'object' || typeof value === 'function')) { | ||
_setNode(value) { | ||
if (this._previousValue === value) { | ||
return; | ||
} | ||
this.clear(); | ||
@@ -358,2 +396,3 @@ this._insert(value); | ||
const node = this.startNode.nextSibling; | ||
value = value === undefined ? '' : value; | ||
if (node === this.endNode.previousSibling && | ||
@@ -368,3 +407,3 @@ node.nodeType === Node.TEXT_NODE) { | ||
else { | ||
this._setNode(document.createTextNode(value === undefined ? '' : value)); | ||
this._setNode(document.createTextNode(value)); | ||
} | ||
@@ -439,2 +478,3 @@ this._previousValue = value; | ||
_setPromise(value) { | ||
this._previousValue = value; | ||
value.then((v) => { | ||
@@ -445,9 +485,5 @@ if (this._previousValue === value) { | ||
}); | ||
this._previousValue = value; | ||
} | ||
clear(startNode = this.startNode) { | ||
let node; | ||
while ((node = startNode.nextSibling) !== this.endNode) { | ||
node.parentNode.removeChild(node); | ||
} | ||
removeNodes(this.startNode.parentNode, startNode.nextSibling, this.endNode); | ||
} | ||
@@ -489,33 +525,47 @@ } | ||
const fragment = document.importNode(this.template.element.content, true); | ||
if (this.template.parts.length > 0) { | ||
const parts = this.template.parts; | ||
if (parts.length > 0) { | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be | ||
// null | ||
const walker = document.createTreeWalker(fragment, 133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT */, null, false); | ||
const parts = this.template.parts; | ||
let index = 0; | ||
let partIndex = 0; | ||
let templatePart = parts[0]; | ||
let node = walker.nextNode(); | ||
while (node != null && partIndex < parts.length) { | ||
if (index === templatePart.index) { | ||
this._parts.push(this._partCallback(this, templatePart, node)); | ||
templatePart = parts[++partIndex]; | ||
} | ||
else { | ||
const walker = document.createTreeWalker(fragment, 133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
NodeFilter.SHOW_TEXT */, null, false); | ||
let index = -1; | ||
for (let i = 0; i < parts.length; i++) { | ||
const part = parts[i]; | ||
while (index < part.index) { | ||
index++; | ||
node = walker.nextNode(); | ||
walker.nextNode(); | ||
} | ||
this._parts.push(this._partCallback(this, part, walker.currentNode)); | ||
} | ||
} | ||
if (this.template.svg) { | ||
const svgElement = fragment.firstChild; | ||
fragment.removeChild(svgElement); | ||
const nodes = svgElement.childNodes; | ||
for (let i = 0; i < nodes.length; i++) { | ||
fragment.appendChild(nodes.item(i)); | ||
} | ||
} | ||
return fragment; | ||
} | ||
} | ||
/** | ||
* Reparents nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), into another container (could be the same container), before | ||
* `beforeNode`. If `beforeNode` is null, it appends the nodes to the | ||
* container. | ||
*/ | ||
export const reparentNodes = (container, start, end = null, before = null) => { | ||
let node = start; | ||
while (node !== end) { | ||
const n = node.nextSibling; | ||
container.insertBefore(node, before); | ||
node = n; | ||
} | ||
}; | ||
/** | ||
* Removes nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), from `container`. | ||
*/ | ||
export const removeNodes = (container, startNode, endNode = null) => { | ||
let node = startNode; | ||
while (node !== endNode) { | ||
const n = node.nextSibling; | ||
container.removeChild(node); | ||
node = n; | ||
} | ||
}; | ||
//# sourceMappingURL=lit-html.js.map |
{ | ||
"name": "lit-html", | ||
"version": "0.7.1", | ||
"version": "0.8.0-pre.1", | ||
"description": "HTML template literals in JavaScript", | ||
@@ -23,3 +23,3 @@ "license": "BSD-3-Clause", | ||
"gen-docs": "typedoc --readme none --mode modules --excludeNotExported --excludePrivate --exclude **/*_test.ts --out ./docs/api .", | ||
"test": "npm run build && wct -l chrome --npm && npm run lint", | ||
"test": "npm run build && wct --npm && npm run lint", | ||
"quicktest": "wct -l chrome -p --npm", | ||
@@ -41,3 +41,5 @@ "checksize": "uglifyjs lit-html.js -mc --toplevel | gzip -9 | wc -c", | ||
"wct-browser-legacy": "^0.0.1-pre.10", | ||
"web-component-tester": "^6.3.0" | ||
"web-component-tester": "^6.3.0", | ||
"@webcomponents/template": "^1.1.0", | ||
"get-own-property-symbols": "^0.9.2" | ||
}, | ||
@@ -44,0 +46,0 @@ "typings": "lit-html.d.ts", |
@@ -243,2 +243,44 @@ # lit-html | ||
#### `asyncAppend(asyncIterable)` and `asyncReplace(asyncIterable)` | ||
`asyncAppend` renders the values of an [async iterable](https://github.com/tc39/proposal-async-iteration), | ||
appending each new value after the previous. | ||
`asyncReplace` renders the values of an [async iterable](https://github.com/tc39/proposal-async-iteration), | ||
replacing the previous value with the new value. | ||
Example: | ||
```javascript | ||
/** | ||
* Returns an async iterable that yields the number of seconds since `date`, | ||
* approximately every second. | ||
*/ | ||
async function* secondsAgo(date) { | ||
const seconds = (Date.now() - date.getTime()) / 1000; | ||
const wholeSeconds = Math.floor(seconds); | ||
yield wholeSeconds; | ||
yield* await new Promise((res) => { | ||
setTimeout(() => { | ||
res(secondsAgo(date)); | ||
}, 1000 - (seconds - wholeSeconds) * 1000); | ||
}); | ||
} | ||
render(html` | ||
This example started ${asyncReplace(secondsAgo(new Date()))} seconds ago. | ||
`, document.body); | ||
``` | ||
In the near future, `ReadableStream`s will be async iterables, enabling streaming `fetch()` | ||
directly into a template: | ||
```javascript | ||
const streamingResponse = (async () => { | ||
const response = await fetch('https://cors-anywhere.herokuapp.com/http://stuff.mit.edu/afs/sipb/contrib/pi/pi-billion.txt'); | ||
return response.body.getReader(); | ||
})(); | ||
render(html`π is: ${asyncAppend(streamingResponse)}`, document.body); | ||
``` | ||
### Promises | ||
@@ -245,0 +287,0 @@ |
@@ -56,8 +56,7 @@ /** | ||
if (templatePart.rawName!.startsWith('on-')) { | ||
const eventName = templatePart.rawName!.substring(3); | ||
const eventName = templatePart.rawName!.slice(3); | ||
return new EventPart(instance, node as Element, eventName); | ||
} | ||
if (templatePart.name!.endsWith('$')) { | ||
const name = templatePart.name!.substring( | ||
0, templatePart.name!.length - 1); | ||
const name = templatePart.name!.slice(0, -1); | ||
return new AttributePart( | ||
@@ -79,3 +78,3 @@ instance, node as Element, name, templatePart.strings!); | ||
let value: any; | ||
if (s.length === 2 && s[0] === '' && s[s.length - 1] === '') { | ||
if (s.length === 2 && s[0] === '' && s[1] === '') { | ||
// An expression that occupies the whole attribute value will leave | ||
@@ -86,9 +85,3 @@ // leading and trailing empty strings. | ||
// Interpolation, so interpolate | ||
value = ''; | ||
for (let i = 0; i < s.length; i++) { | ||
value += s[i]; | ||
if (i < s.length - 1) { | ||
value += getValue(this, values[startIndex + i]); | ||
} | ||
} | ||
value = this._interpolate(values, startIndex); | ||
} | ||
@@ -113,20 +106,15 @@ (this.element as any)[this.name] = value; | ||
const listener = getValue(this, value); | ||
if (listener === this._listener) { | ||
const previous = this._listener; | ||
if (listener === previous) { | ||
return; | ||
} | ||
if (listener == null) { | ||
this.element.removeEventListener(this.eventName, this); | ||
} else if (this._listener == null) { | ||
this.element.addEventListener(this.eventName, this); | ||
} | ||
this._listener = listener; | ||
} | ||
handleEvent(event: Event) { | ||
if (typeof this._listener === 'function') { | ||
this._listener.call(this.element, event); | ||
} else if (typeof this._listener.handleEvent === 'function') { | ||
this._listener.handleEvent(event); | ||
if (previous != null) { | ||
this.element.removeEventListener(this.eventName, previous); | ||
} | ||
if (listener != null) { | ||
this.element.addEventListener(this.eventName, listener); | ||
} | ||
} | ||
} |
@@ -20,9 +20,10 @@ /** | ||
interface State { | ||
keyMap: Map<any, NodePart>; | ||
parts: NodePart[]; | ||
const keyMapCache = new WeakMap<NodePart, Map<any, NodePart>>(); | ||
function cleanMap(part: NodePart, key: any, map: Map<any, NodePart>) { | ||
if (!part.startNode.parentNode) { | ||
map.delete(key); | ||
} | ||
} | ||
const stateCache = new WeakMap<NodePart, State>(); | ||
export function repeat<T>( | ||
@@ -44,21 +45,12 @@ items: T[], keyFn: KeyFn<T>, template: ItemTemplate<T>): DirectiveFn; | ||
let state = stateCache.get(part); | ||
if (state === undefined) { | ||
state = { | ||
keyMap: keyFn && new Map(), | ||
parts: [], | ||
}; | ||
stateCache.set(part, state); | ||
let keyMap = keyMapCache.get(part); | ||
if (keyMap === undefined) { | ||
keyMap = new Map(); | ||
keyMapCache.set(part, keyMap); | ||
} | ||
const container = part.startNode.parentNode as HTMLElement | ShadowRoot | | ||
DocumentFragment; | ||
const oldParts = state.parts; | ||
const endParts = new Map<Node, NodePart>( | ||
oldParts.map((p) => [p.endNode, p] as [Node, NodePart])); | ||
const keyMap = state.keyMap; | ||
let index = -1; | ||
let currentMarker = part.startNode.nextSibling!; | ||
const itemParts = []; | ||
let index = 0; | ||
let currentMarker: Node|undefined; | ||
for (const item of items) { | ||
@@ -68,4 +60,5 @@ let result; | ||
try { | ||
result = template !(item, index++); | ||
key = keyFn && keyFn(item); | ||
++index; | ||
result = template !(item, index); | ||
key = keyFn ? keyFn(item) : index; | ||
} catch (e) { | ||
@@ -76,85 +69,40 @@ console.error(e); | ||
// Try to reuse a part, either keyed or from the list of previous parts | ||
// if there's no keyMap | ||
let itemPart = keyMap === undefined ? oldParts[0] : keyMap.get(key); | ||
// Try to reuse a part | ||
let itemPart = keyMap.get(key); | ||
if (itemPart === undefined) { | ||
// New part, attach it | ||
if (currentMarker === undefined) { | ||
currentMarker = new Text(); | ||
container.insertBefore(currentMarker, part.startNode.nextSibling); | ||
const marker = document.createTextNode(''); | ||
const endNode = document.createTextNode(''); | ||
container.insertBefore(marker, currentMarker); | ||
container.insertBefore(endNode, currentMarker); | ||
itemPart = new NodePart(part.instance, marker, endNode); | ||
if (key !== undefined) { | ||
keyMap.set(key, itemPart); | ||
} | ||
const endNode = new Text(); | ||
container.insertBefore(endNode, currentMarker.nextSibling); | ||
itemPart = new NodePart(part.instance, currentMarker, endNode); | ||
if (key !== undefined && keyMap !== undefined) { | ||
keyMap.set(key, itemPart!); | ||
} else if (currentMarker !== itemPart.startNode) { | ||
// Existing part in the wrong position | ||
const end = itemPart.endNode.nextSibling; | ||
for (let node = itemPart.startNode; node !== end;) { | ||
const n = node.nextSibling!; | ||
container.insertBefore(node, currentMarker); | ||
node = n; | ||
} | ||
} else { | ||
// Existing part, maybe move it | ||
const range = document.createRange(); | ||
range.setStartBefore(itemPart.startNode); | ||
range.setEndBefore(itemPart.endNode); | ||
if (currentMarker === undefined) { | ||
// this should be the first part, make sure it's first | ||
if (part.startNode.nextSibling !== itemPart.startNode) { | ||
// move the whole part | ||
// get previous and next parts | ||
const previousPart = endParts.get(itemPart.startNode); | ||
if (previousPart) { | ||
previousPart.endNode = itemPart.endNode; | ||
endParts.set(previousPart.endNode, previousPart); | ||
} | ||
const contents = range.extractContents(); | ||
if (part.startNode.nextSibling === part.endNode) { | ||
// The container part was empty, so we need a new endPart | ||
itemPart.endNode = new Text(); | ||
container.insertBefore( | ||
itemPart.endNode, part.startNode.nextSibling); | ||
} else { | ||
// endNode should equal the startNode of the currently first part | ||
itemPart.endNode = part.startNode.nextSibling!; | ||
} | ||
container.insertBefore(contents, part.startNode.nextSibling); | ||
} | ||
// else part is in the correct position already | ||
} else if (currentMarker !== itemPart.startNode) { | ||
// move to correct position | ||
const previousPart = endParts.get(itemPart.startNode); | ||
if (previousPart) { | ||
previousPart.endNode = itemPart.endNode; | ||
endParts.set(previousPart.endNode, previousPart); | ||
} | ||
const contents = range.extractContents(); | ||
container.insertBefore(contents, currentMarker); | ||
} | ||
// remove part from oldParts list so it's not cleaned up | ||
oldParts.splice(oldParts.indexOf(itemPart), 1); | ||
// else part is in the correct position already | ||
currentMarker = itemPart.endNode.nextSibling!; | ||
} | ||
// else part is in the correct position already | ||
itemPart.setValue(result); | ||
itemParts.push(itemPart); | ||
currentMarker = itemPart.endNode; | ||
} | ||
// Cleanup | ||
if (oldParts.length > 0) { | ||
const clearStart = oldParts[0].startNode; | ||
const clearEnd = oldParts[oldParts.length - 1].endNode; | ||
const clearRange = document.createRange(); | ||
if (itemParts.length === 0) { | ||
clearRange.setStartBefore(clearStart); | ||
} else { | ||
clearRange.setStartAfter(clearStart); | ||
if (currentMarker !== part.endNode) { | ||
const end = part.endNode; | ||
for (let node = currentMarker; node !== end;) { | ||
const n = node.nextSibling!; | ||
container.removeChild(node); | ||
node = n; | ||
} | ||
clearRange.setEndAfter(clearEnd); | ||
clearRange.deleteContents(); | ||
clearRange.detach(); // is this neccessary? | ||
keyMap.forEach(cleanMap); | ||
} | ||
state.parts = itemParts; | ||
}); | ||
} |
@@ -24,6 +24,6 @@ /** | ||
*/ | ||
export const unsafeHTML = (value: any) => directive((_part: NodePart) => { | ||
export const unsafeHTML = (value: any) => directive((part: NodePart) => { | ||
const tmp = document.createElement('template'); | ||
tmp.innerHTML = value; | ||
return document.importNode(tmp.content, true); | ||
part.setValue(document.importNode(tmp.content, true)); | ||
}); |
@@ -23,3 +23,3 @@ /** | ||
part.setValue(defaultContent); | ||
return promise; | ||
part.setValue(promise); | ||
}); |
@@ -101,6 +101,3 @@ /** | ||
let child; | ||
while ((child = container.lastChild)) { | ||
container.removeChild(child); | ||
} | ||
removeNodes(container, container.firstChild); | ||
container.appendChild(fragment); | ||
@@ -113,16 +110,20 @@ } | ||
*/ | ||
const attributeMarker = `{{lit-${Math.random()}}}`; | ||
const marker = `{{lit-${String(Math.random()).slice(2)}}}`; | ||
const nodeMarker = `<!--${marker}-->`; | ||
const markerRegex = new RegExp(`${marker}|${nodeMarker}`); | ||
const nonWhitespace = /[^\s]/; | ||
const lastAttributeNameRegex = /([\w\-.$]+)=(?:[^"']*|"[^"]*|'[^']*)$/; | ||
/** | ||
* Regex to scan the string preceding an expression to see if we're in a text | ||
* context, and not an attribute context. | ||
* | ||
* This works by seeing if we have a `>` not followed by a `<`. If there is a | ||
* `<` closer to the end of the strings, then we're inside a tag. | ||
* Finds the closing index of the last closed HTML tag. | ||
* This has 3 possible return values: | ||
* - `-1`, meaning there is no tag in str. | ||
* - `string.length`, meaning the last opened tag is unclosed. | ||
* - Some positive number < str.length, meaning the index of the closing '>'. | ||
*/ | ||
const textRegex = />[^<]*$/; | ||
const hasTagsRegex = /[^<]*/; | ||
const textMarkerContent = '_-lit-html-_'; | ||
const textMarker = `<!--${textMarkerContent}-->`; | ||
const attrOrTextRegex = new RegExp(`${attributeMarker}|${textMarker}`); | ||
function findTagClose(str: string): number { | ||
const close = str.lastIndexOf('>'); | ||
const open = str.indexOf('<', close + 1); | ||
return open > -1 ? str.length : close; | ||
} | ||
@@ -156,11 +157,17 @@ /** | ||
element: HTMLTemplateElement; | ||
svg: boolean; | ||
constructor(strings: TemplateStringsArray, svg: boolean = false) { | ||
this.svg = svg; | ||
this.element = document.createElement('template'); | ||
this.element.innerHTML = this._getHtml(strings, svg); | ||
const element = this.element = document.createElement('template'); | ||
element.innerHTML = this._getHtml(strings, svg); | ||
const content = element.content; | ||
if (svg) { | ||
const svgElement = content.firstChild!; | ||
content.removeChild(svgElement); | ||
reparentNodes(content, svgElement.firstChild); | ||
} | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
const walker = document.createTreeWalker( | ||
this.element.content, | ||
content, | ||
133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
@@ -190,26 +197,36 @@ NodeFilter.SHOW_TEXT */ | ||
const attributes = node.attributes; | ||
// Per https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap, | ||
// attributes are not guaranteed to be returned in document order. In | ||
// particular, Edge/IE can return them out of order, so we cannot assume | ||
// a correspondance between part index and attribute index. | ||
let count = 0; | ||
for (let i = 0; i < attributes.length; i++) { | ||
const attribute = attributes.item(i); | ||
const attributeStrings = attribute.value.split(attrOrTextRegex); | ||
if (attributeStrings.length > 1) { | ||
// Get the template literal section leading up to the first | ||
// expression in this attribute attribute | ||
const attributeString = strings[partIndex]; | ||
// Trim the trailing literal value if this is an interpolation | ||
const rawNameString = attributeString.substring( | ||
0, attributeString.length - attributeStrings[0].length); | ||
// Find the attribute name | ||
const rawName = rawNameString.match(/((?:\w|[.\-_$])+)=["']?$/)![1]; | ||
this.parts.push(new TemplatePart( | ||
'attribute', index, attribute.name, rawName, attributeStrings)); | ||
node.removeAttribute(attribute.name); | ||
partIndex += attributeStrings.length - 1; | ||
i--; | ||
if (attributes[i].value.indexOf(marker) >= 0) { | ||
count++; | ||
} | ||
} | ||
while (count-- > 0) { | ||
// Get the template literal section leading up to the first | ||
// expression in this attribute attribute | ||
const stringForPart = strings[partIndex]; | ||
// Find the attribute name | ||
const attributeNameInPart = | ||
lastAttributeNameRegex.exec(stringForPart)![1]; | ||
// Find the corresponding attribute | ||
const attribute = attributes.getNamedItem(attributeNameInPart); | ||
const stringsForAttributeValue = attribute.value.split(markerRegex); | ||
this.parts.push(new TemplatePart( | ||
'attribute', | ||
index, | ||
attribute.name, | ||
attributeNameInPart, | ||
stringsForAttributeValue)); | ||
node.removeAttribute(attribute.name); | ||
partIndex += stringsForAttributeValue.length - 1; | ||
} | ||
} else if (node.nodeType === 3 /* Node.TEXT_NODE */) { | ||
const nodeValue = node.nodeValue!; | ||
const strings = nodeValue.split(attributeMarker); | ||
if (strings.length > 1) { | ||
if (nodeValue.indexOf(marker) > -1) { | ||
const parent = node.parentNode!; | ||
const strings = nodeValue.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
@@ -240,3 +257,3 @@ | ||
nextSibling.nodeType === 1 /* Node.ELEMENT_NODE */) && | ||
nodeValue.trim() === '') { | ||
!nonWhitespace.test(nodeValue)) { | ||
nodesToRemove.push(node); | ||
@@ -249,10 +266,18 @@ currentNode = previousNode; | ||
node.nodeType === 8 /* Node.COMMENT_NODE */ && | ||
node.nodeValue === textMarkerContent) { | ||
node.nodeValue === marker) { | ||
const parent = node.parentNode!; | ||
// If we don't have a previous node add a marker node. | ||
// If the previousSibling is removed, because it's another part | ||
// placholder, or empty text, add a marker node. | ||
if (node.previousSibling === null || | ||
node.previousSibling !== previousNode) { | ||
parent.insertBefore(new Text(), node); | ||
// Add a new marker node to be the startNode of the Part if any of the | ||
// following are true: | ||
// * We don't have a previousSibling | ||
// * previousSibling is being removed (thus it's not the | ||
// `previousNode`) | ||
// * previousSibling is not a Text node | ||
// | ||
// TODO(justinfagnani): We should be able to use the previousNode here | ||
// as the marker node and reduce the number of extra nodes we add to a | ||
// template. See https://github.com/PolymerLabs/lit-html/issues/147 | ||
const previousSibling = node.previousSibling; | ||
if (previousSibling === null || previousSibling !== previousNode || | ||
previousSibling.nodeType !== Node.TEXT_NODE) { | ||
parent.insertBefore(document.createTextNode(''), node); | ||
} else { | ||
@@ -263,7 +288,7 @@ index--; | ||
nodesToRemove.push(node); | ||
// If we don't have a next node add a marker node. | ||
// If we don't have a nextSibling add a marker node. | ||
// We don't have to check if the next node is going to be removed, | ||
// because that node will induce a marker if so. | ||
// because that node will induce a new marker if so. | ||
if (node.nextSibling === null) { | ||
parent.insertBefore(new Text(), node); | ||
parent.insertBefore(document.createTextNode(''), node); | ||
} else { | ||
@@ -287,17 +312,16 @@ index--; | ||
private _getHtml(strings: TemplateStringsArray, svg?: boolean): string { | ||
const l = strings.length; | ||
const a = []; | ||
let isTextBinding = false; | ||
for (let i = 0; i < l - 1; i++) { | ||
const l = strings.length - 1; | ||
let html = ''; | ||
let isTextBinding = true; | ||
for (let i = 0; i < l; i++) { | ||
const s = strings[i]; | ||
a.push(s); | ||
// We're in a text position if the previous string matches the | ||
// textRegex. If it doesn't and the previous string has no tags, then | ||
// we use the previous text position state. | ||
isTextBinding = s.match(textRegex) !== null || | ||
(s.match(hasTagsRegex) !== null && isTextBinding); | ||
a.push(isTextBinding ? textMarker : attributeMarker); | ||
html += s; | ||
// We're in a text position if the previous string closed its tags. | ||
// If it doesn't have any tags, then we use the previous text position | ||
// state. | ||
const closing = findTagClose(s); | ||
isTextBinding = closing > -1 ? closing < s.length : isTextBinding; | ||
html += isTextBinding ? nodeMarker : marker; | ||
} | ||
a.push(strings[l - 1]); | ||
const html = a.join(''); | ||
html += strings[l]; | ||
return svg ? `<svg>${html}</svg>` : html; | ||
@@ -307,7 +331,15 @@ } | ||
/** | ||
* Returns a value ready to be inserted into a Part from a user-provided value. | ||
* | ||
* If the user value is a directive, this invokes the directive with the given | ||
* part. If the value is null, it's converted to undefined to work better | ||
* with certain DOM APIs, like textContent. | ||
*/ | ||
export const getValue = (part: Part, value: any) => { | ||
// `null` as the value of a Text node will render the string 'null' | ||
// so we convert it to undefined | ||
if (value != null && value.__litDirective === true) { | ||
if (isDirective(value)) { | ||
value = value(part); | ||
return directiveValue; | ||
} | ||
@@ -324,9 +356,10 @@ return value === null ? undefined : value; | ||
const isDirective = (o: any) => | ||
typeof o === 'function' && o.__litDirective === true; | ||
const directiveValue = {}; | ||
export interface Part { | ||
instance: TemplateInstance; | ||
size?: number; | ||
// constructor(instance: TemplateInstance) { | ||
// this.instance = instance; | ||
// } | ||
} | ||
@@ -357,21 +390,25 @@ | ||
setValue(values: any[], startIndex: number): void { | ||
protected _interpolate(values: any[], startIndex: number) { | ||
const strings = this.strings; | ||
const l = strings.length - 1; | ||
let text = ''; | ||
for (let i = 0; i < strings.length; i++) { | ||
for (let i = 0; i < l; i++) { | ||
text += strings[i]; | ||
if (i < strings.length - 1) { | ||
const v = getValue(this, values[startIndex + i]); | ||
if (v && | ||
(Array.isArray(v) || typeof v !== 'string' && v[Symbol.iterator])) { | ||
for (const t of v) { | ||
// TODO: we need to recursively call getValue into iterables... | ||
text += t; | ||
} | ||
} else { | ||
text += v; | ||
const v = getValue(this, values[startIndex + i]); | ||
if (v && v !== directiveValue && | ||
(Array.isArray(v) || typeof v !== 'string' && v[Symbol.iterator])) { | ||
for (const t of v) { | ||
// TODO: we need to recursively call getValue into iterables... | ||
text += t; | ||
} | ||
} else { | ||
text += v; | ||
} | ||
} | ||
return text + strings[l]; | ||
} | ||
setValue(values: any[], startIndex: number): void { | ||
const text = this._interpolate(values, startIndex); | ||
this.element.setAttribute(this.name, text); | ||
@@ -385,3 +422,3 @@ } | ||
endNode: Node; | ||
private _previousValue: any; | ||
_previousValue: any; | ||
@@ -397,3 +434,5 @@ constructor(instance: TemplateInstance, startNode: Node, endNode: Node) { | ||
value = getValue(this, value); | ||
if (value === directiveValue) { | ||
return; | ||
} | ||
if (value === null || | ||
@@ -426,2 +465,5 @@ !(typeof value === 'object' || typeof value === 'function')) { | ||
private _setNode(value: Node): void { | ||
if (this._previousValue === value) { | ||
return; | ||
} | ||
this.clear(); | ||
@@ -434,2 +476,3 @@ this._insert(value); | ||
const node = this.startNode.nextSibling!; | ||
value = value === undefined ? '' : value; | ||
if (node === this.endNode.previousSibling && | ||
@@ -443,3 +486,3 @@ node.nodeType === Node.TEXT_NODE) { | ||
} else { | ||
this._setNode(document.createTextNode(value === undefined ? '' : value)); | ||
this._setNode(document.createTextNode(value)); | ||
} | ||
@@ -520,3 +563,4 @@ this._previousValue = value; | ||
protected _setPromise(value: Promise<any>): void { | ||
private _setPromise(value: Promise<any>): void { | ||
this._previousValue = value; | ||
value.then((v: any) => { | ||
@@ -527,10 +571,6 @@ if (this._previousValue === value) { | ||
}); | ||
this._previousValue = value; | ||
} | ||
clear(startNode: Node = this.startNode) { | ||
let node; | ||
while ((node = startNode.nextSibling!) !== this.endNode) { | ||
node.parentNode!.removeChild(node); | ||
} | ||
removeNodes(this.startNode.parentNode!, startNode.nextSibling!, this.endNode); | ||
} | ||
@@ -587,4 +627,5 @@ } | ||
const fragment = document.importNode(this.template.element.content, true); | ||
const parts = this.template.parts; | ||
if (this.template.parts.length > 0) { | ||
if (parts.length > 0) { | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be | ||
@@ -594,3 +635,4 @@ // null | ||
fragment, | ||
133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT */ | ||
133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
NodeFilter.SHOW_TEXT */ | ||
, | ||
@@ -600,27 +642,49 @@ null as any, | ||
const parts = this.template.parts; | ||
let index = 0; | ||
let partIndex = 0; | ||
let templatePart = parts[0]; | ||
let node = walker.nextNode(); | ||
while (node != null && partIndex < parts.length) { | ||
if (index === templatePart.index) { | ||
this._parts.push(this._partCallback(this, templatePart, node)); | ||
templatePart = parts[++partIndex]; | ||
} else { | ||
let index = -1; | ||
for (let i = 0; i < parts.length; i++) { | ||
const part = parts[i]; | ||
while (index < part.index) { | ||
index++; | ||
node = walker.nextNode(); | ||
walker.nextNode(); | ||
} | ||
this._parts.push(this._partCallback(this, part, walker.currentNode)); | ||
} | ||
} | ||
if (this.template.svg) { | ||
const svgElement = fragment.firstChild!; | ||
fragment.removeChild(svgElement); | ||
const nodes = svgElement.childNodes; | ||
for (let i = 0; i < nodes.length; i++) { | ||
fragment.appendChild(nodes.item(i)); | ||
} | ||
} | ||
return fragment; | ||
} | ||
} | ||
/** | ||
* Reparents nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), into another container (could be the same container), before | ||
* `beforeNode`. If `beforeNode` is null, it appends the nodes to the | ||
* container. | ||
*/ | ||
export const reparentNodes = | ||
(container: Node, | ||
start: Node | null, | ||
end: Node | null = null, | ||
before: Node | null = null): void => { | ||
let node = start; | ||
while (node !== end) { | ||
const n = node!.nextSibling; | ||
container.insertBefore(node!, before as Node); | ||
node = n; | ||
} | ||
}; | ||
/** | ||
* Removes nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), from `container`. | ||
*/ | ||
export const removeNodes = | ||
(container: Node, | ||
startNode: Node | null, | ||
endNode: Node | null = null): void => { | ||
let node = startNode; | ||
while (node !== endNode) { | ||
const n = node!.nextSibling; | ||
container.removeChild(node!); | ||
node = n; | ||
} | ||
}; |
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
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
173964
32
2258
416
0
12