import type { Directive, IDirectiveRegistry } from '../directive'; const $Value = Symbol(), $Index = Symbol(), $Exec = Symbol(); const replicate = (n: number, text: string) => { let result = '' if (n < 1) return result while (n > 1) { if (n & 1) result += text n >>= 1 text += text } return result + text } const repeatDirective: Directive = { attribute: (attr) => attr.nodeName === '#repeat' || attr.nodeName === `#foreach`, process: ({ targetNode, componentNode, attribute }) => { const scanDOMTree = (componentNode).constructor['internal.scanDOM']; Promise.resolve().then(() => (targetNode.removeAttribute('#repeat'), targetNode.removeAttribute('#foreach'))); const template = document.createElement('template') as HTMLTemplateElement; template.content.appendChild(targetNode.cloneNode(true)); (template.content.firstChild as Element).removeAttribute('#repeat'); (template.content.firstChild as Element).removeAttribute('#foreach'); const hook = document.createComment('-r-'); targetNode.parentNode!.insertBefore(hook, targetNode); Promise.resolve().then(() => targetNode.remove()); const range = document.createRange(); const clones: HTMLElement[] = []; let lastValue: any[] = []; let scratchPad: DocumentFragment | undefined; const handler = (value: Array = [], sideEffect: Function = () => { }) => { // remove extra if (clones.length > value.length) { const rng = document.createRange(); rng.setStartBefore(clones[value.length]); rng.setEndAfter(clones[clones.length - 1]); rng.deleteContents(); rng.detach(); clones.length = value.length; } // prepare new nodes if (lastValue.length < value.length) { const t = document.createElement('template'); t.innerHTML = replicate(value.length - lastValue.length, template.innerHTML); scratchPad = document.createDocumentFragment(); scratchPad.append(t.content); } else { scratchPad = undefined; } for (let i = 0; i < value.length; i++) { // update existing nodes const clone = clones[i]; if (clone) { const updater = (clone)[$Exec] as Function; if ((clone)[$Value] !== value[i] && updater) { (clone)[$Value] = value[i]; (clone)[$Exec](); } } else { // bind new nodes const newClone = scratchPad!.children.item(i - lastValue.length) as HTMLElement; (newClone)[$Value] = value[i]; (newClone)[$Index] = i; const { update } = scanDOMTree({ root: newClone, element: componentNode as HTMLElement, customArguments: { value: () => (newClone)[$Value], index: () => (newClone)[$Index], } }); (newClone)[$Exec] = () => update('value'); update(); } } if (scratchPad) { // attach new nodes clones.push(...(Array.from(scratchPad.children)) as HTMLElement[]) range.setStartAfter(hook); range.collapse(); range.insertNode(scratchPad); } lastValue = [...value]; if (sideEffect) { sideEffect(); } } handler.sideEffect = attribute.nodeName === '#repeat'; return handler; } } export default function register(registry: IDirectiveRegistry) { registry.register(repeatDirective); }