mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
319 lines
12 KiB
TypeScript
Executable File
319 lines
12 KiB
TypeScript
Executable File
/* global module, document, Node */
|
|
import {Module} from './modules/module';
|
|
import {Hooks} from './hooks';
|
|
import vnode, {VNode, VNodeData, Key} from './vnode';
|
|
import * as is from './is';
|
|
import htmlDomApi, {DOMAPI} from './htmldomapi';
|
|
|
|
function isUndef(s: any): boolean { return s === undefined; }
|
|
function isDef(s: any): boolean { return s !== undefined; }
|
|
|
|
type VNodeQueue = Array<VNode>;
|
|
|
|
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<T> = {
|
|
[K in keyof T]: (T[K])[];
|
|
}
|
|
|
|
type ModuleHooks = ArraysOf<Module>;
|
|
|
|
function createKeyToOldIdx(children: Array<VNode>, 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 {h} from './h';
|
|
export {thunk} from './thunk';
|
|
|
|
export function init(modules: Array<Partial<Module>>, 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<any>).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 (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));
|
|
}
|
|
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<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) {
|
|
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<VNode>,
|
|
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<VNode>,
|
|
newCh: Array<VNode>,
|
|
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<VNode>, ch as Array<VNode>, insertedVnodeQueue);
|
|
} else if (isDef(ch)) {
|
|
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
|
|
addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
|
|
} else if (isDef(oldCh)) {
|
|
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
|
|
} else if (isDef(oldVnode.text)) {
|
|
api.setTextContent(elm, '');
|
|
}
|
|
} else if (oldVnode.text !== vnode.text) {
|
|
if (isDef(oldCh)) {
|
|
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).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;
|
|
};
|
|
}
|