UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

322 lines (293 loc) 10.1 kB
/** * Virtual DOM patching * Modified from snabbdom https://github.com/snabbdom/snabbdom/blob/master/src/init.ts * * The design has been simplified to focus on the purpose in SVG rendering in SVG. * * Licensed under the MIT License * https://github.com/paldepind/snabbdom/blob/master/LICENSE */ import { isArray, isObject } from '../core/util'; import { createElement, createVNode, SVGVNode, XMLNS, XML_NAMESPACE, XLINKNS } from './core'; import * as api from './domapi'; const colonChar = 58; const xChar = 120; const emptyNode = createVNode('', ''); type NonUndefined<T> = T extends undefined ? never : T; function isUndef(s: any): boolean { return s === undefined; } function isDef<A>(s: A): s is NonUndefined<A> { return s !== undefined; } function createKeyToOldIdx( children: SVGVNode[], beginIdx: number, endIdx: number ): KeyToIndexMap { const map: KeyToIndexMap = {}; for (let i = beginIdx; i <= endIdx; ++i) { const key = children[i].key; if (key !== undefined) { if (process.env.NODE_ENV !== 'production') { if (map[key] != null) { console.error(`Duplicate key ${key}`); } } map[key] = i; } } return map; } function sameVnode(vnode1: SVGVNode, vnode2: SVGVNode): boolean { const isSameKey = vnode1.key === vnode2.key; const isSameTag = vnode1.tag === vnode2.tag; return isSameTag && isSameKey; } type KeyToIndexMap = { [key: string]: number }; function createElm(vnode: SVGVNode): Node { let i: any; const children = vnode.children; const tag = vnode.tag; // if (tag === '!') { // if (isUndef(vnode.text)) { // vnode.text = ''; // } // vnode.elm = api.createComment(vnode.text!); // } // else if (isDef(tag)) { const elm = (vnode.elm = createElement(tag)); updateAttrs(emptyNode, vnode); if (isArray(children)) { for (i = 0; i < children.length; ++i) { const ch = children[i]; if (ch != null) { api.appendChild(elm, createElm(ch)); } } } else if (isDef(vnode.text) && !isObject(vnode.text)) { api.appendChild(elm, api.createTextNode(vnode.text)); } } else { vnode.elm = api.createTextNode(vnode.text!); } return vnode.elm; } function addVnodes( parentElm: Node, before: Node | null, vnodes: SVGVNode[], startIdx: number, endIdx: number ) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (ch != null) { api.insertBefore(parentElm, createElm(ch), before); } } } function removeVnodes(parentElm: Node, vnodes: SVGVNode[], startIdx: number, endIdx: number): void { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (ch != null) { if (isDef(ch.tag)) { const parent = api.parentNode(ch.elm); api.removeChild(parent, ch.elm); } else { // Text node api.removeChild(parentElm, ch.elm!); } } } } export function updateAttrs(oldVnode: SVGVNode, vnode: SVGVNode): void { let key: string; const elm = vnode.elm as SVGElement; const oldAttrs = oldVnode && oldVnode.attrs || {}; const attrs = vnode.attrs || {}; if (oldAttrs === attrs) { return; } // update modified attributes, add new attributes // eslint-disable-next-line 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 === 'style') { elm.style.cssText = cur as string; } else if (key.charCodeAt(0) !== xChar) { elm.setAttribute(key, cur as any); } // TODO else if (key === 'xmlns:xlink' || key === 'xmlns') { elm.setAttributeNS(XMLNS, key, cur as any); } else if (key.charCodeAt(3) === colonChar) { // Assume xml namespace elm.setAttributeNS(XML_NAMESPACE, key, cur as any); } else if (key.charCodeAt(5) === colonChar) { // Assume xlink namespace elm.setAttributeNS(XLINKNS, key, cur as any); } else { elm.setAttribute(key, cur as any); } } } } // 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); } } } function updateChildren(parentElm: Node, oldCh: SVGVNode[], newCh: SVGVNode[]) { 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: SVGVNode; 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); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode); 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); api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = oldKeyToIdx[newStartVnode.key]; if (isUndef(idxInOld)) { // New element api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm!); } else { elmToMove = oldCh[idxInOld]; if (elmToMove.tag !== newStartVnode.tag) { api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm!); } else { patchVnode(elmToMove, newStartVnode); oldCh[idxInOld] = undefined; api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!); } } 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); } else { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } } } function patchVnode(oldVnode: SVGVNode, vnode: SVGVNode) { const elm = (vnode.elm = oldVnode.elm)!; const oldCh = oldVnode.children; const ch = vnode.children; if (oldVnode === vnode) { return; } updateAttrs(oldVnode, vnode); if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) { updateChildren(elm, oldCh, ch); } } else if (isDef(ch)) { if (isDef(oldVnode.text)) { api.setTextContent(elm, ''); } addVnodes(elm, null, ch, 0, ch.length - 1); } 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!); } } export default function patch(oldVnode: SVGVNode, vnode: SVGVNode): SVGVNode { if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode); } else { const elm = oldVnode.elm!; const parent = api.parentNode(elm); createElm(vnode); if (parent !== null) { api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } } return vnode; }