You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/oaer/lib/v2dom/v2dom.ts

241 lines
6.7 KiB
TypeScript

/*
* v2dom.ts
* Copyright (C) 2024 veypi <i@veypi.com>
* 2024-10-23 15:54
* Distributed under terms of the GPL license.
*/
import proxy from "./proxy"
interface buildOpts<T> {
id?: string
typ?: string
class?: string
vclass?: [() => string]
attrs?: attrs
style?: string
onclick?: any
children?: childTyp<T>[] | childTyp<T>
updator?: (p: HTMLElement) => void
vbind?: [Object, string]
}
const typs = ['div', 'img', 'span', 'p', 'a', 'input']
type vforChild<T> = { obj: T[], fn: (d: T, idx?: number) => HTMLElement, iterID: string, ifnone: HTMLElement }
type computedFn = () => string | HTMLElement
type childTyp<T> = HTMLElement | vforChild<T> | string | computedFn
type attrs = { [key: string]: string | number | (() => string) }
export const v = <T>(opts: buildOpts<T> | string, children?: childTyp<T> | childTyp<T>[], 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 = <T>(opts: buildOpts<T>) => {
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<any>, is_listen)
} else if (c instanceof Element) {
dom.appendChild(c)
} else if (typeof c === 'undefined' || c === null) {
} else {
console.warn('unknown child type', c)
}
}
export const vfor = <T>(obj: T[], fn: (d: T, idx?: number) => HTMLElement, ifnone?: () => HTMLElement): vforChild<T> => {
let 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<any>, 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
for (let child of dom.children) {
if (child.getAttribute('vbind-iter') === data.iterID && itemIDs.indexOf(child.getAttribute('vbind-iteridx') || '') == -1) {
dom.removeChild(child)
}
if (child.getAttribute('vbind-iternone') === data.iterID) {
noneExist = true
}
}
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)
}
}