UNPKG

lit-html

Version:

HTML template literals in JavaScript

151 lines (135 loc) 5.49 kB
/** * @license * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at * http://polymer.github.io/LICENSE.txt * The complete set of authors may be found at * http://polymer.github.io/AUTHORS.txt * The complete set of contributors may be found at * http://polymer.github.io/CONTRIBUTORS.txt * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at * http://polymer.github.io/PATENTS.txt */ import { NodePart } from "./lit-html.js"; export type KeyFn<T> = (item: T) => any; export type ItemTemplate<T> = (item: T, index: number) => any; export type RepeatResult = (part: NodePart) => any; interface State { keyMap: Map<any, NodePart>; parts: NodePart[]; } const stateCache = new WeakMap<NodePart, State>(); export function repeat<T>(items: T[], keyFn: KeyFn<T>, template: ItemTemplate<T>): RepeatResult; export function repeat<T>(items: T[], template: ItemTemplate<T>): RepeatResult; export function repeat<T>(items: Iterable<T>, keyFnOrTemplate: KeyFn<T>|ItemTemplate<T>, template?: ItemTemplate<T>): RepeatResult { let keyFn: KeyFn<T>; if (arguments.length === 2) { template = keyFnOrTemplate; } else if (arguments.length === 3) { keyFn = keyFnOrTemplate as KeyFn<T>; } return (part: NodePart): any => { let state = stateCache.get(part); if (state === undefined) { state = { keyMap: keyFn && new Map(), parts: [], }; stateCache.set(part, state); } const container = part.startNode.parentNode as HTMLElement|ShadowRoot|DocumentFragment; const oldParts = state.parts; const endParts = new Map<Node, NodePart>(oldParts.map((p) => [p.endNode, p] as [Node, NodePart])); const keyMap = state.keyMap; const itemParts = []; let index = 0; let oldPartsIndex = 0; let currentMarker: Node|undefined; for (const item of items) { let result; let key; try { result = template!(item, index++); key = keyFn && keyFn(item); } catch(e) { console.error(e); continue; } // Try to reuse a part, either keyed or from the list of previous parts // if there's no keyMap let itemPart = keyMap == undefined ? oldParts[oldPartsIndex++] : keyMap.get(key); if (itemPart === undefined) { // New part, attach it if (currentMarker === undefined) { currentMarker = new Text(); container.insertBefore(currentMarker, part.startNode.nextSibling); } const endNode = new Text(); container.insertBefore(endNode, currentMarker.nextSibling); itemPart = new NodePart(part.instance, currentMarker, endNode); if (key !== undefined && keyMap !== undefined) { keyMap.set(key, itemPart!); } } else { // Existing part, maybe move it const range = document.createRange(); range.setStartBefore(itemPart.startNode); range.setEndBefore(itemPart.endNode); if (currentMarker === undefined) { // this should be the first part, make sure it's first if (part.startNode.nextSibling !== itemPart.startNode) { // move the whole part // get previous and next parts const previousPart = endParts.get(itemPart.startNode); if (previousPart) { previousPart.endNode = itemPart.endNode; endParts.set(previousPart.endNode, previousPart); } const contents = range.extractContents(); if (part.startNode.nextSibling === part.endNode) { // The container part was empty, so we need a new endPart itemPart.endNode = new Text(); container.insertBefore(itemPart.endNode, part.startNode.nextSibling); } else { // endNode should equal the startNode of the currently first part itemPart.endNode = part.startNode.nextSibling!; } container.insertBefore(contents, part.startNode.nextSibling); } // else part is in the correct position already } else if (currentMarker !== itemPart.startNode) { // move to correct position const previousPart = endParts.get(itemPart.startNode); if (previousPart) { previousPart.endNode = itemPart.endNode; endParts.set(previousPart.endNode, previousPart); } const contents = range.extractContents(); container.insertBefore(contents, currentMarker); } // remove part from oldParts list so it's not cleaned up oldParts.splice(oldParts.indexOf(itemPart), 1); } // else part is in the correct position already itemPart.setValue(result); itemParts.push(itemPart); currentMarker = itemPart.endNode; } // Cleanup if (oldParts.length > 0) { const clearStart = oldParts[0].startNode; const clearEnd = oldParts[oldParts.length - 1].endNode; const clearRange = document.createRange(); if (itemParts.length === 0) { clearRange.setStartBefore(clearStart); } else { clearRange.setStartAfter(clearStart); } clearRange.setEndAfter(clearEnd); clearRange.deleteContents(); clearRange.detach(); // is this neccessary? } state.parts = itemParts; }; }