/* * v.js * Copyright (C) 2024 veypi * * Distributed under terms of the GPL license. */ import vproxy from './vproxy.js' import vget from './vget.js' import vrouter from './vrouter.js' import utils from './utils.js' import setupVdev from './vdev.js' (async function() { const globalStyle = document.createElement('style') globalStyle.innerHTML = ` [vref] { display: block; } [vparsing] { display: none; -webkit-text-fill-color: transparent; } vslot, vrouter { display: block; } ` if (document.head.firstChild) { document.head.insertBefore(globalStyle, document.head.firstChild) } else { document.head.appendChild(globalStyle) } const DelayCache = [] const config = { attributes: false, childList: true, subtree: true, characterData: false } const runVdelay = (d) => { if (!d.isConnected) { return } let delay = d.getAttribute('vdelay') if (delay) { let fc = DelayCache[delay] if (fc) { fc(d) } else { console.error('delay not found:', delay, d) } } } const callback = function(mutationsList, observer) { mutationsList.forEach(function(mutation) { for (let node of mutation.addedNodes) { if (node.nodeType === 1) { // 元素节点 runVdelay(node) node.querySelectorAll('*[vdelay]').forEach(runVdelay) } } }) } const observer = new MutationObserver(callback); observer.observe(document.body, config); function recordGet(code, data) { code = `with (sandbox) { ${code} }` const fn = new Function('sandbox', code); let res = vproxy.Wrap({}) let keys = [] const proxy = new Proxy(data, { // 拦截所有属性,防止到 Proxy 对象以外的作用域链查找。 has(target, key) { return true; }, get(target, key, receiver) { if (key === Symbol.unscopables) { return undefined; } let v = Reflect.get(target, key, receiver); if (keys.indexOf(key) === -1) { keys.push(key) } return v }, set(target, key, newValue, receiver) { return false } }); fn(proxy) vproxy.Watch(() => { keys.forEach(k => { if (res[k] !== data[k]) { res[k] = data[k] } }) }) return res } function findLastAccess(code, data) { code = `with (sandbox) { ${code} }` const fn = new Function('sandbox', code); let res = { data: null, key: null, } const wrap = (tmp) => { return new Proxy(tmp, { // 拦截所有属性,防止到 Proxy 对象以外的作用域链查找。 has(target, key) { return true; }, get(target, key, receiver) { if (key === Symbol.unscopables) { return undefined; } let v = Reflect.get(target, key, receiver); res.data = target res.key = key if (typeof v === 'function') { console.warn('vhtml not support function with "v:" variables bind') } if (typeof v === 'object' && v) { return wrap(v) } return v }, set(target, key, newValue, receiver) { // console.log('set', target, key, newValue) // return Reflect.set(target, key, newValue, receiver); return false } }) } fn(wrap(data)) return res } const varRegex = /{{|}}/g; const vforRegex = /^(\s*(\w+)\s+in\s+|\((\w+),\s*(\w+)\)\s+in\s+)([\w\?\$\.\[\]\(\)'"]+)$/ class vhtml { /** @type {HTMLElement} */ app = null scoped = null vget = vget vproxy = vproxy __noproxy = true $router = vrouter.$router constructor(id) { if (typeof id === 'string') { this.app = document.getElementById(id) } else if (id instanceof HTMLElement) { this.app = id } else { this.app = document.body } if (!this.app) { console.error(`Can't find element by id: ${id}`) return } let init = async () => { // vget.SetBaseFile(await vget.FetchFile(window.location.pathname)) let mainParser = await vget.FetchUI(window.location.pathname, {}, true) this.scoped = mainParser.env?.scoped || '' if (mainParser.env?.vdev && window.self !== window.top) { setupVdev() } this.parseRef('root', this.app, {}, mainParser.env || {}, mainParser, true) } init() } /** * @param{HTMLElement} dom * @param {Object} scopedData * */ async parseDom(dom, scopedData = {}, env) { if (env instanceof HTMLElement) { console.log(env) throw new Error('env error') } let nodeName = dom.nodeName.toLowerCase() if (dom.nodeType === 3) { this.parseTextNode(dom, scopedData, env) return } else if (dom.nodeType === 8) { // comment node return } else if (dom.nodeType !== 1) { console.log('Other Node Type:', dom.nodeType, dom); return } if (dom.hasAttribute('no-vhtml') || dom.vparsed) { return } let vfortxt = dom.getAttribute('v-for') if (vfortxt !== null) { this.parseVfor(vfortxt, dom, scopedData, env) return } if (nodeName.indexOf('-') !== -1) { let url = '/' + nodeName.split('-').join('/') let singleMode = dom.hasAttribute('single') this.parseRef(url, dom, scopedData, env, null, singleMode) dom.vparsed = true return } if (dom.getAttribute(':vsrc')) { let code = dom.getAttribute(':vsrc') dom.removeAttribute(':vsrc') let attrs = Array.from(dom.attributes).map(a => { let res = { name: a.name, value: a.value } return res }) let oldChilds = Array.from(dom.childNodes) vproxy.Watch(() => { delete dom.vparsed dom.setAttribute('vparsing', '') let vsrc = vproxy.Run(code, scopedData, env) if (!vsrc) { return } Array.from(dom.attributes).forEach(a => { dom.removeAttribute(a.name) }) dom.innerHTML = '' attrs.forEach(a => { dom.setAttribute(a.name, a.value) }) oldChilds.forEach(c => { dom.appendChild(c.cloneNode(true)) }) this.parseRef(vsrc, dom, scopedData, env, null, false) dom.vparsed = true }) return } if (dom.getAttribute('vsrc')) { let singleMode = dom.hasAttribute('single') this.parseRef(dom.getAttribute('vsrc'), dom, scopedData, env, null, singleMode) dom.vparsed = true return } if (nodeName === 'div' && dom.getAttribute('v-html')) { let vhtmlCode = dom.getAttribute('v-html') dom.removeAttribute('v-html') dom.innerHTML = '' this.parseAttrs(dom, scopedData, env) dom.vparsed = true vproxy.Watch(() => { let innerHTML = vproxy.Run(vhtmlCode, scopedData, env) dom.innerHTML = innerHTML let childs = this.parseVif(Array.from(dom.childNodes), scopedData, env) for (let n of childs) { this.parseDom(n, scopedData, env) } }) return } if (nodeName === 'vslot') { this.parseSlots(dom, scopedData, env) dom.vparsed = true return } if (nodeName === 'vrouter') { this.parseAttrs(dom, scopedData, env) vrouter.$router.ParseVrouter(this, dom, env) return } this.parseAttrs(dom, scopedData, env) let childs = this.parseVif(Array.from(dom.childNodes), scopedData, env) for (let n of childs) { this.parseDom(n, scopedData, env) } dom.vparsed = true } onMountedRun(dom, cb, once = true) { if (once) { if (dom.isConnected) { cb(dom) return } let did = DelayCache.push((dom) => { dom.removeAttribute('vdelay') cb(dom) }) dom.setAttribute('vdelay', did - 1) return } if (dom.isConnected) { cb(dom) } let did = DelayCache.push(cb) dom.setAttribute('vdelay', did - 1) } /** * @param{string} name * @param{HTMLElement} dom * */ AllENVs = {} async parseRef(vsrc, dom, data, env, target, singleMode = false) { dom.setAttribute('vparsing', '') let oldEnv = env let vrefof = dom.getAttribute('vrefof') let parentRef = dom.closest(`*[vref='${vrefof}']`) if (parentRef) { env = parentRef.$env } else { // console.log('parentRef not found:', vrefof,vsrc) } if (!target && vsrc) { if (!vsrc.endsWith('.html')) { vsrc = vsrc + '.html' } target = await vget.FetchUI(vsrc, env, dom.hasAttribute('scoped')) } env = Object.assign({}, env, target?.env || {}) // env = target.env dom.$env = env dom.$vsrc = vsrc env.$router = vrouter.$router env.$emit = (evt, ...args) => { evt = evt.toLowerCase() if (!dom.$vevent) { return } let fc = dom.$vevent[evt] if (fc && typeof fc === 'function') { fc(...args) } } let originData = await this.setupRef(dom, data, oldEnv, target, singleMode) if (singleMode) { this.parseAttrs(dom, originData, env, target?.customAttrs) } else { this.parseAttrs(dom, data, oldEnv, target?.customAttrs) } let childs = this.parseVif(Array.from(dom.childNodes), originData, env) for (let n of childs) { this.parseDom(n, originData, env) } dom.removeAttribute('vparsing') this.mountRef(dom, originData, env, target) } /** * @param {HTMLElement} dom * @parm {Object} data * @param {{heads:HTMLElement[],body:HTMLElement,scripts:HTMLElement[],setup:HTMLElement}} target * */ async setupRef(dom, data, env, target, singleMode = false) { let originData = vproxy.Wrap({}) if (target.setup) { let s = target.setup.innerHTML s = await vproxy.ParseImport(s, originData, dom.$env, dom.$vsrc) await vproxy.AsyncRun(s, originData, dom.$env, { $node: dom }) } dom.$refScope = data dom.$refData = originData if (singleMode) { return originData } if (!dom.$refSlots) { // dom 预定义好内容 let slots = vproxy.Wrap({}) dom.childNodes.forEach((n) => { let nName = n.getAttribute ? n.getAttribute('vslot') : '' nName = nName || '' if (!slots[nName]) { slots[nName] = [] } slots[nName].push(n) }) dom.$refSlots = slots } dom.innerHTML = '' let now = target.body.cloneNode(true) dom.append(...now.childNodes) // 处理调用方attr 传参 Object.keys(originData).forEach(k => { const localK = utils.CamelToKebabCase(k) if (typeof originData[k] === 'boolean') { if (dom.hasAttribute(k) || dom.hasAttribute(localK)) { originData[k] = true } } else if (dom.hasAttribute(k)) { originData[k] = dom.getAttribute(k) dom.removeAttribute(k) } else if (dom.hasAttribute(localK)) { originData[k] = dom.getAttribute(localK) dom.removeAttribute(localK) } if (dom.hasAttribute(':' + k) || dom.hasAttribute(':' + localK)) { let val = dom.getAttribute(':' + k) || dom.getAttribute(':' + localK) dom.removeAttribute(':' + k) dom.removeAttribute(':' + localK) // 此时数据还没有监听,删除旧对象,使得外部的对象直接绑定在内部$data上 delete originData[k] if (val) { vproxy.Watch(() => vproxy.Run(val, data, env), () => { originData[k] = vproxy.Run(val, data, env) }, { deep: true }) } else { vproxy.Watch(() => data[k], () => { originData[k] = data[k] }, { deep: true }) } } if (dom.hasAttribute('v:' + k) || dom.hasAttribute('v:' + localK)) { let val = dom.getAttribute('v:' + k) || dom.getAttribute('v:' + localK) dom.removeAttribute('v:' + k) dom.removeAttribute('v:' + localK) if (!val) { val = k } delete originData[k] vproxy.Watch(() => findLastAccess(val, data), (args) => { if (args && args.data && args.key) { originData[k] = args.data[args.key] } else { console.warn('not found variables in:' + val) } }) vproxy.Watch(() => originData[k], () => { let args = findLastAccess(val, data) if (args && args.data && args.key) { args.data[args.key] = originData[k] } else { console.warn('not found variables in:' + val) } }) } }) // handle local component attr let attrs = Array.from(now.attributes) attrs = attrs.filter(a => { if (this.parseAttr(dom, a.name, a.value, originData, env)) { now.removeAttribute(a.name) return false } return true }) attrs.forEach((a) => { if (a.name === 'class') { dom.classList.add(...a.value.trim().split(/\s+/)) } else if (a.name === 'style') { let styles = a.value.split(';') for (let s of styles) { let ss = s.split(':') if (ss.length === 2 && !dom.style[ss[0]]) { let skey = ss[0].trim() if (skey.startsWith('--')) { dom.style.setProperty(skey, ss[1].trim()) } else { dom.style[skey] = ss[1].trim() } } } } else if (!dom.getAttribute(a.name)) { dom.setAttribute(a.name, a.value) } }) return originData } async mountRef(dom, scopedData, env, target) { for (let s of target.scripts) { if (s.hasAttribute('active')) { this.onMountedRun(dom, () => { vproxy.AsyncRun(s.innerHTML, scopedData, env, { $node: dom, $watch: vproxy.Watch }) }, false) } else { vproxy.AsyncRun(s.innerHTML, scopedData, env, { $node: dom, $watch: vproxy.Watch }) } } } parseAttrs(dom, data, env, attrs) { if (dom.nodeName === 'A') { this.parseAHref(dom, data, env) } Array.from(dom.attributes).forEach(a => { if (this.parseAttr(dom, a.name, a.value, data, env)) { dom.removeAttribute(a.name) } }) if (attrs) { // just for body element Object.keys(attrs).forEach(k => { this.parseAttr(dom, k, attrs[k], dom.$refData, env) }) } if (dom.hasAttribute('v-show')) { let code = dom.getAttribute('v-show') let oldDisplay = dom.style.display vproxy.Watch(() => { let res = vproxy.Run(code, data, env) if (res) { dom.style.display = oldDisplay } else { dom.style.display = 'none' } }) } } parseAHref(dom, data, env) { if (!dom.hasAttribute("href") && !dom.hasAttribute(":href")) { return } if (dom.hasAttribute(":href")) { let code = dom.getAttribute(":href") dom.removeAttribute(":href") vproxy.Watch(() => { let href = vproxy.Run(code, data, env) console.log(code, href, env) if (!href || href.startsWith('#') || href.startsWith('http')) { return } else if (href.startsWith('@')) { dom.setAttribute('href', href.slice(1)) } else if (href) { let scoped = env?.scoped if (scoped) { href = scoped + href } dom.setAttribute('href', href) } }) } const fc = (to) => { let url = to?.fullPath if (dom.getAttribute('href') === url) { dom.setAttribute('active', '') } else { dom.removeAttribute('active') } } fc(vrouter.$router.current) vrouter.$router.onChange(fc) } /** * @param {HTMLElement} dom * */ parseAttr(dom, name, value, data, env) { if (name.startsWith(':')) { let attrName = name.slice(1) if (attrName === 'class' || attrName === 'style') { this.handleStyle(dom, attrName, value, data, env) } else { vproxy.Watch(() => { let res if (value) { res = vproxy.Run(value, data, env) } else { res = data[attrName] } utils.SetAttr(dom, attrName, res) }) } return true } else if (name.startsWith('@')) { this.handleEvent(dom, name, value, data, env) return true } else if (name.indexOf('!') > -1) { console.warn('! prefix is deprecated, use : instead:', name, value, dom) } else if (name.startsWith('v:')) { let args = findLastAccess(value, data) if (args && args.data && args.key) { let vkey = args.key let vdata = args.data return utils.BindInputDomValue(dom, vdata, vkey, vproxy.Watch) } else { console.warn('not found variables in:' + value) } } else if (name === 'vdom') { let vbind = findLastAccess(value, data) if (vbind && vbind.data && vbind.key) { vbind.data[vbind.key] = dom } else { console.warn('not found variables in:' + value) } return true } return false } handleStyle(dom, attrName, value, data, env) { let oldValue = '' vproxy.Watch(() => { let res = vproxy.Run(value, data, env) if (typeof res === 'function') { res = res() } if (attrName === 'class') { if (oldValue) { dom.classList.remove(...oldValue.split(/\s+/)) oldValue = '' } if (res instanceof Array) { oldValue = '' res.forEach(r => { if (typeof r === 'string' && r.length) { oldValue += ' ' + r } else if (typeof r === 'object') { for (let k in r) { if (r[k]) { oldValue += ' ' + k } } } }) } else if (typeof res === 'string' && res.length) { oldValue = res.trim() } else if (typeof res === 'object') { oldValue = '' for (let k in res) { if (res[k]) { oldValue += ' ' + k } } } else if (res) { console.warn('class value error:', res) } oldValue = oldValue.trim() if (oldValue) { dom.classList.add(...oldValue.split(/\s+/)) } } else if (attrName === 'style') { if (oldValue) { if (typeof oldValue === 'object') { for (let k in oldValue) { if (k.startsWith('--')) { dom.style.removeProperty(k) } else { dom.style[k] = '' } } } else if (typeof oldValue === 'string') { let styles = oldValue.split(';') for (let s of styles) { let ss = s.split(':') if (ss.length === 2) { if (ss[0].trim().startsWith('--')) { dom.style.removeProperty(ss[0].trim()) } else { dom.style[ss[0].trim()] = '' } } } } } if (typeof res === 'object') { for (let k in res) { if (k.startsWith('--')) { dom.style.setProperty(k, res[k]) } else { dom.style[k] = res[k] } } } else if (typeof res === 'string') { let styles = res.split(';') for (let s of styles) { let ss = s.split(':') if (ss.length === 2) { if (ss[0].trim().startsWith('--')) { dom.style.setProperty(ss[0].trim(), ss[1].trim()) } else { dom.style[ss[0].trim()] = ss[1].trim() } } } } oldValue = res } }) } handleEvent(dom, name, value, data, env) { let actionName = name.slice(1).split('.') let evtMap = { 'self': false, 'prevent': false, 'stop': false } let evt = actionName[0] if (evt === 'mounted') { this.onMountedRun(dom, (d) => { let cb = vproxy.Run(value, data, env) if (typeof cb === 'function') { (function() { })(); cb(d) } }, false) } else if (evt === 'outerclick') { let func = (e) => { let cb = vproxy.Run(value, data, env, { $event: e }) if (typeof cb === 'function') { cb(e) } } utils.AddClicker(dom, 'outer', func) } else if (utils.EventsList.indexOf(evt) !== -1) { if (evt === 'keydown' || evt === 'keyup' || evt === 'keypress') { if (dom.tagName !== 'INPUT' && dom.tagName !== 'TEXTAREA') { dom.setAttribute('tabindex', '0') } } let func = (e) => { let cb = vproxy.Run(value, data, env, { $event: e }) if (typeof cb === 'function') { cb(e) } } actionName.slice(1).forEach(k => { if (k.startsWith('delay')) { let delay = k.slice(5) if (!delay) { delay = 1000 } else if (delay.endsWith('ms')) { delay = Number(delay.slice(0, -2)) } else if (delay.endsWith('s')) { delay = Number(delay.slice(0, -1)) * 1000 } else { delay = Number(delay) } if (isNaN(delay)) { delay = 1000 } func = (e) => { let fc = dom['_' + evt] if (fc && typeof fc === 'number') { clearTimeout(fc) } dom['_' + evt] = setTimeout(() => { let cb = vproxy.Run(value, data, env, { $event: e }) if (typeof cb === 'function') { cb(e) } }, delay) } } evtMap[k] = true }) dom.addEventListener(evt, (e) => { if (actionName.length > 1 && (evt === 'keydown' || evt == 'keyup' || evt == 'keypress')) { let btn = actionName[1] if (btn !== e.key?.toLowerCase()) { return } } if (evtMap['self'] && e.currentTarget !== e.target) { return } if (evtMap['prevent']) { e.preventDefault() } if (evtMap['stop']) { e.stopPropagation() } func(e) }) } else { dom.$vevent = dom.$vevent || {} dom.$vevent[evt] = (...arg) => { let cb = vproxy.Run(value, data, env, {}) if (typeof cb === 'function') { cb(...arg) } } } } parseTextNode(dom, data, env) { // text node let txt = dom.nodeValue.trim() if (!txt) { return } let match let nstart = 0 let start = -1; let txtItems = [] while ((match = varRegex.exec(txt)) !== null) { if (match[0] === '{{') { start = match.index } else if (match[0] === '}}' && start >= 0) { if (nstart !== start) { txtItems.push(txt.slice(nstart, start)) } txtItems.push('') let valStr = txt.slice(start + 2, match.index) let valIdx = txtItems.length start = -1 nstart = match.index + 2 vproxy.Watch(() => { txtItems[valIdx - 1] = vproxy.Run(valStr, data, env) if (typeof txtItems[valIdx - 1] === 'object') { txtItems[valIdx - 1] = JSON.stringify(txtItems[valIdx - 1]) } dom.nodeValue = txtItems.join('') }) } } txtItems.push(txt.slice(nstart)) dom.nodeValue = txtItems.join('') } vforDomCache = {} parseVfor(vfortxt, dom, data, env) { dom.removeAttribute('v-for') let matches = vforRegex.exec(vfortxt) if (matches?.length === 6) { let vforTag = document.createElement('div') vforTag.style.display = 'none' let vforTagID = vproxy.GenUniqueID() this.vforDomCache[vforTagID] = {} dom.parentNode.replaceChild(vforTag, dom) vproxy.Watch(() => { let value = matches[3] || matches[2] let key = matches[4] let iters = vproxy.Run(matches[5], data, env) let cache = this.vforDomCache[vforTagID] let rendereds = new Set() if (typeof iters === 'function') { iters = iters() } else if (typeof iters === 'number') { iters = Array.from({ length: iters }, (_, i) => i) } if (iters === undefined || iters === null) { iters = [] } // 访问长度,触发监听 let _ = iters.length if (typeof iters === 'object') { let keys = Object.keys(iters) let items = [] for (let kid in keys) { let k = keys[kid] let vfk = '' if (iters[k] && iters[k][vproxy.DataID]) { vfk = iters[k][vproxy.DataID] } else { vfk = k + '.' + iters[k] } vfk = vforTagID + "." + vfk rendereds.add(vfk) items.push({ k, vfk, val: iters[k] }) } // Cleanup removed nodes for (let k of Object.keys(cache)) { if (!rendereds.has(k)) { if (cache[k] instanceof Array) { cache[k].forEach(d => d.remove()) } else { cache[k].remove() } delete cache[k] } } // Update and Reorder (Backwards) let refNode = vforTag for (let i = items.length - 1; i >= 0; i--) { let { k, vfk, val } = items[i] let curDom = cache[vfk] if (curDom) { if (key) { curDom.$vforData[key] = k === '0' ? 0 : (Number(k) || k) } if (curDom.isConnected) { if (curDom.nextSibling !== refNode) { vforTag.parentNode.insertBefore(curDom, refNode) } refNode = curDom } continue } let newDom = dom.cloneNode(true) cache[vfk] = newDom let tmpData = { [value]: val } if (key) { tmpData[key] = k === '0' ? 0 : (Number(k) || k) } tmpData = vproxy.Wrap(tmpData, data) newDom.$vforData = tmpData vforTag.parentNode.insertBefore(newDom, refNode) let vif = dom.getAttribute('v-if') if (!vif) { this.parseDom(newDom, tmpData, env) refNode = newDom continue } newDom.removeAttribute('v-if') let watchid = -1 watchid = vproxy.Watch(() => { let dom = cache[vfk] if (!dom) { vproxy.Cancel(watchid) return } let res = vproxy.Run(vif, tmpData, env) if (res) { if (!dom.vparsed) { this.parseDom(dom, tmpData, env) } if (!dom.isConnected) { let founded = false let before = vforTag for (let tmpvfk in cache) { if (tmpvfk === vfk) { founded = true continue } if (founded && cache[tmpvfk].isConnected) { before = cache[tmpvfk] break } } vforTag.parentNode.insertBefore(dom, before) } } else { if (dom.isConnected) { dom.remove() } else { this.onMountedRun(dom, (d) => { dom.remove() }) } } }) if (newDom.isConnected) { refNode = newDom } } } else { console.error('vfor iter object error:', [matches, iters, vfortxt, data]) } }) } else { console.error('vfor error:', vfortxt) } } parseVif(nodes, data, env) { let ifCache = { now: document.createElement('div'), conds: [], doms: [] } const handleIf = (cache) => { let ifData = { now: cache.now, conds: cache.conds, doms: cache.doms } let ifList = [] for (let cid in ifData.conds) { let c = ifData.conds[cid] if (c === '') { c = 'true' } else { c = 'Boolean(' + c + ')' } ifList.push(c) } let ifFc = `let res = [${ifList.join(',')}]\n return res.indexOf(true)` vproxy.Watch(() => { let res = vproxy.Run(ifFc, data, env) let tmpDom = ifData.doms[res] if (!tmpDom) { tmpDom = document.createElement('div') tmpDom.style.display = 'none' } return tmpDom }, (tmpDom) => { if (!tmpDom) { return } this.onMountedRun(ifData.now, (d) => { d.replaceWith(tmpDom) ifData.now = tmpDom }) if (!tmpDom?.vparsed) { this.parseDom(tmpDom, data, env) } }) } let childs = nodes.filter(d => { if (!d.getAttribute || d.getAttribute('v-for')) { return true } if (d.getAttribute('v-if') !== null) { if (ifCache.conds.length > 0) { handleIf(ifCache) ifCache = { now: document.createElement('div'), conds: [], doms: [] } } d.replaceWith(ifCache.now) // dom.replaceChild(ifCache.now, d) ifCache.conds.push(d.getAttribute('v-if')) d.removeAttribute('v-if') ifCache.doms.push(d) return false } else if (d.getAttribute('v-else-if') !== null) { ifCache.conds.push(d.getAttribute('v-else-if')) d.removeAttribute('v-else-if') ifCache.doms.push(d) d.remove() return false } else if (d.getAttribute('v-else') !== null) { ifCache.conds.push('') d.removeAttribute('v-else') ifCache.doms.push(d) d.remove() return false } return true }) if (ifCache.conds.length > 0) { handleIf(ifCache) } return childs } /** * @param {HTMLElement} dom * */ parseSlots(dom, data, env) { // 先插入dom,后解析,避免slot搭配v-for,v-if使用出现bug let slotof = dom.getAttribute('vrefof') let refDom = dom.closest(`*[vref='${slotof}']`) if (!refDom) { // TODO this.onMountedRun(dom, (d) => { this.parseSlots(d, data, env) }) return } while (true) { let tmp = refDom?.parentNode.closest('*[vref]') if (!tmp) { break } if (tmp.getAttribute('vref') === slotof) { refDom = tmp } else { break } } let sName = dom.getAttribute('name') || '' if (dom.getAttribute(':name')) { let nameVal = dom.getAttribute(':name') dom.removeAttribute(':name') sName = vproxy.Run(nameVal, data, env) } // if (!dom.originContent) { dom.$originContent = Array.from(dom.childNodes) dom.innerHTML = '' } // slot模板 dom.$slotCache = {} vproxy.Watch(() => { let slots = refDom.$refSlots || {} // slot数据域 let slotsData = refDom.$refScope || {} let sNodes = slots[sName] if (sNodes && sNodes.length > 0) { let hashID = sNodes[0].hashID if (!hashID) { hashID = vproxy.GenUniqueID() sNodes[0].hashID = hashID } else if (dom.$slotCache[hashID]) { dom.innerHTML = '' dom.append(...dom.$slotCache[hashID]) return } dom.innerHTML = '' sNodes = sNodes.map(n => n.cloneNode(true)) dom.append(...sNodes) let tmpSlotsData = slotsData if (dom.getAttribute('vbind') !== null) { let vbindAttrs = dom.getAttribute('vbind').split(',').map(a => a.trim()) tmpSlotsData = vproxy.Wrap({}) vbindAttrs.forEach(a => { if (data.hasOwnProperty(a)) { tmpSlotsData[a] = data[a] } }) // tmpSlotsData = recordGet(dom.getAttribute('vbind'), data) vproxy.SetDataRoot(tmpSlotsData, slotsData) } let sNodeVrefof = '' sNodes.find(n => { if (n.getAttribute && n.getAttribute('vrefof')) { sNodeVrefof = n.getAttribute('vrefof') return true } return false }) let sNodesEnv = env if (sNodeVrefof) { let sNodeVref = dom.closest(`*[vref='${sNodeVrefof}']`) sNodesEnv = sNodeVref?.$env || env } sNodes = this.parseVif(sNodes, tmpSlotsData, sNodesEnv) sNodes.forEach((n) => this.parseDom(n, tmpSlotsData, sNodesEnv)) dom.$slotCache[hashID] = sNodes } else { dom.innerHTML = '' dom.append(...dom.$originContent) let parsed = false dom.$originContent.forEach((n) => { if (n.hasAttribute && n.vparsed) { parsed = true } }) if (!parsed) { dom.$originContent = this.parseVif(dom.$originContent, data, env) dom.$originContent.forEach((n) => this.parseDom(n, data, env)) } } }) this.parseAttrs(dom, data, env) return dom } } if (window.$vhtml) { console.error('vhtml already exists.') } else { window.$vhtml = new vhtml(document.body) console.log('vhtml loaded.') } })();