lit-html
Advanced tools
Comparing version 1.0.0 to 1.0.1-dev.58f394e
@@ -19,2 +19,18 @@ # Change Log | ||
## [1.0.1] - 2019-05-20 | ||
### Changed | ||
* Many small performance enhancements. | ||
* Private names are now named with a `__` prefix ([#859](https://github.com/Polymer/lit-html/issues/859)). | ||
### Added | ||
* Setup continuous benchmarking with Tachometer ([#887](https://github.com/Polymer/lit-html/issues/887)). | ||
### Fixed | ||
* Prevent empty styles from causing exceptions or breaking rendering when using `shady-render` ([#760](https://github.com/Polymer/lit-html/issues/760)). | ||
* Primitive values in attributes are now always simply stringified, regardless of whether they are iterable. ([#830](https://github.com/Polymer/lit-html/pull/830)) | ||
* Adopt and upgrade template fragments after processing for parts ([#831](https://github.com/Polymer/lit-html/issues/831)). | ||
* Fixed bindings with attribute-like expressions preceeding them ([#855](https://github.com/Polymer/lit-html/issues/855)). | ||
* Fixed errors with bindings in HTML comments ([#882](https://github.com/Polymer/lit-html/issues/882)). | ||
## [1.0.0] - 2019-02-05 | ||
@@ -21,0 +37,0 @@ ### Changed |
@@ -16,3 +16,3 @@ /** | ||
export interface ClassInfo { | ||
[name: string]: string | boolean | number; | ||
readonly [name: string]: string | boolean | number; | ||
} | ||
@@ -19,0 +19,0 @@ /** |
@@ -15,15 +15,2 @@ /** | ||
import { AttributePart, directive, PropertyPart } from '../lit-html.js'; | ||
// On IE11, classList.toggle doesn't accept a second argument. | ||
// Since this is so minor, we just polyfill it. | ||
if (window.navigator.userAgent.match('Trident')) { | ||
DOMTokenList.prototype.toggle = function (token, force) { | ||
if (force === undefined || force) { | ||
this.add(token); | ||
} | ||
else { | ||
this.remove(token); | ||
} | ||
return force === undefined ? true : force; | ||
}; | ||
} | ||
/** | ||
@@ -35,9 +22,2 @@ * Stores the ClassInfo object applied to a given AttributePart. | ||
/** | ||
* Stores AttributeParts that have had static classes applied (e.g. `foo` in | ||
* class="foo ${classMap()}"). Static classes are applied only the first time | ||
* the directive is run on a part. | ||
*/ | ||
// Note, could be a WeakSet, but prefer not requiring this polyfill. | ||
const classMapStatics = new WeakMap(); | ||
/** | ||
* A directive that applies CSS classes. This must be used in the `class` | ||
@@ -58,7 +38,9 @@ * attribute and must be the only part used in the attribute. It takes each | ||
} | ||
const { committer } = part; | ||
const { element } = committer; | ||
// handle static classes | ||
if (!classMapStatics.has(part)) { | ||
part.committer.element.className = part.committer.strings.join(' '); | ||
classMapStatics.set(part, true); | ||
if (!classMapCache.has(part)) { | ||
element.className = committer.strings.join(' '); | ||
} | ||
const { classList } = element; | ||
// remove old classes that no longer apply | ||
@@ -68,3 +50,3 @@ const oldInfo = classMapCache.get(part); | ||
if (!(name in classInfo)) { | ||
part.committer.element.classList.remove(name); | ||
classList.remove(name); | ||
} | ||
@@ -74,6 +56,8 @@ } | ||
for (const name in classInfo) { | ||
if (!oldInfo || (oldInfo[name] !== classInfo[name])) { | ||
const value = classInfo[name]; | ||
if (!oldInfo || value !== oldInfo[name]) { | ||
// We explicitly want a loose truthy check here because | ||
// it seems more convenient that '' and 0 are skipped. | ||
part.committer.element.classList.toggle(name, Boolean(classInfo[name])); | ||
const method = value ? 'add' : 'remove'; | ||
classList[method](name); | ||
} | ||
@@ -80,0 +64,0 @@ } |
@@ -120,11 +120,10 @@ /** | ||
// * 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): | ||
// 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): | ||
// | ||
@@ -135,14 +134,12 @@ // oldHead v v oldTail | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new | ||
// item order | ||
// 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. | ||
// 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 | ||
// 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 | ||
@@ -154,11 +151,10 @@ // next diagram). | ||
// newParts: [0, , , , , , ] <- heads matched: update 0 | ||
// and newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldHead | ||
// & newHead | ||
// 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. | ||
// * 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. | ||
// | ||
@@ -168,107 +164,98 @@ // oldHead v v oldTail | ||
// newParts: [0, , , , , , 6] <- tails matched: update 6 | ||
// and newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldTail | ||
// & newTail | ||
// 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. | ||
// 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. | ||
// 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 | ||
// 5 and newKeys: [0, 2, 1, 4, 3, 7, 6] advance oldTail | ||
// 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. | ||
// 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. | ||
// 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 | ||
// into place newKeys: [0, 2, 1, 4, 3, 7, 6] and advance | ||
// newHead | ||
// 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. | ||
// 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... | ||
// 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. | ||
// 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 | ||
// and newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldHead | ||
// & newHead | ||
// 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. | ||
// 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`. | ||
// newParts), so advance `oldHead`. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] // old head already used; | ||
// advance newParts: [0, 2, 1, , , , 6] // oldHead newKeys: | ||
// [0, 2, 1, 4, 3, 7, 6] | ||
// 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. | ||
// 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. | ||
// 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. | ||
// | ||
@@ -278,10 +265,9 @@ // oldHead v v oldTail | ||
// newParts: [0, 2, 1, 4, , , 6] <- old tail matches new | ||
// head: update newKeys: [0, 2, 1, 4, 3, 7, 6] & move 4, | ||
// advance oldTail & newHead | ||
// 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. | ||
// old head part in place, and advance the `oldHead` and | ||
// `newHead` pointers. | ||
// | ||
@@ -291,16 +277,14 @@ // oldHead v oldTail | ||
// newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3 | ||
// and advance newKeys: [0, 2, 1, 4, 3, 7, 6] oldHead & | ||
// newHead | ||
// 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. | ||
// 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. | ||
// with the main loop. Create the remaining part and insert | ||
// it at the new head position, and the update is complete. | ||
// | ||
@@ -313,22 +297,22 @@ // (oldHead > oldTail) | ||
// | ||
// * 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. | ||
// * 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. | ||
// 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) { | ||
@@ -335,0 +319,0 @@ if (oldParts[oldHead] === null) { |
@@ -16,3 +16,3 @@ /** | ||
export interface StyleInfo { | ||
[name: string]: string; | ||
readonly [name: string]: string; | ||
} | ||
@@ -19,0 +19,0 @@ /** |
@@ -21,9 +21,2 @@ /** | ||
/** | ||
* Stores AttributeParts that have had static styles applied (e.g. `height: 0;` | ||
* in style="height: 0; ${styleMap()}"). Static styles are applied only the | ||
* first time the directive is run on a part. | ||
*/ | ||
// Note, could be a WeakSet, but prefer not requiring this polyfill. | ||
const styleMapStatics = new WeakMap(); | ||
/** | ||
* A directive that applies CSS properties to an element. | ||
@@ -51,9 +44,8 @@ * | ||
} | ||
const { committer } = part; | ||
const { style } = committer.element; | ||
// Handle static styles the first time we see a Part | ||
if (!styleMapStatics.has(part)) { | ||
part.committer.element.style.cssText = | ||
part.committer.strings.join(' '); | ||
styleMapStatics.set(part, true); | ||
if (!styleMapCache.has(part)) { | ||
style.cssText = committer.strings.join(' '); | ||
} | ||
const style = part.committer.element.style; | ||
// Remove old properties that no longer exist in styleInfo | ||
@@ -60,0 +52,0 @@ const oldInfo = styleMapCache.get(part); |
@@ -17,2 +17,4 @@ /** | ||
const _state = new WeakMap(); | ||
// Effectively infinity, but a SMI. | ||
const _infinity = 0x7fffffff; | ||
/** | ||
@@ -41,2 +43,3 @@ * Renders one of a series of values, including Promises, to a Part. | ||
state = { | ||
lastRenderedIndex: _infinity, | ||
values: [], | ||
@@ -47,6 +50,7 @@ }; | ||
const previousValues = state.values; | ||
let previousLength = previousValues.length; | ||
state.values = args; | ||
for (let i = 0; i < args.length; i++) { | ||
// If we've rendered a higher-priority value already, stop. | ||
if (state.lastRenderedIndex !== undefined && i > state.lastRenderedIndex) { | ||
if (i > state.lastRenderedIndex) { | ||
break; | ||
@@ -65,5 +69,3 @@ } | ||
// If this is a Promise we've already handled, skip it. | ||
if (state.lastRenderedIndex !== undefined && | ||
typeof value.then === 'function' && | ||
value === previousValues[i]) { | ||
if (i < previousLength && value === previousValues[i]) { | ||
continue; | ||
@@ -73,3 +75,4 @@ } | ||
// changed. Forget what we rendered before. | ||
state.lastRenderedIndex = undefined; | ||
state.lastRenderedIndex = _infinity; | ||
previousLength = 0; | ||
Promise.resolve(value).then((resolvedValue) => { | ||
@@ -80,5 +83,3 @@ const index = state.values.indexOf(value); | ||
// higher-priority than what's already been rendered. | ||
if (index > -1 && | ||
(state.lastRenderedIndex === undefined || | ||
index < state.lastRenderedIndex)) { | ||
if (index > -1 && index < state.lastRenderedIndex) { | ||
state.lastRenderedIndex = index; | ||
@@ -85,0 +86,0 @@ part.setValue(resolvedValue); |
@@ -34,3 +34,3 @@ /** | ||
*/ | ||
handleAttributeExpressions(element: Element, name: string, strings: string[], options: RenderOptions): Part[]; | ||
handleAttributeExpressions(element: Element, name: string, strings: string[], options: RenderOptions): ReadonlyArray<Part>; | ||
/** | ||
@@ -37,0 +37,0 @@ * Create parts for a text-position binding. |
@@ -31,4 +31,4 @@ /** | ||
if (prefix === '.') { | ||
const comitter = new PropertyCommitter(element, name.slice(1), strings); | ||
return comitter.parts; | ||
const committer = new PropertyCommitter(element, name.slice(1), strings); | ||
return committer.parts; | ||
} | ||
@@ -41,4 +41,4 @@ if (prefix === '@') { | ||
} | ||
const comitter = new AttributeCommitter(element, name, strings); | ||
return comitter.parts; | ||
const committer = new AttributeCommitter(element, name, strings); | ||
return committer.parts; | ||
} | ||
@@ -45,0 +45,0 @@ /** |
@@ -21,12 +21,33 @@ /** | ||
/** | ||
* Brands a function as a directive so that lit-html will call the function | ||
* during template rendering, rather than passing as a value. | ||
* Brands a function as a directive factory function so that lit-html will call | ||
* the function during template rendering, rather than passing as a value. | ||
* | ||
* A _directive_ is a function that takes a Part as an argument. It has the | ||
* signature: `(part: Part) => void`. | ||
* | ||
* A directive _factory_ is a function that takes arguments for data and | ||
* configuration and returns a directive. Users of directive usually refer to | ||
* the directive factory as the directive. For example, "The repeat directive". | ||
* | ||
* Usually a template author will invoke a directive factory in their template | ||
* with relevant arguments, which will then return a directive function. | ||
* | ||
* Here's an example of using the `repeat()` directive factory that takes an | ||
* array and a function to render an item: | ||
* | ||
* ```js | ||
* html`<ul><${repeat(items, (item) => html`<li>${item}</li>`)}</ul>` | ||
* ``` | ||
* | ||
* When `repeat` is invoked, it returns a directive function that closes over | ||
* `items` and the template function. When the outer template is rendered, the | ||
* return directive function is called with the Part for the expression. | ||
* `repeat` then performs it's custom logic to render multiple items. | ||
* | ||
* @param f The directive factory function. Must be a function that returns a | ||
* function of the signature `(part: Part) => void`. The returned function will | ||
* be called with the part object | ||
* be called with the part object. | ||
* | ||
* @example | ||
* | ||
* ``` | ||
* import {directive, html} from 'lit-html'; | ||
@@ -39,3 +60,2 @@ * | ||
* }); | ||
* ``` | ||
*/ | ||
@@ -42,0 +62,0 @@ export declare const directive: <F extends DirectiveFactory>(f: F) => F; |
@@ -16,12 +16,33 @@ /** | ||
/** | ||
* Brands a function as a directive so that lit-html will call the function | ||
* during template rendering, rather than passing as a value. | ||
* Brands a function as a directive factory function so that lit-html will call | ||
* the function during template rendering, rather than passing as a value. | ||
* | ||
* A _directive_ is a function that takes a Part as an argument. It has the | ||
* signature: `(part: Part) => void`. | ||
* | ||
* A directive _factory_ is a function that takes arguments for data and | ||
* configuration and returns a directive. Users of directive usually refer to | ||
* the directive factory as the directive. For example, "The repeat directive". | ||
* | ||
* Usually a template author will invoke a directive factory in their template | ||
* with relevant arguments, which will then return a directive function. | ||
* | ||
* Here's an example of using the `repeat()` directive factory that takes an | ||
* array and a function to render an item: | ||
* | ||
* ```js | ||
* html`<ul><${repeat(items, (item) => html`<li>${item}</li>`)}</ul>` | ||
* ``` | ||
* | ||
* When `repeat` is invoked, it returns a directive function that closes over | ||
* `items` and the template function. When the outer template is rendered, the | ||
* return directive function is called with the Part for the expression. | ||
* `repeat` then performs it's custom logic to render multiple items. | ||
* | ||
* @param f The directive factory function. Must be a function that returns a | ||
* function of the signature `(part: Part) => void`. The returned function will | ||
* be called with the part object | ||
* be called with the part object. | ||
* | ||
* @example | ||
* | ||
* ``` | ||
* import {directive, html} from 'lit-html'; | ||
@@ -34,5 +55,3 @@ * | ||
* }); | ||
* ``` | ||
*/ | ||
// tslint:disable-next-line:no-any | ||
export const directive = (f) => ((...args) => { | ||
@@ -39,0 +58,0 @@ const d = f(...args); |
@@ -19,13 +19,12 @@ /** | ||
/** | ||
* 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. | ||
* Reparents nodes, starting from `start` (inclusive) to `end` (exclusive), | ||
* into another container (could be the same container), before `before`. If | ||
* `before` 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`. | ||
* Removes nodes, starting from `start` (inclusive) to `end` (exclusive), from | ||
* `container`. | ||
*/ | ||
export declare const removeNodes: (container: Node, startNode: Node | null, endNode?: Node | null) => void; | ||
export declare const removeNodes: (container: Node, start: Node | null, end?: Node | null) => void; | ||
//# sourceMappingURL=dom.d.ts.map |
@@ -21,27 +21,24 @@ /** | ||
/** | ||
* 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. | ||
* Reparents nodes, starting from `start` (inclusive) to `end` (exclusive), | ||
* into another container (could be the same container), before `before`. If | ||
* `before` 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; | ||
while (start !== end) { | ||
const n = start.nextSibling; | ||
container.insertBefore(start, before); | ||
start = n; | ||
} | ||
}; | ||
/** | ||
* Removes nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), from `container`. | ||
* Removes nodes, starting from `start` (inclusive) to `end` (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; | ||
export const removeNodes = (container, start, end = null) => { | ||
while (start !== end) { | ||
const n = start.nextSibling; | ||
container.removeChild(start); | ||
start = n; | ||
} | ||
}; | ||
//# sourceMappingURL=dom.js.map |
@@ -22,3 +22,3 @@ /** | ||
export interface Part { | ||
value: unknown; | ||
readonly value: unknown; | ||
/** | ||
@@ -30,3 +30,10 @@ * Sets the current part value, but does not write it to the DOM. | ||
/** | ||
* Commits the current part value, cause it to actually be written to the DOM. | ||
* Commits the current part value, causing it to actually be written to the | ||
* DOM. | ||
* | ||
* Directives are run at the start of `commit`, so that if they call | ||
* `part.setValue(...)` synchronously that value will be used in the current | ||
* commit, and there's no need to call `part.commit()` within the directive. | ||
* If directives set a part value asynchronously, then they must call | ||
* `part.commit()` manually. | ||
*/ | ||
@@ -39,3 +46,3 @@ commit(): void; | ||
*/ | ||
export declare const noChange: object; | ||
export declare const noChange: {}; | ||
/** | ||
@@ -42,0 +49,0 @@ * A sentinel value that signals a NodePart to fully clear its content. |
@@ -18,13 +18,15 @@ /** | ||
export declare const isPrimitive: (value: unknown) => value is Primitive; | ||
export declare const isIterable: (value: unknown) => value is Iterable<unknown>; | ||
/** | ||
* Sets attribute values for AttributeParts, so that the value is only set once | ||
* even if there are multiple parts for an attribute. | ||
* Writes attribute values to the DOM for a group of AttributeParts bound to a | ||
* single attibute. The value is only set once even if there are multiple parts | ||
* for an attribute. | ||
*/ | ||
export declare class AttributeCommitter { | ||
element: Element; | ||
name: string; | ||
strings: string[]; | ||
parts: AttributePart[]; | ||
readonly element: Element; | ||
readonly name: string; | ||
readonly strings: ReadonlyArray<string>; | ||
readonly parts: ReadonlyArray<AttributePart>; | ||
dirty: boolean; | ||
constructor(element: Element, name: string, strings: string[]); | ||
constructor(element: Element, name: string, strings: ReadonlyArray<string>); | ||
/** | ||
@@ -37,18 +39,29 @@ * Creates a single part. Override this to create a differnt type of part. | ||
} | ||
/** | ||
* A Part that controls all or part of an attribute value. | ||
*/ | ||
export declare class AttributePart implements Part { | ||
committer: AttributeCommitter; | ||
readonly committer: AttributeCommitter; | ||
value: unknown; | ||
constructor(comitter: AttributeCommitter); | ||
constructor(committer: AttributeCommitter); | ||
setValue(value: unknown): void; | ||
commit(): void; | ||
} | ||
/** | ||
* A Part that controls a location within a Node tree. Like a Range, NodePart | ||
* has start and end locations and can set and update the Nodes between those | ||
* locations. | ||
* | ||
* NodeParts support several value types: primitives, Nodes, TemplateResults, | ||
* as well as arrays and iterables of those types. | ||
*/ | ||
export declare class NodePart implements Part { | ||
options: RenderOptions; | ||
readonly options: RenderOptions; | ||
startNode: Node; | ||
endNode: Node; | ||
value: unknown; | ||
_pendingValue: unknown; | ||
private __pendingValue; | ||
constructor(options: RenderOptions); | ||
/** | ||
* Inserts this part into a container. | ||
* Appends this part into a container. | ||
* | ||
@@ -59,5 +72,5 @@ * This part must be empty, as its contents are not automatically moved. | ||
/** | ||
* Inserts this part between `ref` and `ref`'s next sibling. Both `ref` and | ||
* its next sibling must be static, unchanging nodes such as those that appear | ||
* in a literal section of a template. | ||
* Inserts this part after the `ref` node (between `ref` and `ref`'s next | ||
* sibling). Both `ref` and its next sibling must be static, unchanging nodes | ||
* such as those that appear in a literal section of a template. | ||
* | ||
@@ -74,3 +87,3 @@ * This part must be empty, as its contents are not automatically moved. | ||
/** | ||
* Appends this part after `ref` | ||
* Inserts this part after the `ref` part. | ||
* | ||
@@ -82,7 +95,7 @@ * This part must be empty, as its contents are not automatically moved. | ||
commit(): void; | ||
private _insert; | ||
private _commitNode; | ||
private _commitText; | ||
private _commitTemplateResult; | ||
private _commitIterable; | ||
private __insert; | ||
private __commitNode; | ||
private __commitText; | ||
private __commitTemplateResult; | ||
private __commitIterable; | ||
clear(startNode?: Node): void; | ||
@@ -98,8 +111,8 @@ } | ||
export declare class BooleanAttributePart implements Part { | ||
element: Element; | ||
name: string; | ||
strings: string[]; | ||
readonly element: Element; | ||
readonly name: string; | ||
readonly strings: ReadonlyArray<string>; | ||
value: unknown; | ||
_pendingValue: unknown; | ||
constructor(element: Element, name: string, strings: string[]); | ||
private __pendingValue; | ||
constructor(element: Element, name: string, strings: ReadonlyArray<string>); | ||
setValue(value: unknown): void; | ||
@@ -118,6 +131,6 @@ commit(): void; | ||
export declare class PropertyCommitter extends AttributeCommitter { | ||
single: boolean; | ||
constructor(element: Element, name: string, strings: string[]); | ||
readonly single: boolean; | ||
constructor(element: Element, name: string, strings: ReadonlyArray<string>); | ||
protected _createPart(): PropertyPart; | ||
_getValue(): unknown; | ||
protected _getValue(): unknown; | ||
commit(): void; | ||
@@ -129,9 +142,9 @@ } | ||
export declare class EventPart implements Part { | ||
element: Element; | ||
eventName: string; | ||
eventContext?: EventTarget; | ||
readonly element: Element; | ||
readonly eventName: string; | ||
readonly eventContext?: EventTarget; | ||
value: undefined | EventHandlerWithOptions; | ||
_options?: AddEventListenerOptions; | ||
_pendingValue: undefined | EventHandlerWithOptions; | ||
_boundHandleEvent: (event: Event) => void; | ||
private __options?; | ||
private __pendingValue; | ||
private readonly __boundHandleEvent; | ||
constructor(element: Element, eventName: string, eventContext?: EventTarget); | ||
@@ -138,0 +151,0 @@ setValue(value: undefined | EventHandlerWithOptions): void; |
136
lib/parts.js
@@ -27,5 +27,11 @@ /** | ||
}; | ||
export const isIterable = (value) => { | ||
return Array.isArray(value) || | ||
// tslint:disable-next-line:no-any | ||
!!(value && value[Symbol.iterator]); | ||
}; | ||
/** | ||
* Sets attribute values for AttributeParts, so that the value is only set once | ||
* even if there are multiple parts for an attribute. | ||
* Writes attribute values to the DOM for a group of AttributeParts bound to a | ||
* single attibute. The value is only set once even if there are multiple parts | ||
* for an attribute. | ||
*/ | ||
@@ -58,6 +64,6 @@ export class AttributeCommitter { | ||
const v = part.value; | ||
if (v != null && | ||
(Array.isArray(v) || | ||
// tslint:disable-next-line:no-any | ||
typeof v !== 'string' && v[Symbol.iterator])) { | ||
if (isPrimitive(v) || !isIterable(v)) { | ||
text += typeof v === 'string' ? v : String(v); | ||
} | ||
else { | ||
for (const t of v) { | ||
@@ -67,5 +73,2 @@ text += typeof t === 'string' ? t : String(t); | ||
} | ||
else { | ||
text += typeof v === 'string' ? v : String(v); | ||
} | ||
} | ||
@@ -83,6 +86,9 @@ } | ||
} | ||
/** | ||
* A Part that controls all or part of an attribute value. | ||
*/ | ||
export class AttributePart { | ||
constructor(comitter) { | ||
constructor(committer) { | ||
this.value = undefined; | ||
this.committer = comitter; | ||
this.committer = committer; | ||
} | ||
@@ -112,10 +118,18 @@ setValue(value) { | ||
} | ||
/** | ||
* A Part that controls a location within a Node tree. Like a Range, NodePart | ||
* has start and end locations and can set and update the Nodes between those | ||
* locations. | ||
* | ||
* NodeParts support several value types: primitives, Nodes, TemplateResults, | ||
* as well as arrays and iterables of those types. | ||
*/ | ||
export class NodePart { | ||
constructor(options) { | ||
this.value = undefined; | ||
this._pendingValue = undefined; | ||
this.__pendingValue = undefined; | ||
this.options = options; | ||
} | ||
/** | ||
* Inserts this part into a container. | ||
* Appends this part into a container. | ||
* | ||
@@ -129,5 +143,5 @@ * This part must be empty, as its contents are not automatically moved. | ||
/** | ||
* Inserts this part between `ref` and `ref`'s next sibling. Both `ref` and | ||
* its next sibling must be static, unchanging nodes such as those that appear | ||
* in a literal section of a template. | ||
* Inserts this part after the `ref` node (between `ref` and `ref`'s next | ||
* sibling). Both `ref` and its next sibling must be static, unchanging nodes | ||
* such as those that appear in a literal section of a template. | ||
* | ||
@@ -146,7 +160,7 @@ * This part must be empty, as its contents are not automatically moved. | ||
appendIntoPart(part) { | ||
part._insert(this.startNode = createMarker()); | ||
part._insert(this.endNode = createMarker()); | ||
part.__insert(this.startNode = createMarker()); | ||
part.__insert(this.endNode = createMarker()); | ||
} | ||
/** | ||
* Appends this part after `ref` | ||
* Inserts this part after the `ref` part. | ||
* | ||
@@ -156,3 +170,3 @@ * This part must be empty, as its contents are not automatically moved. | ||
insertAfterPart(ref) { | ||
ref._insert(this.startNode = createMarker()); | ||
ref.__insert(this.startNode = createMarker()); | ||
this.endNode = ref.endNode; | ||
@@ -162,11 +176,11 @@ ref.endNode = this.startNode; | ||
setValue(value) { | ||
this._pendingValue = value; | ||
this.__pendingValue = value; | ||
} | ||
commit() { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
while (isDirective(this.__pendingValue)) { | ||
const directive = this.__pendingValue; | ||
this.__pendingValue = noChange; | ||
directive(this); | ||
} | ||
const value = this._pendingValue; | ||
const value = this.__pendingValue; | ||
if (value === noChange) { | ||
@@ -177,15 +191,13 @@ return; | ||
if (value !== this.value) { | ||
this._commitText(value); | ||
this.__commitText(value); | ||
} | ||
} | ||
else if (value instanceof TemplateResult) { | ||
this._commitTemplateResult(value); | ||
this.__commitTemplateResult(value); | ||
} | ||
else if (value instanceof Node) { | ||
this._commitNode(value); | ||
this.__commitNode(value); | ||
} | ||
else if (Array.isArray(value) || | ||
// tslint:disable-next-line:no-any | ||
value[Symbol.iterator]) { | ||
this._commitIterable(value); | ||
else if (isIterable(value)) { | ||
this.__commitIterable(value); | ||
} | ||
@@ -198,9 +210,9 @@ else if (value === nothing) { | ||
// Fallback, will render the string representation | ||
this._commitText(value); | ||
this.__commitText(value); | ||
} | ||
} | ||
_insert(node) { | ||
__insert(node) { | ||
this.endNode.parentNode.insertBefore(node, this.endNode); | ||
} | ||
_commitNode(value) { | ||
__commitNode(value) { | ||
if (this.value === value) { | ||
@@ -210,6 +222,6 @@ return; | ||
this.clear(); | ||
this._insert(value); | ||
this.__insert(value); | ||
this.value = value; | ||
} | ||
_commitText(value) { | ||
__commitText(value) { | ||
const node = this.startNode.nextSibling; | ||
@@ -225,7 +237,7 @@ value = value == null ? '' : value; | ||
else { | ||
this._commitNode(document.createTextNode(typeof value === 'string' ? value : String(value))); | ||
this.__commitNode(document.createTextNode(typeof value === 'string' ? value : String(value))); | ||
} | ||
this.value = value; | ||
} | ||
_commitTemplateResult(value) { | ||
__commitTemplateResult(value) { | ||
const template = this.options.templateFactory(value); | ||
@@ -244,7 +256,7 @@ if (this.value instanceof TemplateInstance && | ||
instance.update(value.values); | ||
this._commitNode(fragment); | ||
this.__commitNode(fragment); | ||
this.value = instance; | ||
} | ||
} | ||
_commitIterable(value) { | ||
__commitIterable(value) { | ||
// For an Iterable, we create a new InstancePart per item, then set its | ||
@@ -306,3 +318,3 @@ // value to the item. This is a little bit of overhead for every item in | ||
this.value = undefined; | ||
this._pendingValue = undefined; | ||
this.__pendingValue = undefined; | ||
if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') { | ||
@@ -316,14 +328,14 @@ throw new Error('Boolean attributes can only contain a single expression'); | ||
setValue(value) { | ||
this._pendingValue = value; | ||
this.__pendingValue = value; | ||
} | ||
commit() { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
while (isDirective(this.__pendingValue)) { | ||
const directive = this.__pendingValue; | ||
this.__pendingValue = noChange; | ||
directive(this); | ||
} | ||
if (this._pendingValue === noChange) { | ||
if (this.__pendingValue === noChange) { | ||
return; | ||
} | ||
const value = !!this._pendingValue; | ||
const value = !!this.__pendingValue; | ||
if (this.value !== value) { | ||
@@ -336,5 +348,5 @@ if (value) { | ||
} | ||
this.value = value; | ||
} | ||
this.value = value; | ||
this._pendingValue = noChange; | ||
this.__pendingValue = noChange; | ||
} | ||
@@ -398,21 +410,21 @@ } | ||
this.value = undefined; | ||
this._pendingValue = undefined; | ||
this.__pendingValue = undefined; | ||
this.element = element; | ||
this.eventName = eventName; | ||
this.eventContext = eventContext; | ||
this._boundHandleEvent = (e) => this.handleEvent(e); | ||
this.__boundHandleEvent = (e) => this.handleEvent(e); | ||
} | ||
setValue(value) { | ||
this._pendingValue = value; | ||
this.__pendingValue = value; | ||
} | ||
commit() { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
while (isDirective(this.__pendingValue)) { | ||
const directive = this.__pendingValue; | ||
this.__pendingValue = noChange; | ||
directive(this); | ||
} | ||
if (this._pendingValue === noChange) { | ||
if (this.__pendingValue === noChange) { | ||
return; | ||
} | ||
const newListener = this._pendingValue; | ||
const newListener = this.__pendingValue; | ||
const oldListener = this.value; | ||
@@ -426,10 +438,10 @@ const shouldRemoveListener = newListener == null || | ||
if (shouldRemoveListener) { | ||
this.element.removeEventListener(this.eventName, this._boundHandleEvent, this._options); | ||
this.element.removeEventListener(this.eventName, this.__boundHandleEvent, this.__options); | ||
} | ||
if (shouldAddListener) { | ||
this._options = getOptions(newListener); | ||
this.element.addEventListener(this.eventName, this._boundHandleEvent, this._options); | ||
this.__options = getOptions(newListener); | ||
this.element.addEventListener(this.eventName, this.__boundHandleEvent, this.__options); | ||
} | ||
this.value = newListener; | ||
this._pendingValue = noChange; | ||
this.__pendingValue = noChange; | ||
} | ||
@@ -436,0 +448,0 @@ handleEvent(event) { |
@@ -19,5 +19,5 @@ /** | ||
export interface RenderOptions { | ||
templateFactory: TemplateFactory; | ||
eventContext?: EventTarget; | ||
readonly templateFactory: TemplateFactory; | ||
readonly eventContext?: EventTarget; | ||
} | ||
//# sourceMappingURL=render-options.d.ts.map |
@@ -76,3 +76,3 @@ /** | ||
*/ | ||
export declare const render: (result: TemplateResult, container: Element | DocumentFragment, options: ShadyRenderOptions) => void; | ||
export declare const render: (result: TemplateResult, container: Element | DocumentFragment | ShadowRoot, options: ShadyRenderOptions) => void; | ||
//# sourceMappingURL=shady-render.d.ts.map |
@@ -40,4 +40,4 @@ /** | ||
else if (typeof window.ShadyCSS.prepareTemplateDom === 'undefined') { | ||
console.warn(`Incompatible ShadyCSS version detected.` + | ||
`Please update to at least @webcomponents/webcomponentsjs@2.0.2 and` + | ||
console.warn(`Incompatible ShadyCSS version detected. ` + | ||
`Please update to at least @webcomponents/webcomponentsjs@2.0.2 and ` + | ||
`@webcomponents/shadycss@1.3.1.`); | ||
@@ -116,4 +116,5 @@ compatibleShadyCSSVersion = false; | ||
const styles = renderedDOM.querySelectorAll('style'); | ||
const { length } = styles; | ||
// If there are no styles, skip unnecessary work | ||
if (styles.length === 0) { | ||
if (length === 0) { | ||
// Ensure prepareTemplateStyles is called to support adding | ||
@@ -131,3 +132,3 @@ // styles via `prepareAdoptedCssText` since that requires that | ||
// currently does this anyway. When it does not, this should be changed. | ||
for (let i = 0; i < styles.length; i++) { | ||
for (let i = 0; i < length; i++) { | ||
const style = styles[i]; | ||
@@ -141,3 +142,4 @@ style.parentNode.removeChild(style); | ||
// `template`. | ||
insertNodeIntoTemplate(template, condensedStyle, template.element.content.firstChild); | ||
const content = template.element.content; | ||
insertNodeIntoTemplate(template, condensedStyle, content.firstChild); | ||
// Note, it's important that ShadyCSS gets the template that `lit-html` | ||
@@ -147,16 +149,18 @@ // will actually render so that it can update the style inside when | ||
window.ShadyCSS.prepareTemplateStyles(template.element, scopeName); | ||
if (window.ShadyCSS.nativeShadow) { | ||
// When in native Shadow DOM, re-add styling to rendered content using | ||
// the style ShadyCSS produced. | ||
const style = template.element.content.querySelector('style'); | ||
const style = content.querySelector('style'); | ||
if (window.ShadyCSS.nativeShadow && style !== null) { | ||
// When in native Shadow DOM, ensure the style created by ShadyCSS is | ||
// included in initially rendered output (`renderedDOM`). | ||
renderedDOM.insertBefore(style.cloneNode(true), renderedDOM.firstChild); | ||
} | ||
else { | ||
// When not in native Shadow DOM, at this point ShadyCSS will have | ||
// removed the style from the lit template and parts will be broken as a | ||
// When no style is left in the template, parts will be broken as a | ||
// result. To fix this, we put back the style node ShadyCSS removed | ||
// and then tell lit to remove that node from the template. | ||
// There can be no style in the template in 2 cases (1) when Shady DOM | ||
// is in use, ShadyCSS removes all styles, (2) when native Shadow DOM | ||
// is in use ShadyCSS removes the style if it contains no content. | ||
// NOTE, ShadyCSS creates its own style so we can safely add/remove | ||
// `condensedStyle` here. | ||
template.element.content.insertBefore(condensedStyle, template.element.content.firstChild); | ||
content.insertBefore(condensedStyle, content.firstChild); | ||
const removes = new Set(); | ||
@@ -226,4 +230,5 @@ removes.add(condensedStyle); | ||
const hasRendered = parts.has(container); | ||
const needsScoping = container instanceof ShadowRoot && | ||
compatibleShadyCSSVersion && result instanceof TemplateResult; | ||
const needsScoping = compatibleShadyCSSVersion && | ||
container.nodeType === 11 /* Node.DOCUMENT_FRAGMENT_NODE */ && | ||
!!container.host && result instanceof TemplateResult; | ||
// Handle first render to a scope specially... | ||
@@ -230,0 +235,0 @@ const firstScopeRender = needsScoping && !shadyRenderSet.has(scopeName); |
@@ -56,6 +56,6 @@ /** | ||
export declare type templateCache = { | ||
stringsArray: WeakMap<TemplateStringsArray, Template>; | ||
keyString: Map<string, Template>; | ||
readonly stringsArray: WeakMap<TemplateStringsArray, Template>; | ||
readonly keyString: Map<string, Template>; | ||
}; | ||
export declare const templateCaches: Map<string, templateCache>; | ||
//# sourceMappingURL=template-factory.d.ts.map |
@@ -14,3 +14,2 @@ /** | ||
*/ | ||
import { Part } from './part.js'; | ||
import { RenderOptions } from './render-options.js'; | ||
@@ -24,10 +23,10 @@ import { TemplateProcessor } from './template-processor.js'; | ||
export declare class TemplateInstance { | ||
_parts: Array<Part | undefined>; | ||
processor: TemplateProcessor; | ||
options: RenderOptions; | ||
template: Template; | ||
private readonly __parts; | ||
readonly processor: TemplateProcessor; | ||
readonly options: RenderOptions; | ||
readonly template: Template; | ||
constructor(template: Template, processor: TemplateProcessor, options: RenderOptions); | ||
update(values: unknown[]): void; | ||
update(values: ReadonlyArray<unknown>): void; | ||
_clone(): DocumentFragment; | ||
} | ||
//# sourceMappingURL=template-instance.d.ts.map |
@@ -25,3 +25,3 @@ /** | ||
constructor(template, processor, options) { | ||
this._parts = []; | ||
this.__parts = []; | ||
this.template = template; | ||
@@ -33,3 +33,3 @@ this.processor = processor; | ||
let i = 0; | ||
for (const part of this._parts) { | ||
for (const part of this.__parts) { | ||
if (part !== undefined) { | ||
@@ -40,3 +40,3 @@ part.setValue(values[i]); | ||
} | ||
for (const part of this._parts) { | ||
for (const part of this.__parts) { | ||
if (part !== undefined) { | ||
@@ -48,52 +48,87 @@ part.commit(); | ||
_clone() { | ||
// When using the Custom Elements polyfill, clone the node, rather than | ||
// importing it, to keep the fragment in the template's document. This | ||
// leaves the fragment inert so custom elements won't upgrade and | ||
// potentially modify their contents by creating a polyfilled ShadowRoot | ||
// while we traverse the tree. | ||
// There are a number of steps in the lifecycle of a template instance's | ||
// DOM fragment: | ||
// 1. Clone - create the instance fragment | ||
// 2. Adopt - adopt into the main document | ||
// 3. Process - find part markers and create parts | ||
// 4. Upgrade - upgrade custom elements | ||
// 5. Update - set node, attribute, property, etc., values | ||
// 6. Connect - connect to the document. Optional and outside of this | ||
// method. | ||
// | ||
// We have a few constraints on the ordering of these steps: | ||
// * We need to upgrade before updating, so that property values will pass | ||
// through any property setters. | ||
// * We would like to process before upgrading so that we're sure that the | ||
// cloned fragment is inert and not disturbed by self-modifying DOM. | ||
// * We want custom elements to upgrade even in disconnected fragments. | ||
// | ||
// Given these constraints, with full custom elements support we would | ||
// prefer the order: Clone, Process, Adopt, Upgrade, Update, Connect | ||
// | ||
// But Safari dooes not implement CustomElementRegistry#upgrade, so we | ||
// can not implement that order and still have upgrade-before-update and | ||
// upgrade disconnected fragments. So we instead sacrifice the | ||
// process-before-upgrade constraint, since in Custom Elements v1 elements | ||
// must not modify their light DOM in the constructor. We still have issues | ||
// when co-existing with CEv0 elements like Polymer 1, and with polyfills | ||
// that don't strictly adhere to the no-modification rule because shadow | ||
// DOM, which may be created in the constructor, is emulated by being placed | ||
// in the light DOM. | ||
// | ||
// The resulting order is on native is: Clone, Adopt, Upgrade, Process, | ||
// Update, Connect. document.importNode() performs Clone, Adopt, and Upgrade | ||
// in one step. | ||
// | ||
// The Custom Elements v1 polyfill supports upgrade(), so the order when | ||
// polyfilled is the more ideal: Clone, Process, Adopt, Upgrade, Update, | ||
// Connect. | ||
const fragment = isCEPolyfill ? | ||
this.template.element.content.cloneNode(true) : | ||
document.importNode(this.template.element.content, true); | ||
const stack = []; | ||
const parts = this.template.parts; | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
const walker = document.createTreeWalker(fragment, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false); | ||
let partIndex = 0; | ||
let nodeIndex = 0; | ||
const _prepareInstance = (fragment) => { | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be | ||
// null | ||
const walker = document.createTreeWalker(fragment, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false); | ||
let node = walker.nextNode(); | ||
// Loop through all the nodes and parts of a template | ||
while (partIndex < parts.length && node !== null) { | ||
const part = parts[partIndex]; | ||
// Consecutive Parts may have the same node index, in the case of | ||
// multiple bound attributes on an element. So each iteration we either | ||
// increment the nodeIndex, if we aren't on a node with a part, or the | ||
// partIndex if we are. By not incrementing the nodeIndex when we find a | ||
// part, we allow for the next part to be associated with the current | ||
// node if neccessasry. | ||
if (!isTemplatePartActive(part)) { | ||
this._parts.push(undefined); | ||
partIndex++; | ||
let part; | ||
let node = walker.nextNode(); | ||
// Loop through all the nodes and parts of a template | ||
while (partIndex < parts.length) { | ||
part = parts[partIndex]; | ||
if (!isTemplatePartActive(part)) { | ||
this.__parts.push(undefined); | ||
partIndex++; | ||
continue; | ||
} | ||
// Progress the tree walker until we find our next part's node. | ||
// Note that multiple parts may share the same node (attribute parts | ||
// on a single element), so this loop may not run at all. | ||
while (nodeIndex < part.index) { | ||
nodeIndex++; | ||
if (node.nodeName === 'TEMPLATE') { | ||
stack.push(node); | ||
walker.currentNode = node.content; | ||
} | ||
else if (nodeIndex === part.index) { | ||
if (part.type === 'node') { | ||
const part = this.processor.handleTextExpression(this.options); | ||
part.insertAfterNode(node.previousSibling); | ||
this._parts.push(part); | ||
} | ||
else { | ||
this._parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings, this.options)); | ||
} | ||
partIndex++; | ||
} | ||
else { | ||
nodeIndex++; | ||
if (node.nodeName === 'TEMPLATE') { | ||
_prepareInstance(node.content); | ||
} | ||
if ((node = walker.nextNode()) === null) { | ||
// We've exhausted the content inside a nested template element. | ||
// Because we still have parts (the outer for-loop), we know: | ||
// - There is a template in the stack | ||
// - The walker will find a nextNode outside the template | ||
walker.currentNode = stack.pop(); | ||
node = walker.nextNode(); | ||
} | ||
} | ||
}; | ||
_prepareInstance(fragment); | ||
// We've arrived at our part's node. | ||
if (part.type === 'node') { | ||
const part = this.processor.handleTextExpression(this.options); | ||
part.insertAfterNode(node.previousSibling); | ||
this.__parts.push(part); | ||
} | ||
else { | ||
this.__parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings, this.options)); | ||
} | ||
partIndex++; | ||
} | ||
if (isCEPolyfill) { | ||
@@ -100,0 +135,0 @@ document.adoptNode(fragment); |
@@ -30,3 +30,3 @@ /** | ||
*/ | ||
handleAttributeExpressions(element: Element, name: string, strings: string[], options: RenderOptions): Part[]; | ||
handleAttributeExpressions(element: Element, name: string, strings: ReadonlyArray<string>, options: RenderOptions): ReadonlyArray<Part>; | ||
/** | ||
@@ -33,0 +33,0 @@ * Create parts for a text-position binding. |
@@ -20,7 +20,7 @@ /** | ||
export declare class TemplateResult { | ||
strings: TemplateStringsArray; | ||
values: unknown[]; | ||
type: string; | ||
processor: TemplateProcessor; | ||
constructor(strings: TemplateStringsArray, values: unknown[], type: string, processor: TemplateProcessor); | ||
readonly strings: TemplateStringsArray; | ||
readonly values: ReadonlyArray<unknown>; | ||
readonly type: string; | ||
readonly processor: TemplateProcessor; | ||
constructor(strings: TemplateStringsArray, values: ReadonlyArray<unknown>, type: string, processor: TemplateProcessor); | ||
/** | ||
@@ -35,3 +35,3 @@ * Returns a string of HTML used to create a `<template>` element. | ||
* | ||
* This class wraps HTMl in an `<svg>` tag in order to parse its contents in the | ||
* This class wraps HTML in an `<svg>` tag in order to parse its contents in the | ||
* SVG namespace, then modifies the template to remove the `<svg>` tag so that | ||
@@ -38,0 +38,0 @@ * clones only container the original fragment. |
@@ -34,30 +34,53 @@ /** | ||
getHTML() { | ||
const endIndex = this.strings.length - 1; | ||
const l = this.strings.length - 1; | ||
let html = ''; | ||
for (let i = 0; i < endIndex; i++) { | ||
let isCommentBinding = false; | ||
for (let i = 0; i < l; i++) { | ||
const s = this.strings[i]; | ||
// This exec() call does two things: | ||
// 1) Appends a suffix to the bound attribute name to opt out of special | ||
// attribute value parsing that IE11 and Edge do, like for style and | ||
// many SVG attributes. The Template class also appends the same suffix | ||
// when looking up attributes to create Parts. | ||
// 2) Adds an unquoted-attribute-safe marker for the first expression in | ||
// an attribute. Subsequent attribute expressions will use node markers, | ||
// and this is safe since attributes with multiple expressions are | ||
// guaranteed to be quoted. | ||
const match = lastAttributeNameRegex.exec(s); | ||
if (match) { | ||
// We're starting a new bound attribute. | ||
// Add the safe attribute suffix, and use unquoted-attribute-safe | ||
// marker. | ||
html += s.substr(0, match.index) + match[1] + match[2] + | ||
boundAttributeSuffix + match[3] + marker; | ||
// For each binding we want to determine the kind of marker to insert | ||
// into the template source before it's parsed by the browser's HTML | ||
// parser. The marker type is based on whether the expression is in an | ||
// attribute, text, or comment poisition. | ||
// * For node-position bindings we insert a comment with the marker | ||
// sentinel as its text content, like <!--{{lit-guid}}-->. | ||
// * For attribute bindings we insert just the marker sentinel for the | ||
// first binding, so that we support unquoted attribute bindings. | ||
// Subsequent bindings can use a comment marker because multi-binding | ||
// attributes must be quoted. | ||
// * For comment bindings we insert just the marker sentinel so we don't | ||
// close the comment. | ||
// | ||
// The following code scans the template source, but is *not* an HTML | ||
// parser. We don't need to track the tree structure of the HTML, only | ||
// whether a binding is inside a comment, and if not, if it appears to be | ||
// the first binding in an attribute. | ||
const commentOpen = s.lastIndexOf('<!--'); | ||
// We're in comment position if we have a comment open with no following | ||
// comment close. Because <-- can appear in an attribute value there can | ||
// be false positives. | ||
isCommentBinding = (commentOpen > -1 || isCommentBinding) && | ||
s.indexOf('-->', commentOpen + 1) === -1; | ||
// Check to see if we have an attribute-like sequence preceeding the | ||
// expression. This can match "name=value" like structures in text, | ||
// comments, and attribute values, so there can be false-positives. | ||
const attributeMatch = lastAttributeNameRegex.exec(s); | ||
if (attributeMatch === null) { | ||
// We're only in this branch if we don't have a attribute-like | ||
// preceeding sequence. For comments, this guards against unusual | ||
// attribute values like <div foo="<!--${'bar'}">. Cases like | ||
// <!-- foo=${'bar'}--> are handled correctly in the attribute branch | ||
// below. | ||
html += s + (isCommentBinding ? marker : nodeMarker); | ||
} | ||
else { | ||
// We're either in a bound node, or trailing bound attribute. | ||
// Either way, nodeMarker is safe to use. | ||
html += s + nodeMarker; | ||
// For attributes we use just a marker sentinel, and also append a | ||
// $lit$ suffix to the name to opt-out of attribute-specific parsing | ||
// that IE and Edge do for style and certain SVG attributes. | ||
html += s.substr(0, attributeMatch.index) + attributeMatch[1] + | ||
attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] + | ||
marker; | ||
} | ||
} | ||
return html + this.strings[endIndex]; | ||
html += this.strings[l]; | ||
return html; | ||
} | ||
@@ -73,3 +96,3 @@ getTemplateElement() { | ||
* | ||
* This class wraps HTMl in an `<svg>` tag in order to parse its contents in the | ||
* This class wraps HTML in an `<svg>` tag in order to parse its contents in the | ||
* SVG namespace, then modifies the template to remove the `<svg>` tag so that | ||
@@ -76,0 +99,0 @@ * clones only container the original fragment. |
@@ -37,4 +37,4 @@ /** | ||
export declare class Template { | ||
parts: TemplatePart[]; | ||
element: HTMLTemplateElement; | ||
readonly parts: TemplatePart[]; | ||
readonly element: HTMLTemplateElement; | ||
constructor(result: TemplateResult, element: HTMLTemplateElement); | ||
@@ -59,9 +59,9 @@ } | ||
export declare type TemplatePart = { | ||
type: 'node'; | ||
readonly type: 'node'; | ||
index: number; | ||
} | { | ||
type: 'attribute'; | ||
readonly type: 'attribute'; | ||
index: number; | ||
name: string; | ||
strings: string[]; | ||
readonly name: string; | ||
readonly strings: ReadonlyArray<string>; | ||
}; | ||
@@ -77,9 +77,10 @@ export declare const isTemplatePartActive: (part: TemplatePart) => boolean; | ||
* See attributes in the HTML spec: | ||
* https://www.w3.org/TR/html5/syntax.html#attributes-0 | ||
* https://www.w3.org/TR/html5/syntax.html#elements-attributes | ||
* | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters | ||
* | ||
* " \x09\x0a\x0c\x0d" are HTML space characters: | ||
* https://www.w3.org/TR/html5/infrastructure.html#space-character | ||
* https://www.w3.org/TR/html5/infrastructure.html#space-characters | ||
* | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every | ||
* space character except " ". | ||
* | ||
* So an attribute is: | ||
@@ -86,0 +87,0 @@ * * The name: any character except a control character, space character, ('), |
@@ -36,119 +36,138 @@ /** | ||
this.element = element; | ||
const nodesToRemove = []; | ||
const stack = []; | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
const walker = document.createTreeWalker(element.content, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false); | ||
// Keeps track of the last index associated with a part. We try to delete | ||
// unnecessary nodes, but we never want to associate two different parts | ||
// to the same index. They must have a constant node between. | ||
let lastPartIndex = 0; | ||
let index = -1; | ||
let partIndex = 0; | ||
const nodesToRemove = []; | ||
const _prepareTemplate = (template) => { | ||
const content = template.content; | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be | ||
// null | ||
const walker = document.createTreeWalker(content, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false); | ||
// Keeps track of the last index associated with a part. We try to delete | ||
// unnecessary nodes, but we never want to associate two different parts | ||
// to the same index. They must have a constant node between. | ||
let lastPartIndex = 0; | ||
while (walker.nextNode()) { | ||
index++; | ||
const node = walker.currentNode; | ||
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { | ||
if (node.hasAttributes()) { | ||
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++) { | ||
if (attributes[i].value.indexOf(marker) >= 0) { | ||
count++; | ||
} | ||
const { strings, values: { length } } = result; | ||
while (partIndex < length) { | ||
const node = walker.nextNode(); | ||
if (node === null) { | ||
// We've exhausted the content inside a nested template element. | ||
// Because we still have parts (the outer for-loop), we know: | ||
// - There is a template in the stack | ||
// - The walker will find a nextNode outside the template | ||
walker.currentNode = stack.pop(); | ||
continue; | ||
} | ||
index++; | ||
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { | ||
if (node.hasAttributes()) { | ||
const attributes = node.attributes; | ||
const { length } = 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 correspondence between part index and attribute index. | ||
let count = 0; | ||
for (let i = 0; i < length; i++) { | ||
if (endsWith(attributes[i].name, boundAttributeSuffix)) { | ||
count++; | ||
} | ||
while (count-- > 0) { | ||
// Get the template literal section leading up to the first | ||
// expression in this attribute | ||
const stringForPart = result.strings[partIndex]; | ||
// Find the attribute name | ||
const name = lastAttributeNameRegex.exec(stringForPart)[2]; | ||
// Find the corresponding attribute | ||
// All bound attributes have had a suffix added in | ||
// TemplateResult#getHTML to opt out of special attribute | ||
// handling. To look up the attribute value we also need to add | ||
// the suffix. | ||
const attributeLookupName = name.toLowerCase() + boundAttributeSuffix; | ||
const attributeValue = node.getAttribute(attributeLookupName); | ||
const strings = attributeValue.split(markerRegex); | ||
this.parts.push({ type: 'attribute', index, name, strings }); | ||
node.removeAttribute(attributeLookupName); | ||
partIndex += strings.length - 1; | ||
} | ||
} | ||
if (node.tagName === 'TEMPLATE') { | ||
_prepareTemplate(node); | ||
while (count-- > 0) { | ||
// Get the template literal section leading up to the first | ||
// expression in this attribute | ||
const stringForPart = strings[partIndex]; | ||
// Find the attribute name | ||
const name = lastAttributeNameRegex.exec(stringForPart)[2]; | ||
// Find the corresponding attribute | ||
// All bound attributes have had a suffix added in | ||
// TemplateResult#getHTML to opt out of special attribute | ||
// handling. To look up the attribute value we also need to add | ||
// the suffix. | ||
const attributeLookupName = name.toLowerCase() + boundAttributeSuffix; | ||
const attributeValue = node.getAttribute(attributeLookupName); | ||
node.removeAttribute(attributeLookupName); | ||
const statics = attributeValue.split(markerRegex); | ||
this.parts.push({ type: 'attribute', index, name, strings: statics }); | ||
partIndex += statics.length - 1; | ||
} | ||
} | ||
else if (node.nodeType === 3 /* Node.TEXT_NODE */) { | ||
const data = node.data; | ||
if (data.indexOf(marker) >= 0) { | ||
const parent = node.parentNode; | ||
const strings = data.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
// Generate a new text node for each literal section | ||
// These nodes are also used as the markers for node parts | ||
for (let i = 0; i < lastIndex; i++) { | ||
parent.insertBefore((strings[i] === '') ? createMarker() : | ||
document.createTextNode(strings[i]), node); | ||
this.parts.push({ type: 'node', index: ++index }); | ||
if (node.tagName === 'TEMPLATE') { | ||
stack.push(node); | ||
walker.currentNode = node.content; | ||
} | ||
} | ||
else if (node.nodeType === 3 /* Node.TEXT_NODE */) { | ||
const data = node.data; | ||
if (data.indexOf(marker) >= 0) { | ||
const parent = node.parentNode; | ||
const strings = data.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
// Generate a new text node for each literal section | ||
// These nodes are also used as the markers for node parts | ||
for (let i = 0; i < lastIndex; i++) { | ||
let insert; | ||
let s = strings[i]; | ||
if (s === '') { | ||
insert = createMarker(); | ||
} | ||
// If there's no text, we must insert a comment to mark our place. | ||
// Else, we can trust it will stick around after cloning. | ||
if (strings[lastIndex] === '') { | ||
parent.insertBefore(createMarker(), node); | ||
nodesToRemove.push(node); | ||
} | ||
else { | ||
node.data = strings[lastIndex]; | ||
const match = lastAttributeNameRegex.exec(s); | ||
if (match !== null && endsWith(match[2], boundAttributeSuffix)) { | ||
s = s.slice(0, match.index) + match[1] + | ||
match[2].slice(0, -boundAttributeSuffix.length) + match[3]; | ||
} | ||
insert = document.createTextNode(s); | ||
} | ||
// We have a part for each match found | ||
partIndex += lastIndex; | ||
parent.insertBefore(insert, node); | ||
this.parts.push({ type: 'node', index: ++index }); | ||
} | ||
// If there's no text, we must insert a comment to mark our place. | ||
// Else, we can trust it will stick around after cloning. | ||
if (strings[lastIndex] === '') { | ||
parent.insertBefore(createMarker(), node); | ||
nodesToRemove.push(node); | ||
} | ||
else { | ||
node.data = strings[lastIndex]; | ||
} | ||
// We have a part for each match found | ||
partIndex += lastIndex; | ||
} | ||
else if (node.nodeType === 8 /* Node.COMMENT_NODE */) { | ||
if (node.data === marker) { | ||
const parent = node.parentNode; | ||
// 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 | ||
// * The previousSibling is already the start of a previous part | ||
if (node.previousSibling === null || index === lastPartIndex) { | ||
index++; | ||
parent.insertBefore(createMarker(), node); | ||
} | ||
lastPartIndex = index; | ||
this.parts.push({ type: 'node', index }); | ||
// If we don't have a nextSibling, keep this node so we have an end. | ||
// Else, we can remove it to save future costs. | ||
if (node.nextSibling === null) { | ||
node.data = ''; | ||
} | ||
else { | ||
nodesToRemove.push(node); | ||
index--; | ||
} | ||
partIndex++; | ||
} | ||
else if (node.nodeType === 8 /* Node.COMMENT_NODE */) { | ||
if (node.data === marker) { | ||
const parent = node.parentNode; | ||
// 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 | ||
// * The previousSibling is already the start of a previous part | ||
if (node.previousSibling === null || index === lastPartIndex) { | ||
index++; | ||
parent.insertBefore(createMarker(), node); | ||
} | ||
lastPartIndex = index; | ||
this.parts.push({ type: 'node', index }); | ||
// If we don't have a nextSibling, keep this node so we have an end. | ||
// Else, we can remove it to save future costs. | ||
if (node.nextSibling === null) { | ||
node.data = ''; | ||
} | ||
else { | ||
let i = -1; | ||
while ((i = node.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: 'node', index: -1 }); | ||
} | ||
nodesToRemove.push(node); | ||
index--; | ||
} | ||
partIndex++; | ||
} | ||
else { | ||
let i = -1; | ||
while ((i = node.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: 'node', index: -1 }); | ||
partIndex++; | ||
} | ||
} | ||
} | ||
}; | ||
_prepareTemplate(element); | ||
} | ||
// Remove text binding nodes after the walk to not disturb the TreeWalker | ||
@@ -160,2 +179,6 @@ for (const n of nodesToRemove) { | ||
} | ||
const endsWith = (str, suffix) => { | ||
const index = str.length - suffix.length; | ||
return index >= 0 && str.slice(index) === suffix; | ||
}; | ||
export const isTemplatePartActive = (part) => part.index !== -1; | ||
@@ -172,9 +195,10 @@ // Allows `document.createComment('')` to be renamed for a | ||
* See attributes in the HTML spec: | ||
* https://www.w3.org/TR/html5/syntax.html#attributes-0 | ||
* https://www.w3.org/TR/html5/syntax.html#elements-attributes | ||
* | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters | ||
* | ||
* " \x09\x0a\x0c\x0d" are HTML space characters: | ||
* https://www.w3.org/TR/html5/infrastructure.html#space-character | ||
* https://www.w3.org/TR/html5/infrastructure.html#space-characters | ||
* | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every | ||
* space character except " ". | ||
* | ||
* So an attribute is: | ||
@@ -191,3 +215,3 @@ * * The name: any character except a control character, space character, ('), | ||
*/ | ||
export const lastAttributeNameRegex = /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F \x09\x0a\x0c\x0d"'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/; | ||
export const lastAttributeNameRegex = /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/; | ||
//# sourceMappingURL=template.js.map |
@@ -19,3 +19,3 @@ /** | ||
export { noChange, nothing, Part } from './lib/part.js'; | ||
export { AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isPrimitive, NodePart, PropertyCommitter, PropertyPart } from './lib/parts.js'; | ||
export { AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isIterable, isPrimitive, NodePart, PropertyCommitter, PropertyPart } from './lib/parts.js'; | ||
export { RenderOptions } from './lib/render-options.js'; | ||
@@ -22,0 +22,0 @@ export { parts, render } from './lib/render.js'; |
@@ -38,3 +38,3 @@ /** | ||
export { noChange, nothing } from './lib/part.js'; | ||
export { AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isPrimitive, NodePart, PropertyCommitter, PropertyPart } from './lib/parts.js'; | ||
export { AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isIterable, isPrimitive, NodePart, PropertyCommitter, PropertyPart } from './lib/parts.js'; | ||
export { parts, render } from './lib/render.js'; | ||
@@ -41,0 +41,0 @@ export { templateCaches, templateFactory } from './lib/template-factory.js'; |
{ | ||
"name": "lit-html", | ||
"version": "1.0.0", | ||
"version": "1.0.1-dev.58f394e", | ||
"description": "HTML template literals in JavaScript", | ||
"license": "BSD-3-Clause", | ||
"homepage": "https://lit-html.polymer-project.org/", | ||
"repository": "Polymer/lit-html", | ||
@@ -41,2 +42,3 @@ "main": "lit-html.js", | ||
"clang-format": "^1.2.4", | ||
"lit-html-benchmarks": "^0.2.0", | ||
"mocha": "^5.2.0", | ||
@@ -46,4 +48,5 @@ "rollup": "^0.64.1", | ||
"rollup-plugin-terser": "^1.0.1", | ||
"tachometer": "^0.4.0", | ||
"tslint": "^5.11.0", | ||
"typescript": "^3.2.2", | ||
"typescript": "^3.4.1", | ||
"uglify-es": "^3.3.5", | ||
@@ -50,0 +53,0 @@ "wct-mocha": "^1.0.0", |
@@ -14,2 +14,3 @@ /** | ||
*/ | ||
import { removeNodes, reparentNodes } from '../lib/dom.js'; | ||
/** | ||
@@ -25,22 +26,26 @@ * A lightweight <template> polyfill that supports minimum features to cover | ||
export const initTemplatePolyfill = (forced = false) => { | ||
if (typeof HTMLTemplateElement !== 'undefined' && !forced) { | ||
// Minimal polyfills (like this one) may provide only a subset of Template's | ||
// functionality. So, we explicitly check that at least content is present to | ||
// prevent installing patching with multiple polyfills, which might happen if | ||
// multiple versions of lit-html were included on a page. | ||
if (!forced && 'content' in document.createElement('template')) { | ||
return; | ||
} | ||
const contentDoc = document.implementation.createHTMLDocument('template'); | ||
// tslint:disable-next-line:no-any | ||
const body = contentDoc.body; | ||
const descriptor = { | ||
enumerable: true, | ||
configurable: true, | ||
}; | ||
const upgrade = (template) => { | ||
template.content = contentDoc.createDocumentFragment(); | ||
Object.defineProperty(template, 'innerHTML', { | ||
set: function (text) { | ||
contentDoc.body.innerHTML = text; | ||
const content = this.content; | ||
while (content.firstChild) { | ||
content.removeChild(content.firstChild); | ||
} | ||
const body = contentDoc.body; | ||
while (body.firstChild) { | ||
content.appendChild(body.firstChild); | ||
} | ||
}, | ||
configurable: true | ||
const content = contentDoc.createDocumentFragment(); | ||
Object.defineProperties(template, { | ||
content: Object.assign({}, descriptor, { get() { | ||
return content; | ||
} }), | ||
innerHTML: Object.assign({}, descriptor, { set: function (text) { | ||
body.innerHTML = text; | ||
removeNodes(content, content.firstChild); | ||
reparentNodes(content, body.firstChild); | ||
} }), | ||
}); | ||
@@ -50,5 +55,4 @@ }; | ||
Document.prototype.createElement = function createElement(tagName, options) { | ||
let el = capturedCreateElement.call(this, tagName, options); | ||
if (el.localName === 'template') { | ||
el = capturedCreateElement.call(this, 'div'); | ||
const el = capturedCreateElement.call(this, tagName, options); | ||
if (el.tagName === 'TEMPLATE') { | ||
upgrade(el); | ||
@@ -55,0 +59,0 @@ } |
@@ -44,9 +44,4 @@ # lit-html | ||
## Status | ||
`lit-html` is under active development and has not yet had a 1.0 release. The | ||
internal API may still change somewhat. The `html` and `render` API is stable. | ||
## Contributing | ||
Please see [CONTRIBUTING.md](./CONTRIBUTING.md). |
@@ -20,4 +20,4 @@ /** | ||
type CachedTemplate = { | ||
instance: TemplateInstance, | ||
nodes: DocumentFragment | ||
readonly instance: TemplateInstance, | ||
readonly nodes: DocumentFragment | ||
}; | ||
@@ -24,0 +24,0 @@ const templateCaches = |
@@ -18,18 +18,4 @@ /** | ||
// On IE11, classList.toggle doesn't accept a second argument. | ||
// Since this is so minor, we just polyfill it. | ||
if (window.navigator.userAgent.match('Trident')) { | ||
DOMTokenList.prototype.toggle = function( | ||
token: string, force?: boolean|undefined) { | ||
if (force === undefined || force) { | ||
this.add(token); | ||
} else { | ||
this.remove(token); | ||
} | ||
return force === undefined ? true : force; | ||
}; | ||
} | ||
export interface ClassInfo { | ||
[name: string]: string|boolean|number; | ||
readonly [name: string]: string|boolean|number; | ||
} | ||
@@ -44,10 +30,2 @@ | ||
/** | ||
* Stores AttributeParts that have had static classes applied (e.g. `foo` in | ||
* class="foo ${classMap()}"). Static classes are applied only the first time | ||
* the directive is run on a part. | ||
*/ | ||
// Note, could be a WeakSet, but prefer not requiring this polyfill. | ||
const classMapStatics = new WeakMap(); | ||
/** | ||
* A directive that applies CSS classes. This must be used in the `class` | ||
@@ -69,7 +47,13 @@ * attribute and must be the only part used in the attribute. It takes each | ||
} | ||
const {committer} = part; | ||
const {element} = committer; | ||
// handle static classes | ||
if (!classMapStatics.has(part)) { | ||
part.committer.element.className = part.committer.strings.join(' '); | ||
classMapStatics.set(part, true); | ||
if (!classMapCache.has(part)) { | ||
element.className = committer.strings.join(' '); | ||
} | ||
const {classList} = element; | ||
// remove old classes that no longer apply | ||
@@ -79,11 +63,14 @@ const oldInfo = classMapCache.get(part); | ||
if (!(name in classInfo)) { | ||
part.committer.element.classList.remove(name); | ||
classList.remove(name); | ||
} | ||
} | ||
// add new classes | ||
for (const name in classInfo) { | ||
if (!oldInfo || (oldInfo[name] !== classInfo[name])) { | ||
const value = classInfo[name]; | ||
if (!oldInfo || value !== oldInfo[name]) { | ||
// We explicitly want a loose truthy check here because | ||
// it seems more convenient that '' and 0 are skipped. | ||
part.committer.element.classList.toggle(name, Boolean(classInfo[name])); | ||
const method = value ? 'add' : 'remove'; | ||
classList[method](name); | ||
} | ||
@@ -90,0 +77,0 @@ } |
@@ -145,11 +145,10 @@ /** | ||
// * 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): | ||
// 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): | ||
// | ||
@@ -160,14 +159,12 @@ // oldHead v v oldTail | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new | ||
// item order | ||
// 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. | ||
// 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 | ||
// 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 | ||
@@ -179,11 +176,10 @@ // next diagram). | ||
// newParts: [0, , , , , , ] <- heads matched: update 0 | ||
// and newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldHead | ||
// & newHead | ||
// 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. | ||
// * 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. | ||
// | ||
@@ -193,107 +189,98 @@ // oldHead v v oldTail | ||
// newParts: [0, , , , , , 6] <- tails matched: update 6 | ||
// and newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldTail | ||
// & newTail | ||
// 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. | ||
// 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. | ||
// 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 | ||
// 5 and newKeys: [0, 2, 1, 4, 3, 7, 6] advance oldTail | ||
// 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. | ||
// 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. | ||
// 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 | ||
// into place newKeys: [0, 2, 1, 4, 3, 7, 6] and advance | ||
// newHead | ||
// 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. | ||
// 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... | ||
// 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. | ||
// 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 | ||
// and newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldHead | ||
// & newHead | ||
// 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. | ||
// 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`. | ||
// newParts), so advance `oldHead`. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] // old head already used; | ||
// advance newParts: [0, 2, 1, , , , 6] // oldHead newKeys: | ||
// [0, 2, 1, 4, 3, 7, 6] | ||
// 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. | ||
// 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. | ||
// 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. | ||
// | ||
@@ -303,10 +290,9 @@ // oldHead v v oldTail | ||
// newParts: [0, 2, 1, 4, , , 6] <- old tail matches new | ||
// head: update newKeys: [0, 2, 1, 4, 3, 7, 6] & move 4, | ||
// advance oldTail & newHead | ||
// 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. | ||
// old head part in place, and advance the `oldHead` and | ||
// `newHead` pointers. | ||
// | ||
@@ -316,16 +302,14 @@ // oldHead v oldTail | ||
// newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3 | ||
// and advance newKeys: [0, 2, 1, 4, 3, 7, 6] oldHead & | ||
// newHead | ||
// 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. | ||
// 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. | ||
// with the main loop. Create the remaining part and insert | ||
// it at the new head position, and the update is complete. | ||
// | ||
@@ -338,22 +322,22 @@ // (oldHead > oldTail) | ||
// | ||
// * 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. | ||
// * 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. | ||
// 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. | ||
@@ -446,4 +430,4 @@ while (oldHead <= oldTail && newHead <= newTail) { | ||
// tail, since old pointers are no longer valid | ||
const newPart = createAndInsertPart( | ||
containerPart, newParts[newTail + 1]!); | ||
const newPart = | ||
createAndInsertPart(containerPart, newParts[newTail + 1]); | ||
updatePart(newPart, newValues[newHead]); | ||
@@ -450,0 +434,0 @@ newParts[newHead++] = newPart; |
@@ -18,3 +18,3 @@ /** | ||
export interface StyleInfo { | ||
[name: string]: string; | ||
readonly [name: string]: string; | ||
} | ||
@@ -29,10 +29,2 @@ | ||
/** | ||
* Stores AttributeParts that have had static styles applied (e.g. `height: 0;` | ||
* in style="height: 0; ${styleMap()}"). Static styles are applied only the | ||
* first time the directive is run on a part. | ||
*/ | ||
// Note, could be a WeakSet, but prefer not requiring this polyfill. | ||
const styleMapStatics = new WeakMap<AttributePart, true>(); | ||
/** | ||
* A directive that applies CSS properties to an element. | ||
@@ -62,9 +54,9 @@ * | ||
const {committer} = part; | ||
const {style} = committer.element as HTMLElement; | ||
// Handle static styles the first time we see a Part | ||
if (!styleMapStatics.has(part)) { | ||
(part.committer.element as HTMLElement).style.cssText = | ||
part.committer.strings.join(' '); | ||
styleMapStatics.set(part, true); | ||
if (!styleMapCache.has(part)) { | ||
style.cssText = committer.strings.join(' '); | ||
} | ||
const style = (part.committer.element as HTMLElement).style; | ||
@@ -71,0 +63,0 @@ // Remove old properties that no longer exist in styleInfo |
@@ -19,4 +19,4 @@ /** | ||
interface PreviousValue { | ||
value: unknown; | ||
fragment: DocumentFragment; | ||
readonly value: unknown; | ||
readonly fragment: DocumentFragment; | ||
} | ||
@@ -23,0 +23,0 @@ |
@@ -23,3 +23,3 @@ /** | ||
*/ | ||
lastRenderedIndex?: number; | ||
lastRenderedIndex: number; | ||
@@ -30,2 +30,4 @@ values: unknown[]; | ||
const _state = new WeakMap<Part, AsyncState>(); | ||
// Effectively infinity, but a SMI. | ||
const _infinity = 0x7fffffff; | ||
@@ -55,2 +57,3 @@ /** | ||
state = { | ||
lastRenderedIndex: _infinity, | ||
values: [], | ||
@@ -61,2 +64,3 @@ }; | ||
const previousValues = state.values; | ||
let previousLength = previousValues.length; | ||
state.values = args; | ||
@@ -66,3 +70,3 @@ | ||
// If we've rendered a higher-priority value already, stop. | ||
if (state.lastRenderedIndex !== undefined && i > state.lastRenderedIndex) { | ||
if (i > state.lastRenderedIndex) { | ||
break; | ||
@@ -84,5 +88,3 @@ } | ||
// If this is a Promise we've already handled, skip it. | ||
if (state.lastRenderedIndex !== undefined && | ||
typeof (value as {then?: unknown}).then === 'function' && | ||
value === previousValues[i]) { | ||
if (i < previousLength && value === previousValues[i]) { | ||
continue; | ||
@@ -93,3 +95,4 @@ } | ||
// changed. Forget what we rendered before. | ||
state.lastRenderedIndex = undefined; | ||
state.lastRenderedIndex = _infinity; | ||
previousLength = 0; | ||
@@ -101,5 +104,3 @@ Promise.resolve(value).then((resolvedValue: unknown) => { | ||
// higher-priority than what's already been rendered. | ||
if (index > -1 && | ||
(state.lastRenderedIndex === undefined || | ||
index < state.lastRenderedIndex)) { | ||
if (index > -1 && index < state.lastRenderedIndex) { | ||
state.lastRenderedIndex = index; | ||
@@ -106,0 +107,0 @@ part.setValue(resolvedValue); |
@@ -18,7 +18,1 @@ interface ShadyCSS { | ||
} | ||
/** Allows code to check `instanceof ShadowRoot`. */ | ||
declare interface ShadowRootConstructor { | ||
new(): ShadowRoot; | ||
} | ||
declare const ShadowRoot: ShadowRootConstructor; |
@@ -39,7 +39,7 @@ /** | ||
element: Element, name: string, strings: string[], | ||
options: RenderOptions): Part[] { | ||
options: RenderOptions): ReadonlyArray<Part> { | ||
const prefix = name[0]; | ||
if (prefix === '.') { | ||
const comitter = new PropertyCommitter(element, name.slice(1), strings); | ||
return comitter.parts; | ||
const committer = new PropertyCommitter(element, name.slice(1), strings); | ||
return committer.parts; | ||
} | ||
@@ -52,4 +52,4 @@ if (prefix === '@') { | ||
} | ||
const comitter = new AttributeCommitter(element, name, strings); | ||
return comitter.parts; | ||
const committer = new AttributeCommitter(element, name, strings); | ||
return committer.parts; | ||
} | ||
@@ -56,0 +56,0 @@ /** |
@@ -29,12 +29,33 @@ /** | ||
/** | ||
* Brands a function as a directive so that lit-html will call the function | ||
* during template rendering, rather than passing as a value. | ||
* Brands a function as a directive factory function so that lit-html will call | ||
* the function during template rendering, rather than passing as a value. | ||
* | ||
* A _directive_ is a function that takes a Part as an argument. It has the | ||
* signature: `(part: Part) => void`. | ||
* | ||
* A directive _factory_ is a function that takes arguments for data and | ||
* configuration and returns a directive. Users of directive usually refer to | ||
* the directive factory as the directive. For example, "The repeat directive". | ||
* | ||
* Usually a template author will invoke a directive factory in their template | ||
* with relevant arguments, which will then return a directive function. | ||
* | ||
* Here's an example of using the `repeat()` directive factory that takes an | ||
* array and a function to render an item: | ||
* | ||
* ```js | ||
* html`<ul><${repeat(items, (item) => html`<li>${item}</li>`)}</ul>` | ||
* ``` | ||
* | ||
* When `repeat` is invoked, it returns a directive function that closes over | ||
* `items` and the template function. When the outer template is rendered, the | ||
* return directive function is called with the Part for the expression. | ||
* `repeat` then performs it's custom logic to render multiple items. | ||
* | ||
* @param f The directive factory function. Must be a function that returns a | ||
* function of the signature `(part: Part) => void`. The returned function will | ||
* be called with the part object | ||
* be called with the part object. | ||
* | ||
* @example | ||
* | ||
* ``` | ||
* import {directive, html} from 'lit-html'; | ||
@@ -47,5 +68,3 @@ * | ||
* }); | ||
* ``` | ||
*/ | ||
// tslint:disable-next-line:no-any | ||
export const directive = <F extends DirectiveFactory>(f: F): F => | ||
@@ -52,0 +71,0 @@ ((...args: unknown[]) => { |
@@ -20,3 +20,3 @@ /** | ||
interface MaybePolyfilledCe extends CustomElementRegistry { | ||
polyfillWrapFlushCallback?: object; | ||
readonly polyfillWrapFlushCallback?: object; | ||
} | ||
@@ -32,6 +32,5 @@ | ||
/** | ||
* 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. | ||
* Reparents nodes, starting from `start` (inclusive) to `end` (exclusive), | ||
* into another container (could be the same container), before `before`. If | ||
* `before` is null, it appends the nodes to the container. | ||
*/ | ||
@@ -43,7 +42,6 @@ export const reparentNodes = | ||
before: Node|null = null): void => { | ||
let node = start; | ||
while (node !== end) { | ||
const n = node!.nextSibling; | ||
container.insertBefore(node!, before as Node); | ||
node = n; | ||
while (start !== end) { | ||
const n = start!.nextSibling; | ||
container.insertBefore(start!, before); | ||
start = n; | ||
} | ||
@@ -53,14 +51,12 @@ }; | ||
/** | ||
* Removes nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), from `container`. | ||
* Removes nodes, starting from `start` (inclusive) to `end` (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; | ||
} | ||
}; | ||
(container: Node, start: Node|null, end: Node|null = null): void => { | ||
while (start !== end) { | ||
const n = start!.nextSibling; | ||
container.removeChild(start!); | ||
start = n; | ||
} | ||
}; |
@@ -24,3 +24,3 @@ /** | ||
export interface Part { | ||
value: unknown; | ||
readonly value: unknown; | ||
@@ -34,3 +34,10 @@ /** | ||
/** | ||
* Commits the current part value, cause it to actually be written to the DOM. | ||
* Commits the current part value, causing it to actually be written to the | ||
* DOM. | ||
* | ||
* Directives are run at the start of `commit`, so that if they call | ||
* `part.setValue(...)` synchronously that value will be used in the current | ||
* commit, and there's no need to call `part.commit()` within the directive. | ||
* If directives set a part value asynchronously, then they must call | ||
* `part.commit()` manually. | ||
*/ | ||
@@ -44,3 +51,3 @@ commit(): void; | ||
*/ | ||
export const noChange: object = {}; | ||
export const noChange = {}; | ||
@@ -47,0 +54,0 @@ /** |
@@ -34,15 +34,21 @@ /** | ||
}; | ||
export const isIterable = (value: unknown): value is Iterable<unknown> => { | ||
return Array.isArray(value) || | ||
// tslint:disable-next-line:no-any | ||
!!(value && (value as any)[Symbol.iterator]); | ||
}; | ||
/** | ||
* Sets attribute values for AttributeParts, so that the value is only set once | ||
* even if there are multiple parts for an attribute. | ||
* Writes attribute values to the DOM for a group of AttributeParts bound to a | ||
* single attibute. The value is only set once even if there are multiple parts | ||
* for an attribute. | ||
*/ | ||
export class AttributeCommitter { | ||
element: Element; | ||
name: string; | ||
strings: string[]; | ||
parts: AttributePart[]; | ||
readonly element: Element; | ||
readonly name: string; | ||
readonly strings: ReadonlyArray<string>; | ||
readonly parts: ReadonlyArray<AttributePart>; | ||
dirty = true; | ||
constructor(element: Element, name: string, strings: string[]) { | ||
constructor(element: Element, name: string, strings: ReadonlyArray<string>) { | ||
this.element = element; | ||
@@ -53,3 +59,3 @@ this.name = name; | ||
for (let i = 0; i < strings.length - 1; i++) { | ||
this.parts[i] = this._createPart(); | ||
(this.parts as AttributePart[])[i] = this._createPart(); | ||
} | ||
@@ -75,11 +81,8 @@ } | ||
const v = part.value; | ||
if (v != null && | ||
(Array.isArray(v) || | ||
// tslint:disable-next-line:no-any | ||
typeof v !== 'string' && (v as any)[Symbol.iterator])) { | ||
for (const t of v as Iterable<unknown>) { | ||
if (isPrimitive(v) || !isIterable(v)) { | ||
text += typeof v === 'string' ? v : String(v); | ||
} else { | ||
for (const t of v) { | ||
text += typeof t === 'string' ? t : String(t); | ||
} | ||
} else { | ||
text += typeof v === 'string' ? v : String(v); | ||
} | ||
@@ -101,8 +104,11 @@ } | ||
/** | ||
* A Part that controls all or part of an attribute value. | ||
*/ | ||
export class AttributePart implements Part { | ||
committer: AttributeCommitter; | ||
readonly committer: AttributeCommitter; | ||
value: unknown = undefined; | ||
constructor(comitter: AttributeCommitter) { | ||
this.committer = comitter; | ||
constructor(committer: AttributeCommitter) { | ||
this.committer = committer; | ||
} | ||
@@ -135,8 +141,16 @@ | ||
/** | ||
* A Part that controls a location within a Node tree. Like a Range, NodePart | ||
* has start and end locations and can set and update the Nodes between those | ||
* locations. | ||
* | ||
* NodeParts support several value types: primitives, Nodes, TemplateResults, | ||
* as well as arrays and iterables of those types. | ||
*/ | ||
export class NodePart implements Part { | ||
options: RenderOptions; | ||
readonly options: RenderOptions; | ||
startNode!: Node; | ||
endNode!: Node; | ||
value: unknown = undefined; | ||
_pendingValue: unknown = undefined; | ||
private __pendingValue: unknown = undefined; | ||
@@ -148,3 +162,3 @@ constructor(options: RenderOptions) { | ||
/** | ||
* Inserts this part into a container. | ||
* Appends this part into a container. | ||
* | ||
@@ -159,5 +173,5 @@ * This part must be empty, as its contents are not automatically moved. | ||
/** | ||
* Inserts this part between `ref` and `ref`'s next sibling. Both `ref` and | ||
* its next sibling must be static, unchanging nodes such as those that appear | ||
* in a literal section of a template. | ||
* Inserts this part after the `ref` node (between `ref` and `ref`'s next | ||
* sibling). Both `ref` and its next sibling must be static, unchanging nodes | ||
* such as those that appear in a literal section of a template. | ||
* | ||
@@ -177,8 +191,8 @@ * This part must be empty, as its contents are not automatically moved. | ||
appendIntoPart(part: NodePart) { | ||
part._insert(this.startNode = createMarker()); | ||
part._insert(this.endNode = createMarker()); | ||
part.__insert(this.startNode = createMarker()); | ||
part.__insert(this.endNode = createMarker()); | ||
} | ||
/** | ||
* Appends this part after `ref` | ||
* Inserts this part after the `ref` part. | ||
* | ||
@@ -188,3 +202,3 @@ * This part must be empty, as its contents are not automatically moved. | ||
insertAfterPart(ref: NodePart) { | ||
ref._insert(this.startNode = createMarker()); | ||
ref.__insert(this.startNode = createMarker()); | ||
this.endNode = ref.endNode; | ||
@@ -195,12 +209,12 @@ ref.endNode = this.startNode; | ||
setValue(value: unknown): void { | ||
this._pendingValue = value; | ||
this.__pendingValue = value; | ||
} | ||
commit() { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
while (isDirective(this.__pendingValue)) { | ||
const directive = this.__pendingValue; | ||
this.__pendingValue = noChange; | ||
directive(this); | ||
} | ||
const value = this._pendingValue; | ||
const value = this.__pendingValue; | ||
if (value === noChange) { | ||
@@ -211,13 +225,10 @@ return; | ||
if (value !== this.value) { | ||
this._commitText(value); | ||
this.__commitText(value); | ||
} | ||
} else if (value instanceof TemplateResult) { | ||
this._commitTemplateResult(value); | ||
this.__commitTemplateResult(value); | ||
} else if (value instanceof Node) { | ||
this._commitNode(value); | ||
} else if ( | ||
Array.isArray(value) || | ||
// tslint:disable-next-line:no-any | ||
(value as any)[Symbol.iterator]) { | ||
this._commitIterable(value as Iterable<unknown>); | ||
this.__commitNode(value); | ||
} else if (isIterable(value)) { | ||
this.__commitIterable(value); | ||
} else if (value === nothing) { | ||
@@ -228,11 +239,11 @@ this.value = nothing; | ||
// Fallback, will render the string representation | ||
this._commitText(value); | ||
this.__commitText(value); | ||
} | ||
} | ||
private _insert(node: Node) { | ||
private __insert(node: Node) { | ||
this.endNode.parentNode!.insertBefore(node, this.endNode); | ||
} | ||
private _commitNode(value: Node): void { | ||
private __commitNode(value: Node): void { | ||
if (this.value === value) { | ||
@@ -242,7 +253,7 @@ return; | ||
this.clear(); | ||
this._insert(value); | ||
this.__insert(value); | ||
this.value = value; | ||
} | ||
private _commitText(value: unknown): void { | ||
private __commitText(value: unknown): void { | ||
const node = this.startNode.nextSibling!; | ||
@@ -257,3 +268,3 @@ value = value == null ? '' : value; | ||
} else { | ||
this._commitNode(document.createTextNode( | ||
this.__commitNode(document.createTextNode( | ||
typeof value === 'string' ? value : String(value))); | ||
@@ -264,3 +275,3 @@ } | ||
private _commitTemplateResult(value: TemplateResult): void { | ||
private __commitTemplateResult(value: TemplateResult): void { | ||
const template = this.options.templateFactory(value); | ||
@@ -279,3 +290,3 @@ if (this.value instanceof TemplateInstance && | ||
instance.update(value.values); | ||
this._commitNode(fragment); | ||
this.__commitNode(fragment); | ||
this.value = instance; | ||
@@ -285,3 +296,3 @@ } | ||
private _commitIterable(value: Iterable<unknown>): void { | ||
private __commitIterable(value: Iterable<unknown>): void { | ||
// For an Iterable, we create a new InstancePart per item, then set its | ||
@@ -330,3 +341,3 @@ // value to the item. This is a little bit of overhead for every item in | ||
itemParts.length = partIndex; | ||
this.clear(itemPart && itemPart!.endNode); | ||
this.clear(itemPart && itemPart.endNode); | ||
} | ||
@@ -349,9 +360,9 @@ } | ||
export class BooleanAttributePart implements Part { | ||
element: Element; | ||
name: string; | ||
strings: string[]; | ||
readonly element: Element; | ||
readonly name: string; | ||
readonly strings: ReadonlyArray<string>; | ||
value: unknown = undefined; | ||
_pendingValue: unknown = undefined; | ||
private __pendingValue: unknown = undefined; | ||
constructor(element: Element, name: string, strings: string[]) { | ||
constructor(element: Element, name: string, strings: ReadonlyArray<string>) { | ||
if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') { | ||
@@ -367,15 +378,15 @@ throw new Error( | ||
setValue(value: unknown): void { | ||
this._pendingValue = value; | ||
this.__pendingValue = value; | ||
} | ||
commit() { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
while (isDirective(this.__pendingValue)) { | ||
const directive = this.__pendingValue; | ||
this.__pendingValue = noChange; | ||
directive(this); | ||
} | ||
if (this._pendingValue === noChange) { | ||
if (this.__pendingValue === noChange) { | ||
return; | ||
} | ||
const value = !!this._pendingValue; | ||
const value = !!this.__pendingValue; | ||
if (this.value !== value) { | ||
@@ -387,5 +398,5 @@ if (value) { | ||
} | ||
this.value = value; | ||
} | ||
this.value = value; | ||
this._pendingValue = noChange; | ||
this.__pendingValue = noChange; | ||
} | ||
@@ -404,5 +415,5 @@ } | ||
export class PropertyCommitter extends AttributeCommitter { | ||
single: boolean; | ||
readonly single: boolean; | ||
constructor(element: Element, name: string, strings: string[]) { | ||
constructor(element: Element, name: string, strings: ReadonlyArray<string>) { | ||
super(element, name, strings); | ||
@@ -417,3 +428,3 @@ this.single = | ||
_getValue() { | ||
protected _getValue() { | ||
if (this.single) { | ||
@@ -460,9 +471,9 @@ return this.parts[0].value; | ||
export class EventPart implements Part { | ||
element: Element; | ||
eventName: string; | ||
eventContext?: EventTarget; | ||
readonly element: Element; | ||
readonly eventName: string; | ||
readonly eventContext?: EventTarget; | ||
value: undefined|EventHandlerWithOptions = undefined; | ||
_options?: AddEventListenerOptions; | ||
_pendingValue: undefined|EventHandlerWithOptions = undefined; | ||
_boundHandleEvent: (event: Event) => void; | ||
private __options?: AddEventListenerOptions; | ||
private __pendingValue: undefined|EventHandlerWithOptions = undefined; | ||
private readonly __boundHandleEvent: (event: Event) => void; | ||
@@ -473,20 +484,20 @@ constructor(element: Element, eventName: string, eventContext?: EventTarget) { | ||
this.eventContext = eventContext; | ||
this._boundHandleEvent = (e) => this.handleEvent(e); | ||
this.__boundHandleEvent = (e) => this.handleEvent(e); | ||
} | ||
setValue(value: undefined|EventHandlerWithOptions): void { | ||
this._pendingValue = value; | ||
this.__pendingValue = value; | ||
} | ||
commit() { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange as EventHandlerWithOptions; | ||
while (isDirective(this.__pendingValue)) { | ||
const directive = this.__pendingValue; | ||
this.__pendingValue = noChange as EventHandlerWithOptions; | ||
directive(this); | ||
} | ||
if (this._pendingValue === noChange) { | ||
if (this.__pendingValue === noChange) { | ||
return; | ||
} | ||
const newListener = this._pendingValue; | ||
const newListener = this.__pendingValue; | ||
const oldListener = this.value; | ||
@@ -503,11 +514,11 @@ const shouldRemoveListener = newListener == null || | ||
this.element.removeEventListener( | ||
this.eventName, this._boundHandleEvent, this._options); | ||
this.eventName, this.__boundHandleEvent, this.__options); | ||
} | ||
if (shouldAddListener) { | ||
this._options = getOptions(newListener); | ||
this.__options = getOptions(newListener); | ||
this.element.addEventListener( | ||
this.eventName, this._boundHandleEvent, this._options); | ||
this.eventName, this.__boundHandleEvent, this.__options); | ||
} | ||
this.value = newListener; | ||
this._pendingValue = noChange as EventHandlerWithOptions; | ||
this.__pendingValue = noChange as EventHandlerWithOptions; | ||
} | ||
@@ -514,0 +525,0 @@ |
@@ -22,4 +22,4 @@ /** | ||
export interface RenderOptions { | ||
templateFactory: TemplateFactory; | ||
eventContext?: EventTarget; | ||
readonly templateFactory: TemplateFactory; | ||
readonly eventContext?: EventTarget; | ||
} |
@@ -48,4 +48,4 @@ /** | ||
console.warn( | ||
`Incompatible ShadyCSS version detected.` + | ||
`Please update to at least @webcomponents/webcomponentsjs@2.0.2 and` + | ||
`Incompatible ShadyCSS version detected. ` + | ||
`Please update to at least @webcomponents/webcomponentsjs@2.0.2 and ` + | ||
`@webcomponents/shadycss@1.3.1.`); | ||
@@ -133,4 +133,5 @@ compatibleShadyCSSVersion = false; | ||
const styles = renderedDOM.querySelectorAll('style'); | ||
const {length} = styles; | ||
// If there are no styles, skip unnecessary work | ||
if (styles.length === 0) { | ||
if (length === 0) { | ||
// Ensure prepareTemplateStyles is called to support adding | ||
@@ -148,3 +149,3 @@ // styles via `prepareAdoptedCssText` since that requires that | ||
// currently does this anyway. When it does not, this should be changed. | ||
for (let i = 0; i < styles.length; i++) { | ||
for (let i = 0; i < length; i++) { | ||
const style = styles[i]; | ||
@@ -158,4 +159,4 @@ style.parentNode!.removeChild(style); | ||
// `template`. | ||
insertNodeIntoTemplate( | ||
template, condensedStyle, template.element.content.firstChild); | ||
const content = template.element.content; | ||
insertNodeIntoTemplate(template, condensedStyle, content.firstChild); | ||
// Note, it's important that ShadyCSS gets the template that `lit-html` | ||
@@ -165,16 +166,17 @@ // will actually render so that it can update the style inside when | ||
window.ShadyCSS!.prepareTemplateStyles(template.element, scopeName); | ||
if (window.ShadyCSS!.nativeShadow) { | ||
// When in native Shadow DOM, re-add styling to rendered content using | ||
// the style ShadyCSS produced. | ||
const style = template.element.content.querySelector('style')!; | ||
const style = content.querySelector('style'); | ||
if (window.ShadyCSS!.nativeShadow && style !== null) { | ||
// When in native Shadow DOM, ensure the style created by ShadyCSS is | ||
// included in initially rendered output (`renderedDOM`). | ||
renderedDOM.insertBefore(style.cloneNode(true), renderedDOM.firstChild); | ||
} else { | ||
// When not in native Shadow DOM, at this point ShadyCSS will have | ||
// removed the style from the lit template and parts will be broken as a | ||
// When no style is left in the template, parts will be broken as a | ||
// result. To fix this, we put back the style node ShadyCSS removed | ||
// and then tell lit to remove that node from the template. | ||
// There can be no style in the template in 2 cases (1) when Shady DOM | ||
// is in use, ShadyCSS removes all styles, (2) when native Shadow DOM | ||
// is in use ShadyCSS removes the style if it contains no content. | ||
// NOTE, ShadyCSS creates its own style so we can safely add/remove | ||
// `condensedStyle` here. | ||
template.element.content.insertBefore( | ||
condensedStyle, template.element.content.firstChild); | ||
content.insertBefore(condensedStyle, content.firstChild); | ||
const removes = new Set(); | ||
@@ -248,8 +250,9 @@ removes.add(condensedStyle); | ||
(result: TemplateResult, | ||
container: Element|DocumentFragment, | ||
container: Element|DocumentFragment|ShadowRoot, | ||
options: ShadyRenderOptions) => { | ||
const scopeName = options.scopeName; | ||
const hasRendered = parts.has(container); | ||
const needsScoping = container instanceof ShadowRoot && | ||
compatibleShadyCSSVersion && result instanceof TemplateResult; | ||
const needsScoping = compatibleShadyCSSVersion && | ||
container.nodeType === 11 /* Node.DOCUMENT_FRAGMENT_NODE */ && | ||
!!(container as ShadowRoot).host && result instanceof TemplateResult; | ||
// Handle first render to a scope specially... | ||
@@ -256,0 +259,0 @@ const firstScopeRender = needsScoping && !shadyRenderSet.has(scopeName); |
@@ -92,6 +92,6 @@ /** | ||
export type templateCache = { | ||
stringsArray: WeakMap<TemplateStringsArray, Template>; | ||
keyString: Map<string, Template>; | ||
readonly stringsArray: WeakMap<TemplateStringsArray, Template>; // | ||
readonly keyString: Map<string, Template>; | ||
}; | ||
export const templateCaches = new Map<string, templateCache>(); |
@@ -23,3 +23,3 @@ /** | ||
import {TemplateProcessor} from './template-processor.js'; | ||
import {isTemplatePartActive, Template} from './template.js'; | ||
import {isTemplatePartActive, Template, TemplatePart} from './template.js'; | ||
@@ -31,6 +31,6 @@ /** | ||
export class TemplateInstance { | ||
_parts: Array<Part|undefined> = []; | ||
processor: TemplateProcessor; | ||
options: RenderOptions; | ||
template: Template; | ||
private readonly __parts: Array<Part|undefined> = []; | ||
readonly processor: TemplateProcessor; | ||
readonly options: RenderOptions; | ||
readonly template: Template; | ||
@@ -45,5 +45,5 @@ constructor( | ||
update(values: unknown[]) { | ||
update(values: ReadonlyArray<unknown>) { | ||
let i = 0; | ||
for (const part of this._parts) { | ||
for (const part of this.__parts) { | ||
if (part !== undefined) { | ||
@@ -54,3 +54,3 @@ part.setValue(values[i]); | ||
} | ||
for (const part of this._parts) { | ||
for (const part of this.__parts) { | ||
if (part !== undefined) { | ||
@@ -63,7 +63,40 @@ part.commit(); | ||
_clone(): DocumentFragment { | ||
// When using the Custom Elements polyfill, clone the node, rather than | ||
// importing it, to keep the fragment in the template's document. This | ||
// leaves the fragment inert so custom elements won't upgrade and | ||
// potentially modify their contents by creating a polyfilled ShadowRoot | ||
// while we traverse the tree. | ||
// There are a number of steps in the lifecycle of a template instance's | ||
// DOM fragment: | ||
// 1. Clone - create the instance fragment | ||
// 2. Adopt - adopt into the main document | ||
// 3. Process - find part markers and create parts | ||
// 4. Upgrade - upgrade custom elements | ||
// 5. Update - set node, attribute, property, etc., values | ||
// 6. Connect - connect to the document. Optional and outside of this | ||
// method. | ||
// | ||
// We have a few constraints on the ordering of these steps: | ||
// * We need to upgrade before updating, so that property values will pass | ||
// through any property setters. | ||
// * We would like to process before upgrading so that we're sure that the | ||
// cloned fragment is inert and not disturbed by self-modifying DOM. | ||
// * We want custom elements to upgrade even in disconnected fragments. | ||
// | ||
// Given these constraints, with full custom elements support we would | ||
// prefer the order: Clone, Process, Adopt, Upgrade, Update, Connect | ||
// | ||
// But Safari dooes not implement CustomElementRegistry#upgrade, so we | ||
// can not implement that order and still have upgrade-before-update and | ||
// upgrade disconnected fragments. So we instead sacrifice the | ||
// process-before-upgrade constraint, since in Custom Elements v1 elements | ||
// must not modify their light DOM in the constructor. We still have issues | ||
// when co-existing with CEv0 elements like Polymer 1, and with polyfills | ||
// that don't strictly adhere to the no-modification rule because shadow | ||
// DOM, which may be created in the constructor, is emulated by being placed | ||
// in the light DOM. | ||
// | ||
// The resulting order is on native is: Clone, Adopt, Upgrade, Process, | ||
// Update, Connect. document.importNode() performs Clone, Adopt, and Upgrade | ||
// in one step. | ||
// | ||
// The Custom Elements v1 polyfill supports upgrade(), so the order when | ||
// polyfilled is the more ideal: Clone, Process, Adopt, Upgrade, Update, | ||
// Connect. | ||
const fragment = isCEPolyfill ? | ||
@@ -73,46 +106,54 @@ this.template.element.content.cloneNode(true) as DocumentFragment : | ||
const stack: Node[] = []; | ||
const parts = this.template.parts; | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
const walker = document.createTreeWalker( | ||
fragment, | ||
133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, | ||
null, | ||
false); | ||
let partIndex = 0; | ||
let nodeIndex = 0; | ||
const _prepareInstance = (fragment: DocumentFragment) => { | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be | ||
// null | ||
const walker = document.createTreeWalker( | ||
fragment, | ||
133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, | ||
null, | ||
false); | ||
let node = walker.nextNode(); | ||
// Loop through all the nodes and parts of a template | ||
while (partIndex < parts.length && node !== null) { | ||
const part = parts[partIndex]; | ||
// Consecutive Parts may have the same node index, in the case of | ||
// multiple bound attributes on an element. So each iteration we either | ||
// increment the nodeIndex, if we aren't on a node with a part, or the | ||
// partIndex if we are. By not incrementing the nodeIndex when we find a | ||
// part, we allow for the next part to be associated with the current | ||
// node if neccessasry. | ||
if (!isTemplatePartActive(part)) { | ||
this._parts.push(undefined); | ||
partIndex++; | ||
} else if (nodeIndex === part.index) { | ||
if (part.type === 'node') { | ||
const part = this.processor.handleTextExpression(this.options); | ||
part.insertAfterNode(node.previousSibling!); | ||
this._parts.push(part); | ||
} else { | ||
this._parts.push(...this.processor.handleAttributeExpressions( | ||
node as Element, part.name, part.strings, this.options)); | ||
} | ||
partIndex++; | ||
} else { | ||
nodeIndex++; | ||
if (node.nodeName === 'TEMPLATE') { | ||
_prepareInstance((node as HTMLTemplateElement).content); | ||
} | ||
let part: TemplatePart; | ||
let node = walker.nextNode(); | ||
// Loop through all the nodes and parts of a template | ||
while (partIndex < parts.length) { | ||
part = parts[partIndex]; | ||
if (!isTemplatePartActive(part)) { | ||
this.__parts.push(undefined); | ||
partIndex++; | ||
continue; | ||
} | ||
// Progress the tree walker until we find our next part's node. | ||
// Note that multiple parts may share the same node (attribute parts | ||
// on a single element), so this loop may not run at all. | ||
while (nodeIndex < part.index) { | ||
nodeIndex++; | ||
if (node!.nodeName === 'TEMPLATE') { | ||
stack.push(node!); | ||
walker.currentNode = (node as HTMLTemplateElement).content; | ||
} | ||
if ((node = walker.nextNode()) === null) { | ||
// We've exhausted the content inside a nested template element. | ||
// Because we still have parts (the outer for-loop), we know: | ||
// - There is a template in the stack | ||
// - The walker will find a nextNode outside the template | ||
walker.currentNode = stack.pop()!; | ||
node = walker.nextNode(); | ||
} | ||
} | ||
}; | ||
_prepareInstance(fragment); | ||
// We've arrived at our part's node. | ||
if (part.type === 'node') { | ||
const part = this.processor.handleTextExpression(this.options); | ||
part.insertAfterNode(node!.previousSibling!); | ||
this.__parts.push(part); | ||
} else { | ||
this.__parts.push(...this.processor.handleAttributeExpressions( | ||
node as Element, part.name, part.strings, this.options)); | ||
} | ||
partIndex++; | ||
} | ||
if (isCEPolyfill) { | ||
@@ -119,0 +160,0 @@ document.adoptNode(fragment); |
@@ -34,4 +34,4 @@ /** | ||
handleAttributeExpressions( | ||
element: Element, name: string, strings: string[], | ||
options: RenderOptions): Part[]; | ||
element: Element, name: string, strings: ReadonlyArray<string>, | ||
options: RenderOptions): ReadonlyArray<Part>; | ||
@@ -38,0 +38,0 @@ /** |
@@ -28,10 +28,10 @@ /** | ||
export class TemplateResult { | ||
strings: TemplateStringsArray; | ||
values: unknown[]; | ||
type: string; | ||
processor: TemplateProcessor; | ||
readonly strings: TemplateStringsArray; | ||
readonly values: ReadonlyArray<unknown>; | ||
readonly type: string; | ||
readonly processor: TemplateProcessor; | ||
constructor( | ||
strings: TemplateStringsArray, values: unknown[], type: string, | ||
processor: TemplateProcessor) { | ||
strings: TemplateStringsArray, values: ReadonlyArray<unknown>, | ||
type: string, processor: TemplateProcessor) { | ||
this.strings = strings; | ||
@@ -47,29 +47,53 @@ this.values = values; | ||
getHTML(): string { | ||
const endIndex = this.strings.length - 1; | ||
const l = this.strings.length - 1; | ||
let html = ''; | ||
for (let i = 0; i < endIndex; i++) { | ||
let isCommentBinding = false; | ||
for (let i = 0; i < l; i++) { | ||
const s = this.strings[i]; | ||
// This exec() call does two things: | ||
// 1) Appends a suffix to the bound attribute name to opt out of special | ||
// attribute value parsing that IE11 and Edge do, like for style and | ||
// many SVG attributes. The Template class also appends the same suffix | ||
// when looking up attributes to create Parts. | ||
// 2) Adds an unquoted-attribute-safe marker for the first expression in | ||
// an attribute. Subsequent attribute expressions will use node markers, | ||
// and this is safe since attributes with multiple expressions are | ||
// guaranteed to be quoted. | ||
const match = lastAttributeNameRegex.exec(s); | ||
if (match) { | ||
// We're starting a new bound attribute. | ||
// Add the safe attribute suffix, and use unquoted-attribute-safe | ||
// marker. | ||
html += s.substr(0, match.index) + match[1] + match[2] + | ||
boundAttributeSuffix + match[3] + marker; | ||
// For each binding we want to determine the kind of marker to insert | ||
// into the template source before it's parsed by the browser's HTML | ||
// parser. The marker type is based on whether the expression is in an | ||
// attribute, text, or comment poisition. | ||
// * For node-position bindings we insert a comment with the marker | ||
// sentinel as its text content, like <!--{{lit-guid}}-->. | ||
// * For attribute bindings we insert just the marker sentinel for the | ||
// first binding, so that we support unquoted attribute bindings. | ||
// Subsequent bindings can use a comment marker because multi-binding | ||
// attributes must be quoted. | ||
// * For comment bindings we insert just the marker sentinel so we don't | ||
// close the comment. | ||
// | ||
// The following code scans the template source, but is *not* an HTML | ||
// parser. We don't need to track the tree structure of the HTML, only | ||
// whether a binding is inside a comment, and if not, if it appears to be | ||
// the first binding in an attribute. | ||
const commentOpen = s.lastIndexOf('<!--'); | ||
// We're in comment position if we have a comment open with no following | ||
// comment close. Because <-- can appear in an attribute value there can | ||
// be false positives. | ||
isCommentBinding = (commentOpen > -1 || isCommentBinding) && | ||
s.indexOf('-->', commentOpen + 1) === -1; | ||
// Check to see if we have an attribute-like sequence preceeding the | ||
// expression. This can match "name=value" like structures in text, | ||
// comments, and attribute values, so there can be false-positives. | ||
const attributeMatch = lastAttributeNameRegex.exec(s); | ||
if (attributeMatch === null) { | ||
// We're only in this branch if we don't have a attribute-like | ||
// preceeding sequence. For comments, this guards against unusual | ||
// attribute values like <div foo="<!--${'bar'}">. Cases like | ||
// <!-- foo=${'bar'}--> are handled correctly in the attribute branch | ||
// below. | ||
html += s + (isCommentBinding ? marker : nodeMarker); | ||
} else { | ||
// We're either in a bound node, or trailing bound attribute. | ||
// Either way, nodeMarker is safe to use. | ||
html += s + nodeMarker; | ||
// For attributes we use just a marker sentinel, and also append a | ||
// $lit$ suffix to the name to opt-out of attribute-specific parsing | ||
// that IE and Edge do for style and certain SVG attributes. | ||
html += s.substr(0, attributeMatch.index) + attributeMatch[1] + | ||
attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] + | ||
marker; | ||
} | ||
} | ||
return html + this.strings[endIndex]; | ||
html += this.strings[l]; | ||
return html; | ||
} | ||
@@ -87,3 +111,3 @@ | ||
* | ||
* This class wraps HTMl in an `<svg>` tag in order to parse its contents in the | ||
* This class wraps HTML in an `<svg>` tag in order to parse its contents in the | ||
* SVG namespace, then modifies the template to remove the `<svg>` tag so that | ||
@@ -90,0 +114,0 @@ * clones only container the original fragment. |
@@ -44,127 +44,146 @@ /** | ||
export class Template { | ||
parts: TemplatePart[] = []; | ||
element: HTMLTemplateElement; | ||
readonly parts: TemplatePart[] = []; | ||
readonly element: HTMLTemplateElement; | ||
constructor(result: TemplateResult, element: HTMLTemplateElement) { | ||
this.element = element; | ||
const nodesToRemove: Node[] = []; | ||
const stack: Node[] = []; | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
const walker = document.createTreeWalker( | ||
element.content, | ||
133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, | ||
null, | ||
false); | ||
// Keeps track of the last index associated with a part. We try to delete | ||
// unnecessary nodes, but we never want to associate two different parts | ||
// to the same index. They must have a constant node between. | ||
let lastPartIndex = 0; | ||
let index = -1; | ||
let partIndex = 0; | ||
const nodesToRemove: Node[] = []; | ||
const _prepareTemplate = (template: HTMLTemplateElement) => { | ||
const content = template.content; | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be | ||
// null | ||
const walker = document.createTreeWalker( | ||
content, | ||
133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, | ||
null, | ||
false); | ||
// Keeps track of the last index associated with a part. We try to delete | ||
// unnecessary nodes, but we never want to associate two different parts | ||
// to the same index. They must have a constant node between. | ||
let lastPartIndex = 0; | ||
while (walker.nextNode()) { | ||
index++; | ||
const node = walker.currentNode as Element | Comment | Text; | ||
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { | ||
if ((node as Element).hasAttributes()) { | ||
const attributes = (node as Element).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++) { | ||
if (attributes[i].value.indexOf(marker) >= 0) { | ||
count++; | ||
} | ||
const {strings, values: {length}} = result; | ||
while (partIndex < length) { | ||
const node = walker.nextNode() as Element | Comment | Text | null; | ||
if (node === null) { | ||
// We've exhausted the content inside a nested template element. | ||
// Because we still have parts (the outer for-loop), we know: | ||
// - There is a template in the stack | ||
// - The walker will find a nextNode outside the template | ||
walker.currentNode = stack.pop()!; | ||
continue; | ||
} | ||
index++; | ||
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { | ||
if ((node as Element).hasAttributes()) { | ||
const attributes = (node as Element).attributes; | ||
const {length} = 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 correspondence between part index and attribute index. | ||
let count = 0; | ||
for (let i = 0; i < length; i++) { | ||
if (endsWith(attributes[i].name, boundAttributeSuffix)) { | ||
count++; | ||
} | ||
while (count-- > 0) { | ||
// Get the template literal section leading up to the first | ||
// expression in this attribute | ||
const stringForPart = result.strings[partIndex]; | ||
// Find the attribute name | ||
const name = lastAttributeNameRegex.exec(stringForPart)![2]; | ||
// Find the corresponding attribute | ||
// All bound attributes have had a suffix added in | ||
// TemplateResult#getHTML to opt out of special attribute | ||
// handling. To look up the attribute value we also need to add | ||
// the suffix. | ||
const attributeLookupName = | ||
name.toLowerCase() + boundAttributeSuffix; | ||
const attributeValue = | ||
(node as Element).getAttribute(attributeLookupName)!; | ||
const strings = attributeValue.split(markerRegex); | ||
this.parts.push({type: 'attribute', index, name, strings}); | ||
(node as Element).removeAttribute(attributeLookupName); | ||
partIndex += strings.length - 1; | ||
} | ||
} | ||
if ((node as Element).tagName === 'TEMPLATE') { | ||
_prepareTemplate(node as HTMLTemplateElement); | ||
while (count-- > 0) { | ||
// Get the template literal section leading up to the first | ||
// expression in this attribute | ||
const stringForPart = strings[partIndex]; | ||
// Find the attribute name | ||
const name = lastAttributeNameRegex.exec(stringForPart)![2]; | ||
// Find the corresponding attribute | ||
// All bound attributes have had a suffix added in | ||
// TemplateResult#getHTML to opt out of special attribute | ||
// handling. To look up the attribute value we also need to add | ||
// the suffix. | ||
const attributeLookupName = | ||
name.toLowerCase() + boundAttributeSuffix; | ||
const attributeValue = | ||
(node as Element).getAttribute(attributeLookupName)!; | ||
(node as Element).removeAttribute(attributeLookupName); | ||
const statics = attributeValue.split(markerRegex); | ||
this.parts.push({type: 'attribute', index, name, strings: statics}); | ||
partIndex += statics.length - 1; | ||
} | ||
} else if (node.nodeType === 3 /* Node.TEXT_NODE */) { | ||
const data = (node as Text).data!; | ||
if (data.indexOf(marker) >= 0) { | ||
const parent = node.parentNode!; | ||
const strings = data.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
// Generate a new text node for each literal section | ||
// These nodes are also used as the markers for node parts | ||
for (let i = 0; i < lastIndex; i++) { | ||
parent.insertBefore( | ||
(strings[i] === '') ? createMarker() : | ||
document.createTextNode(strings[i]), | ||
node); | ||
this.parts.push({type: 'node', index: ++index}); | ||
} | ||
// If there's no text, we must insert a comment to mark our place. | ||
// Else, we can trust it will stick around after cloning. | ||
if (strings[lastIndex] === '') { | ||
parent.insertBefore(createMarker(), node); | ||
nodesToRemove.push(node); | ||
} | ||
if ((node as Element).tagName === 'TEMPLATE') { | ||
stack.push(node); | ||
walker.currentNode = (node as HTMLTemplateElement).content; | ||
} | ||
} else if (node.nodeType === 3 /* Node.TEXT_NODE */) { | ||
const data = (node as Text).data; | ||
if (data.indexOf(marker) >= 0) { | ||
const parent = node.parentNode!; | ||
const strings = data.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
// Generate a new text node for each literal section | ||
// These nodes are also used as the markers for node parts | ||
for (let i = 0; i < lastIndex; i++) { | ||
let insert: Node; | ||
let s = strings[i]; | ||
if (s === '') { | ||
insert = createMarker(); | ||
} else { | ||
(node as Text).data = strings[lastIndex]; | ||
const match = lastAttributeNameRegex.exec(s); | ||
if (match !== null && endsWith(match[2], boundAttributeSuffix)) { | ||
s = s.slice(0, match.index) + match[1] + | ||
match[2].slice(0, -boundAttributeSuffix.length) + match[3]; | ||
} | ||
insert = document.createTextNode(s); | ||
} | ||
// We have a part for each match found | ||
partIndex += lastIndex; | ||
parent.insertBefore(insert, node); | ||
this.parts.push({type: 'node', index: ++index}); | ||
} | ||
} else if (node.nodeType === 8 /* Node.COMMENT_NODE */) { | ||
if ((node as Comment).data === marker) { | ||
const parent = node.parentNode!; | ||
// 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 | ||
// * The previousSibling is already the start of a previous part | ||
if (node.previousSibling === null || index === lastPartIndex) { | ||
index++; | ||
parent.insertBefore(createMarker(), node); | ||
} | ||
lastPartIndex = index; | ||
this.parts.push({type: 'node', index}); | ||
// If we don't have a nextSibling, keep this node so we have an end. | ||
// Else, we can remove it to save future costs. | ||
if (node.nextSibling === null) { | ||
(node as Comment).data = ''; | ||
} else { | ||
nodesToRemove.push(node); | ||
index--; | ||
} | ||
partIndex++; | ||
// If there's no text, we must insert a comment to mark our place. | ||
// Else, we can trust it will stick around after cloning. | ||
if (strings[lastIndex] === '') { | ||
parent.insertBefore(createMarker(), node); | ||
nodesToRemove.push(node); | ||
} 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: 'node', index: -1}); | ||
} | ||
(node as Text).data = strings[lastIndex]; | ||
} | ||
// We have a part for each match found | ||
partIndex += lastIndex; | ||
} | ||
} else if (node.nodeType === 8 /* Node.COMMENT_NODE */) { | ||
if ((node as Comment).data === marker) { | ||
const parent = node.parentNode!; | ||
// 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 | ||
// * The previousSibling is already the start of a previous part | ||
if (node.previousSibling === null || index === lastPartIndex) { | ||
index++; | ||
parent.insertBefore(createMarker(), node); | ||
} | ||
lastPartIndex = index; | ||
this.parts.push({type: 'node', index}); | ||
// If we don't have a nextSibling, keep this node so we have an end. | ||
// Else, we can remove it to save future costs. | ||
if (node.nextSibling === null) { | ||
(node as Comment).data = ''; | ||
} else { | ||
nodesToRemove.push(node); | ||
index--; | ||
} | ||
partIndex++; | ||
} 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: 'node', index: -1}); | ||
partIndex++; | ||
} | ||
} | ||
} | ||
}; | ||
_prepareTemplate(element); | ||
} | ||
// Remove text binding nodes after the walk to not disturb the TreeWalker | ||
@@ -177,2 +196,7 @@ for (const n of nodesToRemove) { | ||
const endsWith = (str: string, suffix: string): boolean => { | ||
const index = str.length - suffix.length; | ||
return index >= 0 && str.slice(index) === suffix; | ||
}; | ||
/** | ||
@@ -195,5 +219,5 @@ * A placeholder for a dynamic expression in an HTML template. | ||
export type TemplatePart = { | ||
type: 'node', | ||
readonly type: 'node', | ||
index: number | ||
}|{type: 'attribute', index: number, name: string, strings: string[]}; | ||
}|{readonly type: 'attribute', index: number, readonly name: string, readonly strings: ReadonlyArray<string>}; | ||
@@ -213,9 +237,10 @@ export const isTemplatePartActive = (part: TemplatePart) => part.index !== -1; | ||
* See attributes in the HTML spec: | ||
* https://www.w3.org/TR/html5/syntax.html#attributes-0 | ||
* https://www.w3.org/TR/html5/syntax.html#elements-attributes | ||
* | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters | ||
* | ||
* " \x09\x0a\x0c\x0d" are HTML space characters: | ||
* https://www.w3.org/TR/html5/infrastructure.html#space-character | ||
* https://www.w3.org/TR/html5/infrastructure.html#space-characters | ||
* | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every | ||
* space character except " ". | ||
* | ||
* So an attribute is: | ||
@@ -233,2 +258,2 @@ * * The name: any character except a control character, space character, ('), | ||
export const lastAttributeNameRegex = | ||
/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F \x09\x0a\x0c\x0d"'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/; | ||
/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/; |
@@ -41,3 +41,3 @@ /** | ||
export {noChange, nothing, Part} from './lib/part.js'; | ||
export {AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isPrimitive, NodePart, PropertyCommitter, PropertyPart} from './lib/parts.js'; | ||
export {AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isIterable, isPrimitive, NodePart, PropertyCommitter, PropertyPart} from './lib/parts.js'; | ||
export {RenderOptions} from './lib/render-options.js'; | ||
@@ -44,0 +44,0 @@ export {parts, render} from './lib/render.js'; |
@@ -15,2 +15,4 @@ /** | ||
import {removeNodes, reparentNodes} from '../lib/dom.js'; | ||
/** | ||
@@ -26,23 +28,33 @@ * A lightweight <template> polyfill that supports minimum features to cover | ||
export const initTemplatePolyfill = (forced = false) => { | ||
if (typeof HTMLTemplateElement !== 'undefined' && !forced) { | ||
// Minimal polyfills (like this one) may provide only a subset of Template's | ||
// functionality. So, we explicitly check that at least content is present to | ||
// prevent installing patching with multiple polyfills, which might happen if | ||
// multiple versions of lit-html were included on a page. | ||
if (!forced && 'content' in document.createElement('template')) { | ||
return; | ||
} | ||
const contentDoc = document.implementation.createHTMLDocument('template'); | ||
const body = contentDoc.body; | ||
const descriptor = { | ||
enumerable: true, | ||
configurable: true, | ||
}; | ||
// tslint:disable-next-line:no-any | ||
const upgrade = (template: any) => { | ||
template.content = contentDoc.createDocumentFragment(); | ||
Object.defineProperty(template, 'innerHTML', { | ||
set: function(text) { | ||
contentDoc.body.innerHTML = text; | ||
const content = (this as HTMLTemplateElement).content; | ||
while (content.firstChild) { | ||
content.removeChild(content.firstChild); | ||
} | ||
const body = contentDoc.body; | ||
while (body.firstChild) { | ||
content.appendChild(body.firstChild); | ||
} | ||
const upgrade = (template: HTMLTemplateElement) => { | ||
const content = contentDoc.createDocumentFragment(); | ||
Object.defineProperties(template, { | ||
content: { | ||
...descriptor, | ||
get() { | ||
return content; | ||
}, | ||
}, | ||
configurable: true | ||
innerHTML: { | ||
...descriptor, | ||
set: function(text) { | ||
body.innerHTML = text; | ||
removeNodes(content, content.firstChild); | ||
reparentNodes(content, body.firstChild); | ||
}, | ||
}, | ||
}); | ||
@@ -54,6 +66,5 @@ }; | ||
tagName: string, options?: ElementCreationOptions) { | ||
let el = capturedCreateElement.call(this, tagName, options); | ||
if (el.localName === 'template') { | ||
el = capturedCreateElement.call(this, 'div'); | ||
upgrade(el); | ||
const el = capturedCreateElement.call(this, tagName, options); | ||
if (el.tagName === 'TEMPLATE') { | ||
upgrade(el as HTMLTemplateElement); | ||
} | ||
@@ -60,0 +71,0 @@ return el; |
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
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
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
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
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
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
522345
17
139
6962
47