diff --git a/src/component.ts b/src/component.ts index f13d7f72..4dd7aa05 100644 --- a/src/component.ts +++ b/src/component.ts @@ -1,13 +1,15 @@ -import h from "../libs/snabbdom/src/h"; -import sdAttrs from "../libs/snabbdom/src/modules/attributes"; -import sdProps from "../libs/snabbdom/src/modules/props"; -import sdListeners from "../libs/snabbdom/src/modules/eventlisteners"; -import { init } from "../libs/snabbdom/src/snabbdom"; -import { VNode } from "../libs/snabbdom/src/vnode"; import { EventBus } from "./event_bus"; import { Observer } from "./observer"; import { QWeb } from "./qweb"; import { idGenerator } from "./utils"; +import { + attrsModule, + eventListenersModule, + h, + init, + propsModule, + VNode +} from "./vdom"; let getId = idGenerator(); @@ -38,7 +40,7 @@ export interface Meta { observer?: Observer; } -const patch = init([sdListeners, sdAttrs, sdProps]); +const patch = init([eventListenersModule, attrsModule, propsModule]); export interface Type extends Function { new (...args: any[]): T; diff --git a/src/qweb.ts b/src/qweb.ts index aeb20528..7c8b3a03 100644 --- a/src/qweb.ts +++ b/src/qweb.ts @@ -1,5 +1,4 @@ -import h from "../libs/snabbdom/src/h"; -import { VNode } from "../libs/snabbdom/src/vnode"; +import { VNode, h } from "./vdom"; //------------------------------------------------------------------------------ // Types diff --git a/src/vdom.ts b/src/vdom.ts new file mode 100644 index 00000000..6f60e4ac --- /dev/null +++ b/src/vdom.ts @@ -0,0 +1,911 @@ +//------------------------------------------------------------------------------ +// vnode.ts +//------------------------------------------------------------------------------ +type Key = string | number; + +export interface VNode { + sel: string | undefined; + data: VNodeData | undefined; + children: Array | undefined; + elm: Node | undefined; + text: string | undefined; + key: Key | undefined; +} + +interface VNodeData { + props?: Props; + attrs?: Attrs; + on?: On; + hook?: Hooks; + key?: Key; + ns?: string; // for SVGs + fn?: () => VNode; // for thunks + args?: Array; // for thunks + [key: string]: any; // for any other 3rd party module +} + +function vnode( + sel: string | undefined, + data: any | undefined, + children: Array | undefined, + text: string | undefined, + elm: Element | Text | undefined +): VNode { + let key = data === undefined ? undefined : data.key; + return { + sel: sel, + data: data, + children: children, + text: text, + elm: elm, + key: key + }; +} + + +//------------------------------------------------------------------------------ +// snabbdom.ts +//------------------------------------------------------------------------------ +function isUndef(s: any): boolean { + return s === undefined; +} +function isDef(s: any): boolean { + return s !== undefined; +} + +type VNodeQueue = Array; + +const emptyNode = vnode("", {}, [], undefined, undefined); + +function sameVnode(vnode1: VNode, vnode2: VNode): boolean { + return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; +} + +function isVnode(vnode: any): vnode is VNode { + return vnode.sel !== undefined; +} + +type KeyToIndexMap = { [key: string]: number }; + +type ArraysOf = { [K in keyof T]: (T[K])[] }; + +type ModuleHooks = ArraysOf; + +function createKeyToOldIdx( + children: Array, + beginIdx: number, + endIdx: number +): KeyToIndexMap { + let i: number, + map: KeyToIndexMap = {}, + key: Key | undefined, + ch; + for (i = beginIdx; i <= endIdx; ++i) { + ch = children[i]; + if (ch != null) { + key = ch.key; + if (key !== undefined) map[key] = i; + } + } + return map; +} + +const hooks: (keyof Module)[] = [ + "create", + "update", + "remove", + "destroy", + "pre", + "post" +]; + +export function init(modules: Array>, domApi?: DOMAPI) { + let i: number, + j: number, + cbs = {} as ModuleHooks; + + const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi; + + for (i = 0; i < hooks.length; ++i) { + cbs[hooks[i]] = []; + for (j = 0; j < modules.length; ++j) { + const hook = modules[j][hooks[i]]; + if (hook !== undefined) { + (cbs[hooks[i]] as Array).push(hook); + } + } + } + + function emptyNodeAt(elm: Element) { + const id = elm.id ? "#" + elm.id : ""; + const c = elm.className ? "." + elm.className.split(" ").join(".") : ""; + return vnode( + api.tagName(elm).toLowerCase() + id + c, + {}, + [], + undefined, + elm + ); + } + + function createRmCb(childElm: Node, listeners: number) { + return function rmCb() { + if (--listeners === 0) { + const parent = api.parentNode(childElm); + api.removeChild(parent, childElm); + } + }; + } + + function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { + let i: any, + data = vnode.data; + if (data !== undefined) { + if (isDef((i = data.hook)) && isDef((i = i.init))) { + i(vnode); + data = vnode.data; + } + } + let children = vnode.children, + sel = vnode.sel; + if (sel === "!") { + if (isUndef(vnode.text)) { + vnode.text = ""; + } + vnode.elm = api.createComment(vnode.text as string); + } 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 as VNodeData).ns)) + ? api.createElementNS(i, tag) + : api.createElement(tag)); + 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 (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 (primitive(vnode.text)) { + api.appendChild(elm, api.createTextNode(vnode.text)); + } + i = (vnode.data as VNodeData).hook; // Reuse variable + if (isDef(i)) { + if (i.create) i.create(emptyNode, vnode); + if (i.insert) insertedVnodeQueue.push(vnode); + } + } else { + vnode.elm = api.createTextNode(vnode.text as string); + } + return vnode.elm; + } + + function addVnodes( + parentElm: Node, + before: Node | null, + vnodes: Array, + 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) { + let i: any, + j: number, + data = vnode.data; + if (data !== undefined) { + if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode); + for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); + if (vnode.children !== undefined) { + for (j = 0; j < vnode.children.length; ++j) { + i = vnode.children[j]; + if (i != null && typeof i !== "string") { + invokeDestroyHook(i); + } + } + } + } + } + + function removeVnodes( + parentElm: Node, + vnodes: Array, + startIdx: number, + endIdx: number + ): void { + for (; startIdx <= endIdx; ++startIdx) { + let i: any, + listeners: number, + rm: () => void, + ch = vnodes[startIdx]; + if (ch != null) { + if (isDef(ch.sel)) { + invokeDestroyHook(ch); + listeners = cbs.remove.length + 1; + rm = createRmCb(ch.elm as Node, listeners); + for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); + if ( + isDef((i = ch.data)) && + isDef((i = i.hook)) && + isDef((i = i.remove)) + ) { + i(ch, rm); + } else { + rm(); + } + } else { + // Text node + api.removeChild(parentElm, ch.elm as Node); + } + } + } + } + + function updateChildren( + parentElm: Node, + oldCh: Array, + newCh: Array, + insertedVnodeQueue: VNodeQueue + ) { + let oldStartIdx = 0, + 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: any; + 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 as Node, + api.nextSibling(oldEndVnode.elm as Node) + ); + oldStartVnode = oldCh[++oldStartIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldEndVnode, newStartVnode)) { + // Vnode moved left + patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); + api.insertBefore( + parentElm, + oldEndVnode.elm as Node, + oldStartVnode.elm as Node + ); + 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 as Node + ); + newStartVnode = newCh[++newStartIdx]; + } else { + elmToMove = oldCh[idxInOld]; + if (elmToMove.sel !== newStartVnode.sel) { + api.insertBefore( + parentElm, + createElm(newStartVnode, insertedVnodeQueue), + oldStartVnode.elm as Node + ); + } else { + patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); + oldCh[idxInOld] = undefined as any; + api.insertBefore( + parentElm, + elmToMove.elm as Node, + oldStartVnode.elm as Node + ); + } + newStartVnode = newCh[++newStartIdx]; + } + } + } + if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { + if (oldStartIdx > oldEndIdx) { + before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; + addVnodes( + parentElm, + before, + newCh, + newStartIdx, + newEndIdx, + insertedVnodeQueue + ); + } else { + removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); + } + } + } + + function patchVnode( + oldVnode: VNode, + vnode: VNode, + insertedVnodeQueue: VNodeQueue + ) { + let i: any, hook: any; + if ( + isDef((i = vnode.data)) && + isDef((hook = i.hook)) && + isDef((i = hook.prepatch)) + ) { + i(oldVnode, vnode); + } + const elm = (vnode.elm = oldVnode.elm as Node); + let oldCh = oldVnode.children; + let ch = vnode.children; + if (oldVnode === vnode) return; + if (vnode.data !== undefined) { + for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); + i = vnode.data.hook; + if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode); + } + if (isUndef(vnode.text)) { + if (isDef(oldCh) && isDef(ch)) { + if (oldCh !== ch) + updateChildren( + elm, + oldCh as Array, + ch as Array, + insertedVnodeQueue + ); + } else if (isDef(ch)) { + if (isDef(oldVnode.text)) api.setTextContent(elm, ""); + addVnodes( + elm, + null, + ch as Array, + 0, + (ch as Array).length - 1, + insertedVnodeQueue + ); + } else if (isDef(oldCh)) { + removeVnodes( + elm, + oldCh as Array, + 0, + (oldCh as Array).length - 1 + ); + } else if (isDef(oldVnode.text)) { + api.setTextContent(elm, ""); + } + } else if (oldVnode.text !== vnode.text) { + if (isDef(oldCh)) { + removeVnodes( + elm, + oldCh as Array, + 0, + (oldCh as Array).length - 1 + ); + } + api.setTextContent(elm, vnode.text as string); + } + if (isDef(hook) && isDef((i = hook.postpatch))) { + i(oldVnode, vnode); + } + } + + return function patch(oldVnode: VNode | Element, 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 (!isVnode(oldVnode)) { + oldVnode = emptyNodeAt(oldVnode); + } + + if (sameVnode(oldVnode, vnode)) { + patchVnode(oldVnode, vnode, insertedVnodeQueue); + } else { + elm = oldVnode.elm as Node; + parent = api.parentNode(elm); + + createElm(vnode, insertedVnodeQueue); + + if (parent !== null) { + api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm)); + removeVnodes(parent, [oldVnode], 0, 0); + } + } + + for (i = 0; i < insertedVnodeQueue.length; ++i) { + (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)( + insertedVnodeQueue[i] + ); + } + for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); + return vnode; + }; +} + +//------------------------------------------------------------------------------ +// is.ts +//------------------------------------------------------------------------------ +const array = Array.isArray; +function primitive(s: any): s is string | number { + return typeof s === "string" || typeof s === "number"; +} + +//------------------------------------------------------------------------------ +// htmldomapi.ts +//------------------------------------------------------------------------------ +interface DOMAPI { + createElement: (tagName: any) => HTMLElement; + createElementNS: (namespaceURI: string, qualifiedName: string) => Element; + createTextNode: (text: string) => Text; + createComment: (text: string) => Comment; + insertBefore: ( + parentNode: Node, + newNode: Node, + referenceNode: Node | null + ) => void; + removeChild: (node: Node, child: Node) => void; + appendChild: (node: Node, child: Node) => void; + parentNode: (node: Node) => Node; + nextSibling: (node: Node) => Node; + tagName: (elm: Element) => string; + setTextContent: (node: Node, text: string | null) => void; + getTextContent: (node: Node) => string | null; + isElement: (node: Node) => node is Element; + isText: (node: Node) => node is Text; + isComment: (node: Node) => node is Comment; +} + +function createElement(tagName: any): HTMLElement { + return document.createElement(tagName); +} + +function createElementNS(namespaceURI: string, qualifiedName: string): Element { + return document.createElementNS(namespaceURI, qualifiedName); +} + +function createTextNode(text: string): Text { + return document.createTextNode(text); +} + +function createComment(text: string): Comment { + return document.createComment(text); +} + +function insertBefore( + parentNode: Node, + newNode: Node, + referenceNode: Node | null +): void { + parentNode.insertBefore(newNode, referenceNode); +} + +function removeChild(node: Node, child: Node): void { + node.removeChild(child); +} + +function appendChild(node: Node, child: Node): void { + node.appendChild(child); +} + +function parentNode(node: Node): Node | null { + return node.parentNode; +} + +function nextSibling(node: Node): Node | null { + return node.nextSibling; +} + +function tagName(elm: Element): string { + return elm.tagName; +} + +function setTextContent(node: Node, text: string | null): void { + node.textContent = text; +} + +function getTextContent(node: Node): string | null { + return node.textContent; +} + +function isElement(node: Node): node is Element { + return node.nodeType === 1; +} + +function isText(node: Node): node is Text { + return node.nodeType === 3; +} + +function isComment(node: Node): node is Comment { + return node.nodeType === 8; +} + +const htmlDomApi = { + createElement, + createElementNS, + createTextNode, + createComment, + insertBefore, + removeChild, + appendChild, + parentNode, + nextSibling, + tagName, + setTextContent, + getTextContent, + isElement, + isText, + isComment +} as DOMAPI; + +//------------------------------------------------------------------------------ +// hooks.ts +//------------------------------------------------------------------------------ +type PreHook = () => any; +type InitHook = (vNode: VNode) => any; +type CreateHook = (emptyVNode: VNode, vNode: VNode) => any; +type InsertHook = (vNode: VNode) => any; +type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any; +type UpdateHook = (oldVNode: VNode, vNode: VNode) => any; +type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any; +type DestroyHook = (vNode: VNode) => any; +type RemoveHook = (vNode: VNode, removeCallback: () => void) => any; +type PostHook = () => any; + +interface Hooks { + pre?: PreHook; + init?: InitHook; + create?: CreateHook; + insert?: InsertHook; + prepatch?: PrePatchHook; + update?: UpdateHook; + postpatch?: PostPatchHook; + destroy?: DestroyHook; + remove?: RemoveHook; + post?: PostHook; +} + +//------------------------------------------------------------------------------ +// h.ts +//------------------------------------------------------------------------------ +type VNodes = Array; +type VNodeChildElement = VNode | string | number | undefined | null; +type ArrayOrElement = T | T[]; +type VNodeChildren = ArrayOrElement; + +function addNS( + data: any, + children: VNodes | undefined, + sel: string | undefined +): void { + data.ns = "http://www.w3.org/2000/svg"; + if (sel !== "foreignObject" && children !== undefined) { + for (let i = 0; i < children.length; ++i) { + let childData = children[i].data; + if (childData !== undefined) { + addNS( + childData, + (children[i] as VNode).children as VNodes, + children[i].sel + ); + } + } + } +} + +export function h(sel: string): VNode; +export function h(sel: string, data: VNodeData): VNode; +export function h(sel: string, children: VNodeChildren): VNode; +export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode; +export function h(sel: any, b?: any, c?: any): VNode { + var data: VNodeData = {}, + children: any, + text: any, + i: number; + if (c !== undefined) { + data = b; + if (array(c)) { + children = c; + } else if (primitive(c)) { + text = c; + } else if (c && c.sel) { + children = [c]; + } + } else if (b !== undefined) { + if (array(b)) { + children = b; + } else if (primitive(b)) { + text = b; + } else if (b && b.sel) { + children = [b]; + } else { + data = b; + } + } + if (children !== undefined) { + for (i = 0; i < children.length; ++i) { + if (primitive(children[i])) + children[i] = vnode( + undefined, + undefined, + undefined, + children[i], + undefined + ); + } + } + if ( + sel[0] === "s" && + sel[1] === "v" && + sel[2] === "g" && + (sel.length === 3 || sel[3] === "." || sel[3] === "#") + ) { + addNS(data, children, sel); + } + return vnode(sel, data, children, text, undefined); +} + +//------------------------------------------------------------------------------ +// module/props.ts +//------------------------------------------------------------------------------ +type Props = Record; + +function updateProps(oldVnode: VNode, vnode: VNode): void { + var key: string, + cur: any, + old: any, + elm = vnode.elm, + oldProps = (oldVnode.data as VNodeData).props, + props = (vnode.data as VNodeData).props; + + if (!oldProps && !props) return; + if (oldProps === props) return; + oldProps = oldProps || {}; + props = props || {}; + + for (key in oldProps) { + if (!props[key]) { + delete (elm as any)[key]; + } + } + for (key in props) { + cur = props[key]; + old = oldProps[key]; + if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) { + (elm as any)[key] = cur; + } + } +} + +export const propsModule = { create: updateProps, update: updateProps } as Module; + +//------------------------------------------------------------------------------ +// module/module.ts +//------------------------------------------------------------------------------ +interface Module { + pre: PreHook; + create: CreateHook; + update: UpdateHook; + destroy: DestroyHook; + remove: RemoveHook; + post: PostHook; +} + +//------------------------------------------------------------------------------ +// module/eventlisteners.ts +//------------------------------------------------------------------------------ +type On = { + [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void +} & { + [event: string]: EventListener; +}; + +function invokeHandler(handler: any, vnode?: VNode, event?: Event): void { + if (typeof handler === "function") { + // call function handler + handler.call(vnode, event, vnode); + } else if (typeof handler === "object") { + // call handler with arguments + if (typeof handler[0] === "function") { + // special case for single argument for performance + if (handler.length === 2) { + handler[0].call(vnode, handler[1], event, vnode); + } else { + var args = handler.slice(1); + args.push(event); + args.push(vnode); + handler[0].apply(vnode, args); + } + } else { + // call multiple handlers + for (var i = 0; i < handler.length; i++) { + invokeHandler(handler[i], vnode, event); + } + } + } +} + +function handleEvent(event: Event, vnode: VNode) { + var name = event.type, + on = (vnode.data as VNodeData).on; + + // call event handler(s) if exists + if (on && on[name]) { + invokeHandler(on[name], vnode, event); + } +} + +function createListener() { + return function handler(event: Event) { + handleEvent(event, (handler as any).vnode); + }; +} + +function updateEventListeners(oldVnode: VNode, vnode?: VNode): void { + var oldOn = (oldVnode.data as VNodeData).on, + oldListener = (oldVnode as any).listener, + oldElm: Element = oldVnode.elm as Element, + on = vnode && (vnode.data as VNodeData).on, + elm: Element = (vnode && vnode.elm) as Element, + name: string; + + // optimization for reused immutable handlers + if (oldOn === on) { + return; + } + + // remove existing listeners which no longer used + if (oldOn && oldListener) { + // if element changed or deleted we remove all existing listeners unconditionally + if (!on) { + for (name in oldOn) { + // remove listener if element was changed or existing listeners removed + oldElm.removeEventListener(name, oldListener, false); + } + } else { + for (name in oldOn) { + // remove listener if existing listener removed + if (!on[name]) { + oldElm.removeEventListener(name, oldListener, false); + } + } + } + } + + // add new listeners which has not already attached + if (on) { + // reuse existing listener or create new + var listener = ((vnode as any).listener = + (oldVnode as any).listener || createListener()); + // update vnode for listener + listener.vnode = vnode; + + // if element changed or added we add all needed listeners unconditionally + if (!oldOn) { + for (name in on) { + // add listener if element was changed or new listeners added + elm.addEventListener(name, listener, false); + } + } else { + for (name in on) { + // add listener if new listener added + if (!oldOn[name]) { + elm.addEventListener(name, listener, false); + } + } + } + } +} + +export const eventListenersModule = { + create: updateEventListeners, + update: updateEventListeners, + destroy: updateEventListeners +} as Module; + + +//------------------------------------------------------------------------------ +// attributes.ts +//------------------------------------------------------------------------------ +type Attrs = Record; + +const xlinkNS = "http://www.w3.org/1999/xlink"; +const xmlNS = "http://www.w3.org/XML/1998/namespace"; +const colonChar = 58; +const xChar = 120; + +function updateAttrs(oldVnode: VNode, vnode: VNode): void { + var key: string, + elm: Element = vnode.elm as Element, + oldAttrs = (oldVnode.data as VNodeData).attrs, + attrs = (vnode.data as VNodeData).attrs; + + if (!oldAttrs && !attrs) return; + if (oldAttrs === attrs) return; + oldAttrs = oldAttrs || {}; + attrs = attrs || {}; + + // update modified attributes, add new attributes + for (key in attrs) { + const cur = attrs[key]; + const old = oldAttrs[key]; + if (old !== cur) { + if (cur === true) { + elm.setAttribute(key, ""); + } else if (cur === false) { + elm.removeAttribute(key); + } else { + if (key.charCodeAt(0) !== xChar) { + elm.setAttribute(key, cur); + } else if (key.charCodeAt(3) === colonChar) { + // Assume xml namespace + elm.setAttributeNS(xmlNS, key, cur); + } else if (key.charCodeAt(5) === colonChar) { + // Assume xlink namespace + elm.setAttributeNS(xlinkNS, key, cur); + } else { + elm.setAttribute(key, cur); + } + } + } + } + // remove removed attributes + // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) + // the other option is to remove all attributes with value == undefined + for (key in oldAttrs) { + if (!(key in attrs)) { + elm.removeAttribute(key); + } + } +} + +export const attrsModule = { + create: updateAttrs, + update: updateAttrs +} as Module; + +