/* * 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 vclass?: [() => string] attrs?: attrs style?: string onclick?: any children?: childTyp[] | childTyp updator?: (p: HTMLElement) => void vbind?: [Object, string] } const typs = ['div', 'img', 'span', 'p', 'a', 'input'] type vforChild = { obj: T[], fn: (d: T, idx?: number) => HTMLElement, iterID: string, ifnone: HTMLElement } type vIfCondition = boolean | number | string | undefined | null | (() => vIfCondition) type vIfStatment = HTMLElement | (() => HTMLElement) type vIfChild = { ifID: string, rules: [vIfCondition, vIfStatment][] } type computedFn = () => number | string | HTMLElement | vforChild | [] type childTyp = HTMLElement | vforChild | string | number | computedFn | vIfChild type attrs = { [key: string]: string | number | (() => string) } export const v = (opts: buildOpts | string, children?: childTyp | childTyp[], attrs?: attrs) => { if (typeof opts === 'string') { if (typs.indexOf(opts) >= 0) { opts = { typ: opts } } else { opts = { class: opts } } } if (children) { opts.children = children } if (attrs) { opts.attrs = attrs } return vbase(opts) } export const vspan = (content: string, Class?: string, attrs?: attrs) => { return vbase({ typ: 'span', class: Class, children: [content], attrs }) } export const vbase = (opts: buildOpts) => { let dom = document.createElement(opts.typ || 'div') if (opts.id) { dom.id = opts.id } if (opts.class) { dom.classList.add(...opts.class.split(' ')) } if (opts.vclass) { for (let vc of opts.vclass) { let oldvc: string[] = [] proxy.DomListen(dom, () => { dom.classList.remove(...oldvc) oldvc = vc().split(' ') dom.classList.add(...oldvc) }) } } if (opts.attrs) { for (let a in opts.attrs) { let attr = opts.attrs[a] if (typeof attr === 'function') { proxy.DomListen(dom, () => { dom.setAttribute(a, attr()) }) } else { dom.setAttribute(a, String(attr)) } } } if (opts.onclick) { dom.onclick = opts.onclick } if (opts.children) { handleChild(dom, opts.children, true) } 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) { proxy.DomListen(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 handleChild(dom: HTMLElement, c: any, is_only = false, is_listen = false) { if (is_only) { dom.innerHTML = '' } if (typeof c === 'function') { if (!is_listen) { proxy.DomListen(dom, () => { handleChild(dom, c(), is_only, true) }) } else { handleChild(dom, c(), is_only, true) } } else if (typeof c === 'string' || typeof c === 'number') { dom.innerHTML = String(c) } else if (Array.isArray(c)) { for (let cc of c) { handleChild(dom, cc, is_only && c.length === 1, is_listen) } } else if (c && c.iterID) { handleChildVfor(dom, c as vforChild, is_listen) } else if (c && c.ifID) { handleChildVif(dom, c as vIfChild, is_listen) } else if (c instanceof Element) { dom.appendChild(c) } else if (typeof c === 'undefined' || c === null) { } else { console.warn('unknown child type', c) } } function handleChildVif(dom: HTMLElement, data: vIfChild, is_listen = false) { let fn = () => { let target = emptyDom() for (let r of data.rules) { let c = Boolean(r[0]) if (typeof r[0] === 'function') { c = Boolean(r[0]()) } if (c) { if (typeof r[1] === 'function') { target = r[1]() } else { target = r[1] } break } } let before: Element | null = null for (let domc of dom.children) { if (domc.getAttribute('vbind-if') === data.ifID) { before = domc } } target.setAttribute('vbind-if', data.ifID) if (before) { dom.replaceChild(target, before) } else { dom.appendChild(target) } } if (is_listen) { fn() } else { proxy.DomListen(dom, fn) } } export const vif = (...rules: [vIfCondition, vIfStatment][]): vIfChild => { let ifID = proxy.GenUniqueID() return { ifID, rules } } export const vfor = (obj: T[], fn: (d: T, idx?: number) => HTMLElement, ifnone?: () => HTMLElement, iterID?: string): vforChild => { if (!iterID) { iterID = proxy.GenUniqueID() } let dom = document.createElement('div') as HTMLElement dom.style.display = 'none' dom.style.visibility = 'hidden' dom.style.height = '0' dom.style.width = '0' if (ifnone) { dom = ifnone() } dom.setAttribute('vbind-iternone', iterID) return { obj, fn, iterID, ifnone: dom } } function handleChildVfor(dom: HTMLElement, data: vforChild, is_listen = false) { let fn = () => { let itemIDs: string[] = [] for (let i = 0; i < data.obj.length; i++) { let line = data.obj[i] let itemID = line[proxy.DataID] || '' let nextID = '' if (i < data.obj.length - 1) { nextID = data.obj[i + 1][proxy.DataID] || '' } itemIDs.push(itemID) let inserted = -1 let subdom: HTMLElement | null = null let beforeChild: HTMLElement | null = null let iterFound = false 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') === data.iterID) { iterFound = true if (child.getAttribute('vbind-iteridx') === itemID) { // 找到已存在dom inserted = childID subdom = child as HTMLElement } else if (child.getAttribute('vbind-iteridx') === nextID && nextID != '') { // 用于交换位置 beforeChild = child as HTMLElement } } else if (child.getAttribute('vbind-iternone') === data.iterID) { if (inserted == -1) { beforeChild = child as HTMLElement } } else if (iterFound) { if (!beforeChild) { beforeChild = child as HTMLElement } break } } if (inserted == -1) { subdom = data.fn(line, i) subdom.setAttribute('vbind-iteridx', itemID) subdom.setAttribute('vbind-iter', data.iterID) 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) } } let noneExist = false let removeChilds: Element[] = [] for (let child of dom.children) { if (child.getAttribute('vbind-iter') === data.iterID && itemIDs.indexOf(child.getAttribute('vbind-iteridx') || '') == -1) { removeChilds.push(child) } if (child.getAttribute('vbind-iternone') === data.iterID) { noneExist = true } } removeChilds.forEach(e => e.remove()) if (data.obj.length == 0) { if (!noneExist) { dom.appendChild(data.ifnone) } } else { if (noneExist) { dom.removeChild(data.ifnone) } } } if (is_listen) { fn() } else { proxy.DomListen(dom, fn) } } function emptyDom() { let dom = document.createElement('div') as HTMLElement dom.style.display = 'none' dom.style.visibility = 'hidden' dom.style.height = '0' dom.style.width = '0' return dom }