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/ui/vhtml/vget.js

323 lines
8.5 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* vget.js
* Copyright (C) 2024 veypi <i@veypi.com>
*
* Distributed under terms of the MIT license.
*/
import EventBus from './vbus.js';
import axios from './axios.min.js'
import vcss from './vcss.js'
import vproxy from './vproxy.js';
import vmessage from './vmessage.js'
async function FetchFile(url) {
return fetch(url).then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
}
var cacheUrl = {}
var pendingRequests = {};
let baseFile = ''
const envMap = {}
async function getEnv(scoped, temp) {
scoped = scoped || ''
if (!envMap[scoped]) {
let baseURL = scoped.startsWith('http') ? scoped : window.location.origin + scoped
envMap[scoped] = Object.assign({}, temp, {
scoped: scoped,
$G: vproxy.Wrap({}),
$bus: new EventBus(),
$axios: axios.create({
baseURL: baseURL,
}),
$message: vmessage,
$router: null,
$emit: null,
})
if (scoped === $vhtml.scoped || $vhtml.scoped === null) {
envMap[scoped].$router = $vhtml.$router
} else {
// 对于第三方组件,不配置路由
envMap[scoped].$router = { addRoutes: () => { }, beforeEnter: () => { } }
}
try {
await (await import(baseURL + '/env.js')).default(envMap[scoped])
} catch (e) {
console.warn('error loading ' + baseURL + '/env.js: ' + e)
}
}
return envMap[scoped]
}
/**
* @param {string} url
* @return {Promise<{heads:HTMLCollection, body: HTMLElement, setup?:Element, scripts:Element[]}, scripts:Element>}
*/
async function FetchUI(url, env, ignorescoped) {
if (!url || url === '/') {
url = '/'
}
if (!url.startsWith('http') && !url.startsWith('@')) {
if (!url.startsWith('/')) {
url = '/' + url
}
}
let scoped = env?.scoped
if (scoped && url.startsWith('/')) {
url = scoped + url
}
if (url.startsWith('@')) {
url = url.slice(1)
}
if (cacheUrl[url]) {
return Promise.resolve(cacheUrl[url])
}
if (pendingRequests[url]) {
return pendingRequests[url];
}
let tempenv = {}
const promise = fetch(url + "?random=" + Math.random())
.then(async (response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
for (const [key, value] of response.headers.entries()) {
if (key.startsWith('vhtml-')) {
tempenv[key.slice(6)] = value
}
}
let scoped = tempenv.scoped || ''
if (url.startsWith('http')) {
scoped = new URL(url).origin + scoped
tempenv.scoped = scoped
}
let packEnv = await getEnv(scoped, tempenv)
Object.assign(tempenv, packEnv)
// Object.seal(tempenv)
return response.text()
})
.then(txt => {
// if (baseFile === txt) {
// throw new Error(`HTTP error! status: 404`);
// }
if (baseFile == '') {
baseFile = txt
}
return ParseUI(txt, tempenv, url, ignorescoped)
}).then((parser) => {
cacheUrl[url] = parser
return parser
})
.catch(err => {
let errmsg = '404'
if (err.message !== 'HTTP error! status: 404') {
console.warn(err)
}
let dom404 = document.createElement('div')
dom404.style.cssText = `
backgound:#aaa;
height:100%;
width: 100%;
display:grid;
place-items: center;
`
dom404.innerHTML = `
<div style="width:20rem;height:15rem;border-radius:1rem;padding:1rem;background:#cfc0aa;display:grid;place-items:center;">
<div style="font-size:2rem">404</div>
<p>${url}</p>
</div>
`
let parser = {
heads: [],
body: dom404,
setup: '',
scripts: [],
styles: '',
txt: '',
tmp: '',
env: tempenv,
err: err,
}
cacheUrl[url] = parser
return parser
})
.finally(() => {
delete pendingRequests[url];
});
pendingRequests[url] = promise;
return promise;
}
function generateCompactUniqueString() {
// 获取当前时间戳,精确到毫秒
const timestamp = new Date().getTime();
let shortenedTimestamp = timestamp.toString(36);
if (shortenedTimestamp.length > 4) {
shortenedTimestamp = shortenedTimestamp.substring(shortenedTimestamp.length - 4);
}
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let randomPart = '';
for (let i = 0; i < 4; i++) {
randomPart += characters.charAt(Math.floor(Math.random() * characters.length));
}
// 组合随机部分和时间戳部分保证总长度为8位
return randomPart + shortenedTimestamp.padStart(4, '0');
}
function sync_ref_owner_id(dom, id) {
// 子组件根节点不设置data-v, 所以style class 不生效于body只能通过body {}设置样式
// dom.setAttribute('data-v-' + id, '')
Array.from(dom.childNodes).forEach((n) => {
if (n.nodeType === 1) {
n.setAttribute('vrefof', id)
sync_ref_owner_id(n, id)
}
})
}
async function ParseUI(txt, env, turl, ignorescoped) {
if (turl === undefined) {
turl = '#' + generateCompactUniqueString()
}
if (turl.endsWith('.html')) {
turl = turl.slice(0, -5)
}
let tmp = new DOMParser().parseFromString(txt, 'text/html')
if (tmp.body.hasAttribute('scoped') && !ignorescoped) {
throw new Error(`HTTP error! status: 404`);
}
let target = {
url: turl,
heads: [],
body: document.createElement('div'),
setup: undefined,
scripts: [],
styles: '',
txt: txt,
env: env,
tmp: tmp,
customAttrs: {},
}
target.heads = Array.from(tmp.querySelector('head')?.children)
// target.heads.forEach(h => {
// })
if (turl) {
tmp.querySelectorAll('style').forEach((s) => {
if (s.getAttribute('unscoped') === null) {
target.styles += vcss.parse(s.innerHTML, turl)
} else {
target.styles += s.innerHTML
}
})
if (target.styles) {
const style = document.createElement('style')
style.innerHTML = target.styles
style.setAttribute('vref', turl)
document.head.appendChild(style)
}
}
target.body.append(...tmp.querySelector('body').childNodes)
// target.body = tmp.querySelector('body')
target.body.querySelectorAll('script').forEach((s) => {
let sinner = s.innerHTML.trim()
if (sinner == '') {
s.remove()
return
}
if (s.hasAttribute('setup')) {
target.setup = s
} else if (!s.hasAttribute('no-vhtml')) {
target.scripts.push(s)
}
s.remove()
})
// target.body.classList = tmp.body.classList
Array.from(tmp.body.attributes).forEach((e) => {
if (/^[a-zA-Z]/.test(e.name)) {
target.body.setAttribute(e.name, e.value)
} else {
target.customAttrs[e.name] = e.value
}
})
target.body.setAttribute('vref', turl)
sync_ref_owner_id(target.body, turl)
if (!ignorescoped) {
await loadHeaders(target, env)
}
return target
}
async function loadHeaders(target, env) {
for (let h of target.heads) {
let nodeName = h.nodeName.toLowerCase()
if (nodeName === 'link') {
LoadLink(h, env)
} else if (nodeName === 'script') {
await LoadScript(h, env)
} else if (nodeName === 'title') {
target.title = h.innerText
} else {
}
}
}
/**
* @param {HTMLElement} dom
*/
function LoadScript(dom, env) {
let src = dom.getAttribute('src')
let key = dom.getAttribute('key')
let scoped = env?.scoped
if (scoped && src.startsWith('/')) {
src = scoped + src
}
if (src.startsWith('@')) {
src = src.slice(1)
}
if (src && document.querySelector(`script[src="${src}"]`)) {
return
}
if (key && document.querySelector(`script[key="${key}"]`)) {
return
}
let newDom = document.createElement('script')
newDom.src = src
newDom.key = key
newDom.type = dom.getAttribute('type') || 'text/javascript'
return new Promise((resolve, reject) => {
newDom.onload = () => {
resolve(newDom)
};
newDom.onerror = () => reject(new Error(`Failed to load script ${src}`));
document.head.appendChild(newDom)
})
}
async function LoadLink(dom, env) {
let src = dom.getAttribute('href')
let key = dom.getAttribute('key')
let scoped = env?.scoped
if (scoped && src.startsWith('/')) {
src = scoped + src
}
if (src.startsWith('@')) {
src = src.slice(1)
}
if (src && document.querySelector(`link[href="${src}"]`)) {
return
}
if (key && document.querySelector(`link[key="${key}"]`)) {
return
}
dom.setAttribute('href', src)
document.head.append(dom)
}
export default { FetchUI, FetchFile, LoadScript, LoadLink, ParseUI }