import { Module } from "./modules/module"; import { vnode, VNode } from "./vnode"; import * as is from "./is"; import { htmlDomApi, DOMAPI } from "./htmldomapi"; type NonUndefined = T extends undefined ? never : T; function isUndef(s: any): boolean { return s === undefined; } function isDef(s: A): s is NonUndefined { return s !== undefined; } type VNodeQueue = VNode[]; const emptyNode = vnode("", {}, [], undefined, undefined); function sameVnode(vnode1: VNode, vnode2: VNode): boolean { const isSameKey = vnode1.key === vnode2.key; const isSameIs = vnode1.data?.is === vnode2.data?.is; const isSameSel = vnode1.sel === vnode2.sel; const isSameTextOrFragment = !vnode1.sel && vnode1.sel === vnode2.sel ? typeof vnode1.text === typeof vnode2.text : true; return isSameSel && isSameKey && isSameIs && isSameTextOrFragment; } /** * @todo Remove this function when the document fragment is considered stable. */ function documentFragmentIsNotSupported(): never { throw new Error("The document fragment is not supported on this platform."); } function isElement( api: DOMAPI, vnode: Element | DocumentFragment | VNode ): vnode is Element { return api.isElement(vnode as any); } function isDocumentFragment( api: DOMAPI, vnode: DocumentFragment | VNode ): vnode is DocumentFragment { return api.isDocumentFragment!(vnode as any); } type KeyToIndexMap = { [key: string]: number }; type ArraysOf = { [K in keyof T]: Array; }; type ModuleHooks = ArraysOf>; function createKeyToOldIdx( children: VNode[], beginIdx: number, endIdx: number ): KeyToIndexMap { const map: KeyToIndexMap = {}; for (let i = beginIdx; i <= endIdx; ++i) { const key = children[i]?.key; if (key !== undefined) { map[key as string] = i; } } return map; } const hooks: Array = [ "create", "update", "remove", "destroy", "pre", "post", ]; // TODO Should `domApi` be put into this in the next major version bump? export type Options = { experimental?: { fragments?: boolean; }; }; export function init( modules: Array>, domApi?: DOMAPI, options?: Options ) { const cbs: ModuleHooks = { create: [], update: [], remove: [], destroy: [], pre: [], post: [], }; const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi; for (const hook of hooks) { for (const module of modules) { const currentHook = module[hook]; if (currentHook !== undefined) { (cbs[hook] as any[]).push(currentHook); } } } function emptyNodeAt(elm: Element) { const id = elm.id ? "#" + elm.id : ""; // elm.className doesn't return a string when elm is an SVG element inside a shadowRoot. // https://stackoverflow.com/questions/29454340/detecting-classname-of-svganimatedstring const classes = elm.getAttribute("class"); const c = classes ? "." + classes.split(" ").join(".") : ""; return vnode( api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm ); } function emptyDocumentFragmentAt(frag: DocumentFragment) { return vnode(undefined, {}, [], undefined, frag); } function createRmCb(childElm: Node, listeners: number) { return function rmCb() { if (--listeners === 0) { const parent = api.parentNode(childElm) as Node; api.removeChild(parent, childElm); } }; } function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { let i: any; let data = vnode.data; if (data !== undefined) { const init = data.hook?.init; if (isDef(init)) { init(vnode); data = vnode.data; } } const children = vnode.children; const sel = vnode.sel; if (sel === "!") { if (isUndef(vnode.text)) { vnode.text = ""; } vnode.elm = api.createComment(vnode.text!); } else if (sel !== undefined) { // Parse selector const hashIdx = sel.indexOf("#"); const dotIdx = sel.indexOf(".", hashIdx); const hash = hashIdx > 0 ? hashIdx : sel.length; const dot = dotIdx > 0 ? dotIdx : sel.length; const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; const elm = (vnode.elm = isDef(data) && isDef((i = data.ns)) ? api.createElementNS(i, tag, data) : api.createElement(tag, data)); if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot)); if (dotIdx > 0) elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); if (is.array(children)) { for (i = 0; i < children.length; ++i) { const ch = children[i]; if (ch != null) { api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue)); } } } else if (is.primitive(vnode.text)) { api.appendChild(elm, api.createTextNode(vnode.text)); } const hook = vnode.data!.hook; if (isDef(hook)) { hook.create?.(emptyNode, vnode); if (hook.insert) { insertedVnodeQueue.push(vnode); } } } else if (options?.experimental?.fragments && vnode.children) { vnode.elm = ( api.createDocumentFragment ?? documentFragmentIsNotSupported )(); for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); for (i = 0; i < vnode.children.length; ++i) { const ch = vnode.children[i]; if (ch != null) { api.appendChild( vnode.elm, createElm(ch as VNode, insertedVnodeQueue) ); } } } else { vnode.elm = api.createTextNode(vnode.text!); } return vnode.elm; } function addVnodes( parentElm: Node, before: Node | null, vnodes: VNode[], startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue ) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (ch != null) { api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); } } } function invokeDestroyHook(vnode: VNode) { const data = vnode.data; if (data !== undefined) { data?.hook?.destroy?.(vnode); for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); if (vnode.children !== undefined) { for (let j = 0; j < vnode.children.length; ++j) { const child = vnode.children[j]; if (child != null && typeof child !== "string") { invokeDestroyHook(child); } } } } } function removeVnodes( parentElm: Node, vnodes: VNode[], startIdx: number, endIdx: number ): void { for (; startIdx <= endIdx; ++startIdx) { let listeners: number; let rm: () => void; const ch = vnodes[startIdx]; if (ch != null) { if (isDef(ch.sel)) { invokeDestroyHook(ch); listeners = cbs.remove.length + 1; rm = createRmCb(ch.elm!, listeners); for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); const removeHook = ch?.data?.hook?.remove; if (isDef(removeHook)) { removeHook(ch, rm); } else { rm(); } } else if (ch.children) { // Fragment node invokeDestroyHook(ch); removeVnodes( parentElm, ch.children as VNode[], 0, ch.children.length - 1 ); } else { // Text node api.removeChild(parentElm, ch.elm!); } } } } function updateChildren( parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue ) { let oldStartIdx = 0; let newStartIdx = 0; let oldEndIdx = oldCh.length - 1; let oldStartVnode = oldCh[0]; let oldEndVnode = oldCh[oldEndIdx]; let newEndIdx = newCh.length - 1; let newStartVnode = newCh[0]; let newEndVnode = newCh[newEndIdx]; let oldKeyToIdx: KeyToIndexMap | undefined; let idxInOld: number; let elmToMove: VNode; let before: any; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left } else if (oldEndVnode == null) { oldEndVnode = oldCh[--oldEndIdx]; } else if (newStartVnode == null) { newStartVnode = newCh[++newStartIdx]; } else if (newEndVnode == null) { newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); api.insertBefore( parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!) ); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = oldKeyToIdx[newStartVnode.key as string]; if (isUndef(idxInOld)) { // New element api.insertBefore( parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm! ); } else { elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore( parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm! ); } else { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined as any; api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!); } } newStartVnode = newCh[++newStartIdx]; } } if (newStartIdx <= newEndIdx) { before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; addVnodes( parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue ); } if (oldStartIdx <= oldEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } } function patchVnode( oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue ) { const hook = vnode.data?.hook; hook?.prepatch?.(oldVnode, vnode); const elm = (vnode.elm = oldVnode.elm)!; if (oldVnode === vnode) return; if ( vnode.data !== undefined || (isDef(vnode.text) && vnode.text !== oldVnode.text) ) { vnode.data ??= {}; oldVnode.data ??= {}; for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); vnode.data?.hook?.update?.(oldVnode, vnode); } const oldCh = oldVnode.children as VNode[]; const ch = vnode.children as VNode[]; if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { if (isDef(oldVnode.text)) api.setTextContent(elm, ""); addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { api.setTextContent(elm, ""); } } else if (oldVnode.text !== vnode.text) { if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } api.setTextContent(elm, vnode.text!); } hook?.postpatch?.(oldVnode, vnode); } return function patch( oldVnode: VNode | Element | DocumentFragment, vnode: VNode ): VNode { let i: number, elm: Node, parent: Node; const insertedVnodeQueue: VNodeQueue = []; for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); if (isElement(api, oldVnode)) { oldVnode = emptyNodeAt(oldVnode); } else if (isDocumentFragment(api, oldVnode)) { oldVnode = emptyDocumentFragmentAt(oldVnode); } if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { elm = oldVnode.elm!; parent = api.parentNode(elm) as Node; createElm(vnode, insertedVnodeQueue); if (parent !== null) { api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } } for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]); } for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); return vnode; }; }