/* * v2dom.ts * Copyright (C) 2024 veypi * 2024-10-23 15:54 * Distributed under terms of the GPL license. */ import proxy from "./proxy" interface buildOpts { id?: string typ?: string class?: string attrs?: attrs style?: string innerHtml?: string onclick?: any children?: childTyp[] updator?: (p: HTMLElement) => void vbind?: [Object, string] } const typs = ['div', 'img', 'span', 'p', 'a', 'input'] type iterChild = [T[], (d: T) => HTMLElement] type childTyp = HTMLElement | iterChild type computedFn = () => any type attrs = { [key: string]: string | number | computedFn } export default (opts: buildOpts | string, inner?: string | computedFn | childTyp[], attrs?: attrs) => { if (typeof opts === 'string') { if (typs.indexOf(opts) >= 0) { opts = { typ: opts } } else { opts = { class: opts } } } let dom = document.createElement(opts.typ || 'div') if (inner) { if (typeof inner == 'string') { opts.innerHtml = inner } else if (typeof inner == 'function') { addListener(dom, () => { let res = inner() if (typeof res === 'string') { dom.innerHTML = res } else { dom.innerHTML = '' dom.appendChild(inner()) } }) } else { opts.children = inner } } if (attrs) { opts.attrs = attrs } if (opts.id) { dom.id = opts.id } if (opts.class) { dom.classList.add(...opts.class.split(' ')) } if (opts.innerHtml) { dom.innerHTML = opts.innerHtml } if (opts.attrs) { for (let a in opts.attrs) { let attr = opts.attrs[a] if (typeof attr === 'function') { addListener(dom, () => { dom.setAttribute(a, attr()) }) } else { dom.setAttribute(a, String(attr)) } } } if (opts.onclick) { dom.onclick = opts.onclick } if (opts.children) { let tmpID = '' for (let c of opts.children) { if (Object.prototype.toString.call(c) === '[object Array]') { const iterID = proxy.generateUniqueId() const iterLast = tmpID addListener(dom, () => { c = c as iterChild let itemIDs: string[] = [] for (let i = 0; i < c[0].length; i++) { let line = c[0][i] //@ts-ignore let itemID = line[proxy.DataID] || '' let nextID = '' if (i < c[0].length - 1) { //@ts-ignore nextID = c[0][i + 1][proxy.DataID] || '' } itemIDs.push(itemID) let inserted = -2 let subdom: HTMLElement | null = null let beforeChild: HTMLElement | null = null for (let childID = 0; childID < dom.children.length; childID++) { let child = dom.children[childID] // console.log(`${inserted} ${childID} ${itemID} ${nextID}`) if (child.getAttribute('vbind-iter') === iterID) { if (child.getAttribute('vbind-proxy') === itemID) { inserted = childID subdom = child as HTMLElement } else if (child.getAttribute('vbind-proxy') === nextID && nextID != '') { // 用于交换位置 if (inserted != childID - 1) { beforeChild = child as HTMLElement } } } else if (child.getAttribute('vbind-iterlast') === iterID) { if (inserted == -2) { beforeChild = child as HTMLElement } // 找到第一个不是这个循环块的dom元素时退出 break } } if (inserted == -2) { subdom = c[1](line) subdom.setAttribute('vbind-proxy', itemID) subdom.setAttribute('vbind-iter', iterID) if (iterLast != '') { subdom.setAttribute('vbind-iterlast', iterLast) } if (beforeChild) { dom.insertBefore(subdom, beforeChild) // console.log(`${iterID} insert new`, subdom) } else { // console.log(`${iterID} append new`, subdom) dom.appendChild(subdom) } } else if (beforeChild) { // console.log(`${iterID} insert old`, subdom) dom.insertBefore(subdom!, beforeChild) } else { // console.log(`${iterID} none`, subdom) } } for (let child of dom.children) { if (child.getAttribute('vbind-iter') === iterID && itemIDs.indexOf(child.getAttribute('vbind-proxy') || '') == -1) { dom.removeChild(child) } } }) tmpID = iterID } else { c = c as HTMLElement if (tmpID != '') { c.setAttribute('vbind-iterlast', tmpID) } dom.appendChild(c) } } } if (opts.style) { const regex = /([a-zA-Z-]+)\s*:\s*([^;]+);?/g; let match: any while ((match = regex.exec(opts.style)) !== null) { const key = match[1].trim(); const value = match[2].trim(); dom.style.setProperty(key, value) } } dom.setAttribute('voa', '1') if (opts.updator) { addListener(dom, () => { opts.updator!(dom) }) } if (opts.vbind && opts.typ === 'input') { dom.setAttribute('value', Reflect.get(opts.vbind[0], opts.vbind[1])) dom.addEventListener('input', (e) => { // @ts-ignore Reflect.set(opts.vbind[0], opts.vbind[1], e.target.value) }) } return dom } function addListener(dom: HTMLElement, fn: () => void) { let fns = dom.getAttribute('vbind-fns') let fnid = proxy.Listen(fn) if (fns) { fns += ',' + fnid } else { fns = String(fnid) } dom.setAttribute('vbind-fns', fns) }