diff --git a/ui/vhtml/axios.min.js b/ui/vhtml/axios.min.js deleted file mode 100644 index c0d0b27..0000000 --- a/ui/vhtml/axios.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! Axios v1.10.0 Copyright (c) 2025 Matt Zabriskie and contributors */ -function e(e,t){return function(){return e.apply(t,arguments)}}const{toString:t}=Object.prototype,{getPrototypeOf:n}=Object,{iterator:r,toStringTag:o}=Symbol,s=(i=Object.create(null),e=>{const n=t.call(e);return i[n]||(i[n]=n.slice(8,-1).toLowerCase())});var i;const a=e=>(e=e.toLowerCase(),t=>s(t)===e),c=e=>t=>typeof t===e,{isArray:l}=Array,u=c("undefined");const f=a("ArrayBuffer");const d=c("string"),p=c("function"),h=c("number"),m=e=>null!==e&&"object"==typeof e,y=e=>{if("object"!==s(e))return!1;const t=n(e);return!(null!==t&&t!==Object.prototype&&null!==Object.getPrototypeOf(t)||o in e||r in e)},b=a("Date"),g=a("File"),w=a("Blob"),E=a("FileList"),O=a("URLSearchParams"),[R,S,T,A]=["ReadableStream","Request","Response","Headers"].map(a);function v(e,t,{allOwnKeys:n=!1}={}){if(null==e)return;let r,o;if("object"!=typeof e&&(e=[e]),l(e))for(r=0,o=e.length;r0;)if(r=n[o],t===r.toLowerCase())return r;return null}const C="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,j=e=>!u(e)&&e!==C;const N=(U="undefined"!=typeof Uint8Array&&n(Uint8Array),e=>U&&e instanceof U);var U;const P=a("HTMLFormElement"),_=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),F=a("RegExp"),L=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),r={};v(n,((n,o)=>{let s;!1!==(s=t(n,o,e))&&(r[o]=s||n)})),Object.defineProperties(e,r)};const B=a("AsyncFunction"),k=(D="function"==typeof setImmediate,q=p(C.postMessage),D?setImmediate:q?(I=`axios@${Math.random()}`,M=[],C.addEventListener("message",(({source:e,data:t})=>{e===C&&t===I&&M.length&&M.shift()()}),!1),e=>{M.push(e),C.postMessage(I,"*")}):e=>setTimeout(e));var D,q,I,M;const z="undefined"!=typeof queueMicrotask?queueMicrotask.bind(C):"undefined"!=typeof process&&process.nextTick||k,H={isArray:l,isArrayBuffer:f,isBuffer:function(e){return null!==e&&!u(e)&&null!==e.constructor&&!u(e.constructor)&&p(e.constructor.isBuffer)&&e.constructor.isBuffer(e)},isFormData:e=>{let t;return e&&("function"==typeof FormData&&e instanceof FormData||p(e.append)&&("formdata"===(t=s(e))||"object"===t&&p(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){let t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&f(e.buffer),t},isString:d,isNumber:h,isBoolean:e=>!0===e||!1===e,isObject:m,isPlainObject:y,isReadableStream:R,isRequest:S,isResponse:T,isHeaders:A,isUndefined:u,isDate:b,isFile:g,isBlob:w,isRegExp:F,isFunction:p,isStream:e=>m(e)&&p(e.pipe),isURLSearchParams:O,isTypedArray:N,isFileList:E,forEach:v,merge:function e(){const{caseless:t}=j(this)&&this||{},n={},r=(r,o)=>{const s=t&&x(n,o)||o;y(n[s])&&y(r)?n[s]=e(n[s],r):y(r)?n[s]=e({},r):l(r)?n[s]=r.slice():n[s]=r};for(let e=0,t=arguments.length;e(v(n,((n,o)=>{r&&p(n)?t[o]=e(n,r):t[o]=n}),{allOwnKeys:o}),t),trim:e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,""),stripBOM:e=>(65279===e.charCodeAt(0)&&(e=e.slice(1)),e),inherits:(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:(e,t,r,o)=>{let s,i,a;const c={};if(t=t||{},null==e)return t;do{for(s=Object.getOwnPropertyNames(e),i=s.length;i-- >0;)a=s[i],o&&!o(a,e,t)||c[a]||(t[a]=e[a],c[a]=!0);e=!1!==r&&n(e)}while(e&&(!r||r(e,t))&&e!==Object.prototype);return t},kindOf:s,kindOfTest:a,endsWith:(e,t,n)=>{e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;const r=e.indexOf(t,n);return-1!==r&&r===n},toArray:e=>{if(!e)return null;if(l(e))return e;let t=e.length;if(!h(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},forEachEntry:(e,t)=>{const n=(e&&e[r]).call(e);let o;for(;(o=n.next())&&!o.done;){const n=o.value;t.call(e,n[0],n[1])}},matchAll:(e,t)=>{let n;const r=[];for(;null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:P,hasOwnProperty:_,hasOwnProp:_,reduceDescriptors:L,freezeMethods:e=>{L(e,((t,n)=>{if(p(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;const r=e[n];p(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:(e,t)=>{const n={},r=e=>{e.forEach((e=>{n[e]=!0}))};return l(e)?r(e):r(String(e).split(t)),n},toCamelCase:e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n})),noop:()=>{},toFiniteNumber:(e,t)=>null!=e&&Number.isFinite(e=+e)?e:t,findKey:x,global:C,isContextDefined:j,isSpecCompliantForm:function(e){return!!(e&&p(e.append)&&"FormData"===e[o]&&e[r])},toJSONObject:e=>{const t=new Array(10),n=(e,r)=>{if(m(e)){if(t.indexOf(e)>=0)return;if(!("toJSON"in e)){t[r]=e;const o=l(e)?[]:{};return v(e,((e,t)=>{const s=n(e,r+1);!u(s)&&(o[t]=s)})),t[r]=void 0,o}}return e};return n(e,0)},isAsyncFn:B,isThenable:e=>e&&(m(e)||p(e))&&p(e.then)&&p(e.catch),setImmediate:k,asap:z,isIterable:e=>null!=e&&p(e[r])};function J(e,t,n,r,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o,this.status=o.status?o.status:null)}H.inherits(J,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:H.toJSONObject(this.config),code:this.code,status:this.status}}});const W=J.prototype,K={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((e=>{K[e]={value:e}})),Object.defineProperties(J,K),Object.defineProperty(W,"isAxiosError",{value:!0}),J.from=(e,t,n,r,o,s)=>{const i=Object.create(W);return H.toFlatObject(e,i,(function(e){return e!==Error.prototype}),(e=>"isAxiosError"!==e)),J.call(i,e.message,t,n,r,o),i.cause=e,i.name=e.name,s&&Object.assign(i,s),i};function V(e){return H.isPlainObject(e)||H.isArray(e)}function $(e){return H.endsWith(e,"[]")?e.slice(0,-2):e}function X(e,t,n){return e?e.concat(t).map((function(e,t){return e=$(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}const G=H.toFlatObject(H,{},null,(function(e){return/^is[A-Z]/.test(e)}));function Q(e,t,n){if(!H.isObject(e))throw new TypeError("target must be an object");t=t||new FormData;const r=(n=H.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!H.isUndefined(t[e])}))).metaTokens,o=n.visitor||l,s=n.dots,i=n.indexes,a=(n.Blob||"undefined"!=typeof Blob&&Blob)&&H.isSpecCompliantForm(t);if(!H.isFunction(o))throw new TypeError("visitor must be a function");function c(e){if(null===e)return"";if(H.isDate(e))return e.toISOString();if(H.isBoolean(e))return e.toString();if(!a&&H.isBlob(e))throw new J("Blob is not supported. Use a Buffer instead.");return H.isArrayBuffer(e)||H.isTypedArray(e)?a&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function l(e,n,o){let a=e;if(e&&!o&&"object"==typeof e)if(H.endsWith(n,"{}"))n=r?n:n.slice(0,-2),e=JSON.stringify(e);else if(H.isArray(e)&&function(e){return H.isArray(e)&&!e.some(V)}(e)||(H.isFileList(e)||H.endsWith(n,"[]"))&&(a=H.toArray(e)))return n=$(n),a.forEach((function(e,r){!H.isUndefined(e)&&null!==e&&t.append(!0===i?X([n],r,s):null===i?n:n+"[]",c(e))})),!1;return!!V(e)||(t.append(X(o,n,s),c(e)),!1)}const u=[],f=Object.assign(G,{defaultVisitor:l,convertValue:c,isVisitable:V});if(!H.isObject(e))throw new TypeError("data must be an object");return function e(n,r){if(!H.isUndefined(n)){if(-1!==u.indexOf(n))throw Error("Circular reference detected in "+r.join("."));u.push(n),H.forEach(n,(function(n,s){!0===(!(H.isUndefined(n)||null===n)&&o.call(t,n,H.isString(s)?s.trim():s,r,f))&&e(n,r?r.concat(s):[s])})),u.pop()}}(e),t}function Z(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function Y(e,t){this._pairs=[],e&&Q(e,this,t)}const ee=Y.prototype;function te(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function ne(e,t,n){if(!t)return e;const r=n&&n.encode||te;H.isFunction(n)&&(n={serialize:n});const o=n&&n.serialize;let s;if(s=o?o(t,n):H.isURLSearchParams(t)?t.toString():new Y(t,n).toString(r),s){const t=e.indexOf("#");-1!==t&&(e=e.slice(0,t)),e+=(-1===e.indexOf("?")?"?":"&")+s}return e}ee.append=function(e,t){this._pairs.push([e,t])},ee.toString=function(e){const t=e?function(t){return e.call(this,t,Z)}:Z;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};const re=class{constructor(){this.handlers=[]}use(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}eject(e){this.handlers[e]&&(this.handlers[e]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(e){H.forEach(this.handlers,(function(t){null!==t&&e(t)}))}},oe={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},se={isBrowser:!0,classes:{URLSearchParams:"undefined"!=typeof URLSearchParams?URLSearchParams:Y,FormData:"undefined"!=typeof FormData?FormData:null,Blob:"undefined"!=typeof Blob?Blob:null},protocols:["http","https","file","blob","url","data"]},ie="undefined"!=typeof window&&"undefined"!=typeof document,ae="object"==typeof navigator&&navigator||void 0,ce=ie&&(!ae||["ReactNative","NativeScript","NS"].indexOf(ae.product)<0),le="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&"function"==typeof self.importScripts,ue=ie&&window.location.href||"http://localhost",fe={...Object.freeze({__proto__:null,hasBrowserEnv:ie,hasStandardBrowserWebWorkerEnv:le,hasStandardBrowserEnv:ce,navigator:ae,origin:ue}),...se};function de(e){function t(e,n,r,o){let s=e[o++];if("__proto__"===s)return!0;const i=Number.isFinite(+s),a=o>=e.length;if(s=!s&&H.isArray(r)?r.length:s,a)return H.hasOwnProp(r,s)?r[s]=[r[s],n]:r[s]=n,!i;r[s]&&H.isObject(r[s])||(r[s]=[]);return t(e,n,r[s],o)&&H.isArray(r[s])&&(r[s]=function(e){const t={},n=Object.keys(e);let r;const o=n.length;let s;for(r=0;r{t(function(e){return H.matchAll(/\w+|\[(\w*)]/g,e).map((e=>"[]"===e[0]?"":e[1]||e[0]))}(e),r,n,0)})),n}return null}const pe={transitional:oe,adapter:["xhr","http","fetch"],transformRequest:[function(e,t){const n=t.getContentType()||"",r=n.indexOf("application/json")>-1,o=H.isObject(e);o&&H.isHTMLForm(e)&&(e=new FormData(e));if(H.isFormData(e))return r?JSON.stringify(de(e)):e;if(H.isArrayBuffer(e)||H.isBuffer(e)||H.isStream(e)||H.isFile(e)||H.isBlob(e)||H.isReadableStream(e))return e;if(H.isArrayBufferView(e))return e.buffer;if(H.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();let s;if(o){if(n.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return Q(e,new fe.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,r){return fe.isNode&&H.isBuffer(e)?(this.append(t,e.toString("base64")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((s=H.isFileList(e))||n.indexOf("multipart/form-data")>-1){const t=this.env&&this.env.FormData;return Q(s?{"files[]":e}:e,t&&new t,this.formSerializer)}}return o||r?(t.setContentType("application/json",!1),function(e,t,n){if(H.isString(e))try{return(t||JSON.parse)(e),H.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){const t=this.transitional||pe.transitional,n=t&&t.forcedJSONParsing,r="json"===this.responseType;if(H.isResponse(e)||H.isReadableStream(e))return e;if(e&&H.isString(e)&&(n&&!this.responseType||r)){const n=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e)}catch(e){if(n){if("SyntaxError"===e.name)throw J.from(e,J.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:fe.classes.FormData,Blob:fe.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};H.forEach(["delete","get","head","post","put","patch"],(e=>{pe.headers[e]={}}));const he=pe,me=H.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),ye=Symbol("internals");function be(e){return e&&String(e).trim().toLowerCase()}function ge(e){return!1===e||null==e?e:H.isArray(e)?e.map(ge):String(e)}function we(e,t,n,r,o){return H.isFunction(r)?r.call(this,t,n):(o&&(t=n),H.isString(t)?H.isString(r)?-1!==t.indexOf(r):H.isRegExp(r)?r.test(t):void 0:void 0)}class Ee{constructor(e){e&&this.set(e)}set(e,t,n){const r=this;function o(e,t,n){const o=be(t);if(!o)throw new Error("header name must be a non-empty string");const s=H.findKey(r,o);(!s||void 0===r[s]||!0===n||void 0===n&&!1!==r[s])&&(r[s||t]=ge(e))}const s=(e,t)=>H.forEach(e,((e,n)=>o(e,n,t)));if(H.isPlainObject(e)||e instanceof this.constructor)s(e,t);else if(H.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim()))s((e=>{const t={};let n,r,o;return e&&e.split("\n").forEach((function(e){o=e.indexOf(":"),n=e.substring(0,o).trim().toLowerCase(),r=e.substring(o+1).trim(),!n||t[n]&&me[n]||("set-cookie"===n?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+", "+r:r)})),t})(e),t);else if(H.isObject(e)&&H.isIterable(e)){let n,r,o={};for(const t of e){if(!H.isArray(t))throw TypeError("Object iterator must return a key-value pair");o[r=t[0]]=(n=o[r])?H.isArray(n)?[...n,t[1]]:[n,t[1]]:t[1]}s(o,t)}else null!=e&&o(t,e,n);return this}get(e,t){if(e=be(e)){const n=H.findKey(this,e);if(n){const e=this[n];if(!t)return e;if(!0===t)return function(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}(e);if(H.isFunction(t))return t.call(this,e,n);if(H.isRegExp(t))return t.exec(e);throw new TypeError("parser must be boolean|regexp|function")}}}has(e,t){if(e=be(e)){const n=H.findKey(this,e);return!(!n||void 0===this[n]||t&&!we(0,this[n],n,t))}return!1}delete(e,t){const n=this;let r=!1;function o(e){if(e=be(e)){const o=H.findKey(n,e);!o||t&&!we(0,n[o],o,t)||(delete n[o],r=!0)}}return H.isArray(e)?e.forEach(o):o(e),r}clear(e){const t=Object.keys(this);let n=t.length,r=!1;for(;n--;){const o=t[n];e&&!we(0,this[o],o,e,!0)||(delete this[o],r=!0)}return r}normalize(e){const t=this,n={};return H.forEach(this,((r,o)=>{const s=H.findKey(n,o);if(s)return t[s]=ge(r),void delete t[o];const i=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,((e,t,n)=>t.toUpperCase()+n))}(o):String(o).trim();i!==o&&delete t[o],t[i]=ge(r),n[i]=!0})),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){const t=Object.create(null);return H.forEach(this,((n,r)=>{null!=n&&!1!==n&&(t[r]=e&&H.isArray(n)?n.join(", "):n)})),t}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map((([e,t])=>e+": "+t)).join("\n")}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(e){return e instanceof this?e:new this(e)}static concat(e,...t){const n=new this(e);return t.forEach((e=>n.set(e))),n}static accessor(e){const t=(this[ye]=this[ye]={accessors:{}}).accessors,n=this.prototype;function r(e){const r=be(e);t[r]||(!function(e,t){const n=H.toCamelCase(" "+t);["get","set","has"].forEach((r=>{Object.defineProperty(e,r+n,{value:function(e,n,o){return this[r].call(this,t,e,n,o)},configurable:!0})}))}(n,e),t[r]=!0)}return H.isArray(e)?e.forEach(r):r(e),this}}Ee.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]),H.reduceDescriptors(Ee.prototype,(({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(e){this[n]=e}}})),H.freezeMethods(Ee);const Oe=Ee;function Re(e,t){const n=this||he,r=t||n,o=Oe.from(r.headers);let s=r.data;return H.forEach(e,(function(e){s=e.call(n,s,o.normalize(),t?t.status:void 0)})),o.normalize(),s}function Se(e){return!(!e||!e.__CANCEL__)}function Te(e,t,n){J.call(this,null==e?"canceled":e,J.ERR_CANCELED,t,n),this.name="CanceledError"}function Ae(e,t,n){const r=n.config.validateStatus;n.status&&r&&!r(n.status)?t(new J("Request failed with status code "+n.status,[J.ERR_BAD_REQUEST,J.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n)):e(n)}H.inherits(Te,J,{__CANCEL__:!0});const ve=(e,t,n=3)=>{let r=0;const o=function(e,t){e=e||10;const n=new Array(e),r=new Array(e);let o,s=0,i=0;return t=void 0!==t?t:1e3,function(a){const c=Date.now(),l=r[i];o||(o=c),n[s]=a,r[s]=c;let u=i,f=0;for(;u!==s;)f+=n[u++],u%=e;if(s=(s+1)%e,s===i&&(i=(i+1)%e),c-o{o=s,n=null,r&&(clearTimeout(r),r=null),e.apply(null,t)};return[(...e)=>{const t=Date.now(),a=t-o;a>=s?i(e,t):(n=e,r||(r=setTimeout((()=>{r=null,i(n)}),s-a)))},()=>n&&i(n)]}((n=>{const s=n.loaded,i=n.lengthComputable?n.total:void 0,a=s-r,c=o(a);r=s;e({loaded:s,total:i,progress:i?s/i:void 0,bytes:a,rate:c||void 0,estimated:c&&i&&s<=i?(i-s)/c:void 0,event:n,lengthComputable:null!=i,[t?"download":"upload"]:!0})}),n)},xe=(e,t)=>{const n=null!=e;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},Ce=e=>(...t)=>H.asap((()=>e(...t))),je=fe.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,fe.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(fe.origin),fe.navigator&&/(msie|trident)/i.test(fe.navigator.userAgent)):()=>!0,Ne=fe.hasStandardBrowserEnv?{write(e,t,n,r,o,s){const i=[e+"="+encodeURIComponent(t)];H.isNumber(n)&&i.push("expires="+new Date(n).toGMTString()),H.isString(r)&&i.push("path="+r),H.isString(o)&&i.push("domain="+o),!0===s&&i.push("secure"),document.cookie=i.join("; ")},read(e){const t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove(e){this.write(e,"",Date.now()-864e5)}}:{write(){},read:()=>null,remove(){}};function Ue(e,t,n){let r=!/^([a-z][a-z\d+\-.]*:)?\/\//i.test(t);return e&&(r||0==n)?function(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}(e,t):t}const Pe=e=>e instanceof Oe?{...e}:e;function _e(e,t){t=t||{};const n={};function r(e,t,n,r){return H.isPlainObject(e)&&H.isPlainObject(t)?H.merge.call({caseless:r},e,t):H.isPlainObject(t)?H.merge({},t):H.isArray(t)?t.slice():t}function o(e,t,n,o){return H.isUndefined(t)?H.isUndefined(e)?void 0:r(void 0,e,0,o):r(e,t,0,o)}function s(e,t){if(!H.isUndefined(t))return r(void 0,t)}function i(e,t){return H.isUndefined(t)?H.isUndefined(e)?void 0:r(void 0,e):r(void 0,t)}function a(n,o,s){return s in t?r(n,o):s in e?r(void 0,n):void 0}const c={url:s,method:s,data:s,baseURL:i,transformRequest:i,transformResponse:i,paramsSerializer:i,timeout:i,timeoutMessage:i,withCredentials:i,withXSRFToken:i,adapter:i,responseType:i,xsrfCookieName:i,xsrfHeaderName:i,onUploadProgress:i,onDownloadProgress:i,decompress:i,maxContentLength:i,maxBodyLength:i,beforeRedirect:i,transport:i,httpAgent:i,httpsAgent:i,cancelToken:i,socketPath:i,responseEncoding:i,validateStatus:a,headers:(e,t,n)=>o(Pe(e),Pe(t),0,!0)};return H.forEach(Object.keys(Object.assign({},e,t)),(function(r){const s=c[r]||o,i=s(e[r],t[r],r);H.isUndefined(i)&&s!==a||(n[r]=i)})),n}const Fe=e=>{const t=_e({},e);let n,{data:r,withXSRFToken:o,xsrfHeaderName:s,xsrfCookieName:i,headers:a,auth:c}=t;if(t.headers=a=Oe.from(a),t.url=ne(Ue(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),c&&a.set("Authorization","Basic "+btoa((c.username||"")+":"+(c.password?unescape(encodeURIComponent(c.password)):""))),H.isFormData(r))if(fe.hasStandardBrowserEnv||fe.hasStandardBrowserWebWorkerEnv)a.setContentType(void 0);else if(!1!==(n=a.getContentType())){const[e,...t]=n?n.split(";").map((e=>e.trim())).filter(Boolean):[];a.setContentType([e||"multipart/form-data",...t].join("; "))}if(fe.hasStandardBrowserEnv&&(o&&H.isFunction(o)&&(o=o(t)),o||!1!==o&&je(t.url))){const e=s&&i&&Ne.read(i);e&&a.set(s,e)}return t},Le="undefined"!=typeof XMLHttpRequest&&function(e){return new Promise((function(t,n){const r=Fe(e);let o=r.data;const s=Oe.from(r.headers).normalize();let i,a,c,l,u,{responseType:f,onUploadProgress:d,onDownloadProgress:p}=r;function h(){l&&l(),u&&u(),r.cancelToken&&r.cancelToken.unsubscribe(i),r.signal&&r.signal.removeEventListener("abort",i)}let m=new XMLHttpRequest;function y(){if(!m)return;const r=Oe.from("getAllResponseHeaders"in m&&m.getAllResponseHeaders());Ae((function(e){t(e),h()}),(function(e){n(e),h()}),{data:f&&"text"!==f&&"json"!==f?m.response:m.responseText,status:m.status,statusText:m.statusText,headers:r,config:e,request:m}),m=null}m.open(r.method.toUpperCase(),r.url,!0),m.timeout=r.timeout,"onloadend"in m?m.onloadend=y:m.onreadystatechange=function(){m&&4===m.readyState&&(0!==m.status||m.responseURL&&0===m.responseURL.indexOf("file:"))&&setTimeout(y)},m.onabort=function(){m&&(n(new J("Request aborted",J.ECONNABORTED,e,m)),m=null)},m.onerror=function(){n(new J("Network Error",J.ERR_NETWORK,e,m)),m=null},m.ontimeout=function(){let t=r.timeout?"timeout of "+r.timeout+"ms exceeded":"timeout exceeded";const o=r.transitional||oe;r.timeoutErrorMessage&&(t=r.timeoutErrorMessage),n(new J(t,o.clarifyTimeoutError?J.ETIMEDOUT:J.ECONNABORTED,e,m)),m=null},void 0===o&&s.setContentType(null),"setRequestHeader"in m&&H.forEach(s.toJSON(),(function(e,t){m.setRequestHeader(t,e)})),H.isUndefined(r.withCredentials)||(m.withCredentials=!!r.withCredentials),f&&"json"!==f&&(m.responseType=r.responseType),p&&([c,u]=ve(p,!0),m.addEventListener("progress",c)),d&&m.upload&&([a,l]=ve(d),m.upload.addEventListener("progress",a),m.upload.addEventListener("loadend",l)),(r.cancelToken||r.signal)&&(i=t=>{m&&(n(!t||t.type?new Te(null,e,m):t),m.abort(),m=null)},r.cancelToken&&r.cancelToken.subscribe(i),r.signal&&(r.signal.aborted?i():r.signal.addEventListener("abort",i)));const b=function(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}(r.url);b&&-1===fe.protocols.indexOf(b)?n(new J("Unsupported protocol "+b+":",J.ERR_BAD_REQUEST,e)):m.send(o||null)}))},Be=(e,t)=>{const{length:n}=e=e?e.filter(Boolean):[];if(t||n){let n,r=new AbortController;const o=function(e){if(!n){n=!0,i();const t=e instanceof Error?e:this.reason;r.abort(t instanceof J?t:new Te(t instanceof Error?t.message:t))}};let s=t&&setTimeout((()=>{s=null,o(new J(`timeout ${t} of ms exceeded`,J.ETIMEDOUT))}),t);const i=()=>{e&&(s&&clearTimeout(s),s=null,e.forEach((e=>{e.unsubscribe?e.unsubscribe(o):e.removeEventListener("abort",o)})),e=null)};e.forEach((e=>e.addEventListener("abort",o)));const{signal:a}=r;return a.unsubscribe=()=>H.asap(i),a}},ke=function*(e,t){let n=e.byteLength;if(!t||n{const o=async function*(e,t){for await(const n of De(e))yield*ke(n,t)}(e,t);let s,i=0,a=e=>{s||(s=!0,r&&r(e))};return new ReadableStream({async pull(e){try{const{done:t,value:r}=await o.next();if(t)return a(),void e.close();let s=r.byteLength;if(n){let e=i+=s;n(e)}e.enqueue(new Uint8Array(r))}catch(e){throw a(e),e}},cancel:e=>(a(e),o.return())},{highWaterMark:2})},Ie="function"==typeof fetch&&"function"==typeof Request&&"function"==typeof Response,Me=Ie&&"function"==typeof ReadableStream,ze=Ie&&("function"==typeof TextEncoder?(He=new TextEncoder,e=>He.encode(e)):async e=>new Uint8Array(await new Response(e).arrayBuffer()));var He;const Je=(e,...t)=>{try{return!!e(...t)}catch(e){return!1}},We=Me&&Je((()=>{let e=!1;const t=new Request(fe.origin,{body:new ReadableStream,method:"POST",get duplex(){return e=!0,"half"}}).headers.has("Content-Type");return e&&!t})),Ke=Me&&Je((()=>H.isReadableStream(new Response("").body))),Ve={stream:Ke&&(e=>e.body)};var $e;Ie&&($e=new Response,["text","arrayBuffer","blob","formData","stream"].forEach((e=>{!Ve[e]&&(Ve[e]=H.isFunction($e[e])?t=>t[e]():(t,n)=>{throw new J(`Response type '${e}' is not supported`,J.ERR_NOT_SUPPORT,n)})})));const Xe=async(e,t)=>{const n=H.toFiniteNumber(e.getContentLength());return null==n?(async e=>{if(null==e)return 0;if(H.isBlob(e))return e.size;if(H.isSpecCompliantForm(e)){const t=new Request(fe.origin,{method:"POST",body:e});return(await t.arrayBuffer()).byteLength}return H.isArrayBufferView(e)||H.isArrayBuffer(e)?e.byteLength:(H.isURLSearchParams(e)&&(e+=""),H.isString(e)?(await ze(e)).byteLength:void 0)})(t):n},Ge={http:null,xhr:Le,fetch:Ie&&(async e=>{let{url:t,method:n,data:r,signal:o,cancelToken:s,timeout:i,onDownloadProgress:a,onUploadProgress:c,responseType:l,headers:u,withCredentials:f="same-origin",fetchOptions:d}=Fe(e);l=l?(l+"").toLowerCase():"text";let p,h=Be([o,s&&s.toAbortSignal()],i);const m=h&&h.unsubscribe&&(()=>{h.unsubscribe()});let y;try{if(c&&We&&"get"!==n&&"head"!==n&&0!==(y=await Xe(u,r))){let e,n=new Request(t,{method:"POST",body:r,duplex:"half"});if(H.isFormData(r)&&(e=n.headers.get("content-type"))&&u.setContentType(e),n.body){const[e,t]=xe(y,ve(Ce(c)));r=qe(n.body,65536,e,t)}}H.isString(f)||(f=f?"include":"omit");const o="credentials"in Request.prototype;p=new Request(t,{...d,signal:h,method:n.toUpperCase(),headers:u.normalize().toJSON(),body:r,duplex:"half",credentials:o?f:void 0});let s=await fetch(p,d);const i=Ke&&("stream"===l||"response"===l);if(Ke&&(a||i&&m)){const e={};["status","statusText","headers"].forEach((t=>{e[t]=s[t]}));const t=H.toFiniteNumber(s.headers.get("content-length")),[n,r]=a&&xe(t,ve(Ce(a),!0))||[];s=new Response(qe(s.body,65536,n,(()=>{r&&r(),m&&m()})),e)}l=l||"text";let b=await Ve[H.findKey(Ve,l)||"text"](s,e);return!i&&m&&m(),await new Promise(((t,n)=>{Ae(t,n,{data:b,headers:Oe.from(s.headers),status:s.status,statusText:s.statusText,config:e,request:p})}))}catch(t){if(m&&m(),t&&"TypeError"===t.name&&/Load failed|fetch/i.test(t.message))throw Object.assign(new J("Network Error",J.ERR_NETWORK,e,p),{cause:t.cause||t});throw J.from(t,t&&t.code,e,p)}})};H.forEach(Ge,((e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch(e){}Object.defineProperty(e,"adapterName",{value:t})}}));const Qe=e=>`- ${e}`,Ze=e=>H.isFunction(e)||null===e||!1===e,Ye=e=>{e=H.isArray(e)?e:[e];const{length:t}=e;let n,r;const o={};for(let s=0;s`adapter ${e} `+(!1===t?"is not supported by the environment":"is not available in the build")));throw new J("There is no suitable adapter to dispatch the request "+(t?e.length>1?"since :\n"+e.map(Qe).join("\n"):" "+Qe(e[0]):"as no adapter specified"),"ERR_NOT_SUPPORT")}return r};function et(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Te(null,e)}function tt(e){et(e),e.headers=Oe.from(e.headers),e.data=Re.call(e,e.transformRequest),-1!==["post","put","patch"].indexOf(e.method)&&e.headers.setContentType("application/x-www-form-urlencoded",!1);return Ye(e.adapter||he.adapter)(e).then((function(t){return et(e),t.data=Re.call(e,e.transformResponse,t),t.headers=Oe.from(t.headers),t}),(function(t){return Se(t)||(et(e),t&&t.response&&(t.response.data=Re.call(e,e.transformResponse,t.response),t.response.headers=Oe.from(t.response.headers))),Promise.reject(t)}))}const nt={};["object","boolean","number","function","string","symbol"].forEach(((e,t)=>{nt[e]=function(n){return typeof n===e||"a"+(t<1?"n ":" ")+e}}));const rt={};nt.transitional=function(e,t,n){function r(e,t){return"[Axios v1.10.0] Transitional option '"+e+"'"+t+(n?". "+n:"")}return(n,o,s)=>{if(!1===e)throw new J(r(o," has been removed"+(t?" in "+t:"")),J.ERR_DEPRECATED);return t&&!rt[o]&&(rt[o]=!0,console.warn(r(o," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,o,s)}},nt.spelling=function(e){return(t,n)=>(console.warn(`${n} is likely a misspelling of ${e}`),!0)};const ot={assertOptions:function(e,t,n){if("object"!=typeof e)throw new J("options must be an object",J.ERR_BAD_OPTION_VALUE);const r=Object.keys(e);let o=r.length;for(;o-- >0;){const s=r[o],i=t[s];if(i){const t=e[s],n=void 0===t||i(t,s,e);if(!0!==n)throw new J("option "+s+" must be "+n,J.ERR_BAD_OPTION_VALUE)}else if(!0!==n)throw new J("Unknown option "+s,J.ERR_BAD_OPTION)}},validators:nt},st=ot.validators;class it{constructor(e){this.defaults=e||{},this.interceptors={request:new re,response:new re}}async request(e,t){try{return await this._request(e,t)}catch(e){if(e instanceof Error){let t={};Error.captureStackTrace?Error.captureStackTrace(t):t=new Error;const n=t.stack?t.stack.replace(/^.+\n/,""):"";try{e.stack?n&&!String(e.stack).endsWith(n.replace(/^.+\n.+\n/,""))&&(e.stack+="\n"+n):e.stack=n}catch(e){}}throw e}}_request(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{},t=_e(this.defaults,t);const{transitional:n,paramsSerializer:r,headers:o}=t;void 0!==n&&ot.assertOptions(n,{silentJSONParsing:st.transitional(st.boolean),forcedJSONParsing:st.transitional(st.boolean),clarifyTimeoutError:st.transitional(st.boolean)},!1),null!=r&&(H.isFunction(r)?t.paramsSerializer={serialize:r}:ot.assertOptions(r,{encode:st.function,serialize:st.function},!0)),void 0!==t.allowAbsoluteUrls||(void 0!==this.defaults.allowAbsoluteUrls?t.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:t.allowAbsoluteUrls=!0),ot.assertOptions(t,{baseUrl:st.spelling("baseURL"),withXsrfToken:st.spelling("withXSRFToken")},!0),t.method=(t.method||this.defaults.method||"get").toLowerCase();let s=o&&H.merge(o.common,o[t.method]);o&&H.forEach(["delete","get","head","post","put","patch","common"],(e=>{delete o[e]})),t.headers=Oe.concat(s,o);const i=[];let a=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(a=a&&e.synchronous,i.unshift(e.fulfilled,e.rejected))}));const c=[];let l;this.interceptors.response.forEach((function(e){c.push(e.fulfilled,e.rejected)}));let u,f=0;if(!a){const e=[tt.bind(this),void 0];for(e.unshift.apply(e,i),e.push.apply(e,c),u=e.length,l=Promise.resolve(t);f{if(!n._listeners)return;let t=n._listeners.length;for(;t-- >0;)n._listeners[t](e);n._listeners=null})),this.promise.then=e=>{let t;const r=new Promise((e=>{n.subscribe(e),t=e})).then(e);return r.cancel=function(){n.unsubscribe(t)},r},e((function(e,r,o){n.reason||(n.reason=new Te(e,r,o),t(n.reason))}))}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;const t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}toAbortSignal(){const e=new AbortController,t=t=>{e.abort(t)};return this.subscribe(t),e.signal.unsubscribe=()=>this.unsubscribe(t),e.signal}static source(){let e;return{token:new ct((function(t){e=t})),cancel:e}}}const lt=ct;const ut={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(ut).forEach((([e,t])=>{ut[t]=e}));const ft=ut;const dt=function t(n){const r=new at(n),o=e(at.prototype.request,r);return H.extend(o,at.prototype,r,{allOwnKeys:!0}),H.extend(o,r,null,{allOwnKeys:!0}),o.create=function(e){return t(_e(n,e))},o}(he);dt.Axios=at,dt.CanceledError=Te,dt.CancelToken=lt,dt.isCancel=Se,dt.VERSION="1.10.0",dt.toFormData=Q,dt.AxiosError=J,dt.Cancel=dt.CanceledError,dt.all=function(e){return Promise.all(e)},dt.spread=function(e){return function(t){return e.apply(null,t)}},dt.isAxiosError=function(e){return H.isObject(e)&&!0===e.isAxiosError},dt.mergeConfig=_e,dt.AxiosHeaders=Oe,dt.formToJSON=e=>de(H.isHTMLForm(e)?new FormData(e):e),dt.getAdapter=Ye,dt.HttpStatusCode=ft,dt.default=dt;const pt=dt,{Axios:ht,AxiosError:mt,CanceledError:yt,isCancel:bt,CancelToken:gt,VERSION:wt,all:Et,Cancel:Ot,isAxiosError:Rt,spread:St,toFormData:Tt,AxiosHeaders:At,HttpStatusCode:vt,formToJSON:xt,getAdapter:Ct,mergeConfig:jt}=pt;export{ht as Axios,mt as AxiosError,At as AxiosHeaders,Ot as Cancel,gt as CancelToken,yt as CanceledError,vt as HttpStatusCode,wt as VERSION,Et as all,pt as default,xt as formToJSON,Ct as getAdapter,Rt as isAxiosError,bt as isCancel,jt as mergeConfig,St as spread,Tt as toFormData}; -//# sourceMappingURL=axios.min.js.map diff --git a/ui/vhtml/utils.js b/ui/vhtml/utils.js deleted file mode 100644 index 6847e2d..0000000 --- a/ui/vhtml/utils.js +++ /dev/null @@ -1,293 +0,0 @@ - - -function CamelToKebabCase(str) { - // 首先将字符串的第一个字符转换为小写,避免在首字符前加上'-' - if (str.length === 0) return ''; - let firstChar = str.charAt(0).toLowerCase(); - - // 对剩余部分应用原逻辑:找到每个大写字母,并替换为连字符加上该字母的小写形式 - let rest = str.slice(1).replace(/([A-Z])/g, function(match, p1) { - return '-' + p1.toLowerCase(); - }); - - return firstChar + rest; -} - -const outerClickList = [] -const globalClick = document.addEventListener('click', (event) => { - outerClickList.forEach((item) => { - if (item?.dom instanceof Element && typeof item?.callback === 'function') { - if (!item.dom.contains(event.target)) { - item.callback(event) - } - } - }) -}) -const AddClicker = (dom, typ, callback) => { - if (typ === 'outer') { - let idx = outerClickList.length - outerClickList.push({ dom, callback }) - return () => { - outerClickList[idx] = null - } - } -} - -const EventsList = [ - // 窗口和框架事件 - 'load', - 'unload', - 'beforeunload', - 'resize', - 'scroll', - - // 表单事件 - 'submit', - 'reset', - 'input', - 'change', - 'focus', - 'blur', - - // 键盘事件 - 'keydown', - 'keypress', - 'keyup', - - // 鼠标事件 - 'click', - 'dblclick', - 'contextmenu', - 'mousedown', - 'mouseup', - 'mousemove', - 'mouseover', - 'mouseout', - 'mouseenter', - 'mouseleave', - - // 触摸事件 - 'touchstart', - 'touchmove', - 'touchend', - 'touchcancel', - - // 拖拽事件 - 'drag', - 'dragstart', - 'dragend', - 'dragover', - 'dragenter', - 'dragleave', - 'drop', - - // 剪贴板事件 - 'copy', - 'cut', - 'paste', - - // 动画事件 - 'animationstart', - 'animationend', - 'animationiteration', - - // 过渡事件 - 'transitionend', - - // 文件操作事件 - 'abort', - 'error', - 'loadstart', - 'progress', - - // 音视频事件 - 'play', - 'pause', - 'ended', - 'volumechange', - 'timeupdate', - 'loadeddata', - 'waiting', - 'playing', - - // 网络状态事件 - 'online', - 'offline', - - // 存储事件 - 'storage', - - // 页面可见性事件 - 'visibilitychange' -]; - -function BindInputDomValue(dom, data, key, watch) { - const element = typeof dom === 'string' ? document.querySelector(dom) : dom; - - if (!element) { - console.error('DOM元素未找到'); - return; - } - // 根据元素类型进行双向绑定 - const elementType = element.type || element.tagName.toLowerCase(); - switch (elementType) { - // 文本输入类 - case 'text': - case 'password': - case 'email': - case 'tel': - case 'url': - case 'search': - case 'number': - case 'range': - case 'color': - case 'date': - case 'time': - case 'datetime-local': - case 'month': - case 'week': - case 'hidden': - case 'textarea': - watch(() => data[key], (value) => { - if (value === undefined) { - element.value = '' - } else { - element.value = value - } - }) - element.addEventListener('input', function() { - data[key] = this.value; - }); - break; - case 'checkbox': - watch(function() { - element.checked = !!data[key]; - }); - element.addEventListener('change', function() { - data[key] = this.checked; - console.log(data, data[key]) - }); - break; - // 单选框 - case 'radio': - // 初始化 - watch(() => { - element.checked = element.value === data[key]; - }) - element.addEventListener('change', function() { - if (this.checked) { - data[key] = this.value; - } - }); - break; - - // 下拉选择框 - case 'select-one': - case 'select-multiple': - watch(() => { - let newValue = data[key] - if (element.multiple) { - const values = Array.isArray(newValue) ? newValue : []; - for (let i = 0; i < element.options.length; i++) { - element.options[i].selected = values.includes(element.options[i].value); - } - } else { - element.value = newValue || ''; - } - }); - // 监听变化 - element.addEventListener('change', function() { - if (this.multiple) { - // 多选 - const selectedValues = []; - for (let i = 0; i < this.options.length; i++) { - if (this.options[i].selected) { - selectedValues.push(this.options[i].value); - } - } - data[key] = selectedValues; - } else { - // 单选 - data[key] = this.value; - } - }); - break; - - default: - console.warn(`${elementType} not support v!bind only for input element`, element); - return false - } - return true -} - -function SetAttr(dom, key, value) { - // 属性名映射表 - const propertyMap = { - 'htmlfor': 'htmlFor', - 'readonly': 'readOnly', - 'maxlength': 'maxLength', - 'minlength': 'minLength', - 'cellspacing': 'cellSpacing', - 'cellpadding': 'cellPadding', - 'rowspan': 'rowSpan', - 'colspan': 'colSpan', - 'tabindex': 'tabIndex', - 'usemap': 'useMap', - 'frameborder': 'frameBorder', - 'contenteditable': 'contentEditable', - 'spellcheck': 'spellcheck', - 'innerhtml': 'innerHTML', - 'innertext': 'innerText', - 'autocapitalize': 'autocapitalize', - }; - - // 需要使用 DOM 属性设置的属性 - const domProperties = new Set([ - 'innerHTML', 'innerText', 'outerHTML', 'textContent', - 'value', 'checked', 'selected', 'disabled', 'readOnly', - 'maxLength', 'minLength', 'htmlFor', - 'tabIndex', 'scrollTop', 'scrollLeft', 'scrollWidth', 'scrollHeight', - 'clientWidth', 'clientHeight', 'offsetWidth', 'offsetHeight', - 'style', 'dataset' - ]); - - // 布尔属性 - const booleanAttributes = new Set([ - 'checked', 'selected', 'disabled', 'readonly', 'required', - 'hidden', 'autofocus', 'multiple', 'novalidate' - ]); - - // 转换属性名 - const lowerKey = key.toLowerCase(); - const mappedKey = propertyMap[lowerKey] || key; - - - - // 设置属性的策略: - if (domProperties.has(mappedKey)) { - // DOM 属性 - if (value === undefined) { - dom[mappedKey] = '' - } else { - dom[mappedKey] = value; - } - } else if (booleanAttributes.has(lowerKey)) { - // 布尔属性 - if (value) { - dom.setAttribute(lowerKey, ''); - } else { - dom.removeAttribute(lowerKey); - } - } else { - // 其他属性使用 setAttribute - if (value === undefined) { - dom.removeAttribute(key); - } else { - dom.setAttribute(key, value); - } - } -} - - -export default { CamelToKebabCase, EventsList, BindInputDomValue, SetAttr, AddClicker } - diff --git a/ui/vhtml/v.js b/ui/vhtml/v.js deleted file mode 100644 index caf4ebe..0000000 --- a/ui/vhtml/v.js +++ /dev/null @@ -1,1128 +0,0 @@ -/* - * 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.') - } -})(); diff --git a/ui/vhtml/vbus.js b/ui/vhtml/vbus.js deleted file mode 100644 index 045c8ef..0000000 --- a/ui/vhtml/vbus.js +++ /dev/null @@ -1,138 +0,0 @@ -/* - * vbus.js - * Copyright (C) 2025 veypi - * - * Distributed under terms of the MIT license. - */ - - -class EventBus { - constructor() { - // 存储事件监听器的对象 - this.events = {}; - } - - /** - * 订阅事件 - * @param {string} eventName - 事件名称 - * @param {Function} callback - 回调函数 - * @param {Object} context - 执行上下文(可选) - * @returns {Function} 取消订阅的函数 - */ - on(eventName, callback, context = null) { - if (typeof callback !== 'function') { - throw new Error('回调函数必须是一个函数'); - } - - if (!this.events[eventName]) { - this.events[eventName] = []; - } - - const listener = { callback, context }; - this.events[eventName].push(listener); - - // 返回取消订阅的函数 - return () => this.off(eventName, callback, context); - } - - /** - * 一次性事件监听 - * @param {string} eventName - 事件名称 - * @param {Function} callback - 回调函数 - * @param {Object} context - 执行上下文(可选) - * @returns {Function} 取消订阅的函数 - */ - once(eventName, callback, context = null) { - const onceWrapper = (...args) => { - this.off(eventName, onceWrapper, context); - callback.apply(context, args); - }; - - return this.on(eventName, onceWrapper, context); - } - - /** - * 取消事件订阅 - * @param {string} eventName - 事件名称 - * @param {Function} callback - 要移除的回调函数(可选) - * @param {Object} context - 执行上下文(可选) - */ - off(eventName, callback = null, context = null) { - if (!this.events[eventName]) { - return; - } - - // 如果没有指定回调函数,移除该事件的所有监听器 - if (!callback) { - delete this.events[eventName]; - return; - } - - // 移除特定的监听器 - this.events[eventName] = this.events[eventName].filter(listener => { - return !(listener.callback === callback && listener.context === context); - }); - - // 如果该事件没有监听器了,删除该事件 - if (this.events[eventName].length === 0) { - delete this.events[eventName]; - } - } - - /** - * 触发事件 - * @param {string} eventName - 事件名称 - * @param {...any} args - 传递给回调函数的参数 - */ - emit(eventName, ...args) { - if (!this.events[eventName]) { - return; - } - - // 复制监听器数组,避免在执行过程中修改原数组导致的问题 - const listeners = [...this.events[eventName]]; - - listeners.forEach(listener => { - try { - listener.callback.apply(listener.context, args); - } catch (error) { - console.error(`事件 "${eventName}" 的监听器执行出错:`, error); - } - }); - } - - /** - * 获取事件的监听器数量 - * @param {string} eventName - 事件名称 - * @returns {number} 监听器数量 - */ - listenerCount(eventName) { - return this.events[eventName] ? this.events[eventName].length : 0; - } - - /** - * 获取所有事件名称 - * @returns {string[]} 事件名称数组 - */ - eventNames() { - return Object.keys(this.events); - } - - /** - * 移除所有事件监听器 - */ - removeAllListeners() { - this.events = {}; - } - - /** - * 检查是否有某个事件的监听器 - * @param {string} eventName - 事件名称 - * @returns {boolean} 是否有监听器 - */ - hasListeners(eventName) { - return this.listenerCount(eventName) > 0; - } -} - -export default EventBus; diff --git a/ui/vhtml/vcss.js b/ui/vhtml/vcss.js deleted file mode 100644 index 3369554..0000000 --- a/ui/vhtml/vcss.js +++ /dev/null @@ -1,566 +0,0 @@ -/* - * vcss.js - * Copyright (C) 2025 veypi - * - * Distributed under terms of the GPL license. - * simple css parser - */ - - - -class CSSParser { - constructor() { - this.scopeAttribute = ''; - this.scopeBody = '' - this.scopedKeyframes = new Map(); // 存储已处理的keyframe映射 - } - - /** - * 解析CSS文本并添加作用域 - * @param {string} cssText - CSS文本 - * @param {string} scope - 作用域标识符 - * @returns {string} - 处理后的CSS文本 - */ - parse(cssText, scope) { - this.scopeAttribute = `[vrefof="${scope}"]`; - this.scopeBody = `[vref="${scope}"]` - this.scopedKeyframes.clear(); - this.scopeSuffix = scope.replace(/[^a-zA-Z0-9]/g, ''); // 清理作用域后缀 - - // 移除注释 - cssText = this.removeComments(cssText); - - // 第一遍:收集所有keyframes名称 - this.collectKeyframes(cssText); - - // 第二遍:解析CSS规则并替换动画名称 - return this.parseRules(cssText); - } - - /** - * 收集所有keyframes名称 - * @param {string} cssText - */ - collectKeyframes(cssText) { - const keyframeRegex = /@keyframes\s+([^\s{]+)/gi; - let match; - - while ((match = keyframeRegex.exec(cssText)) !== null) { - const originalName = match[1]; - const scopedName = originalName + '-' + this.scopeSuffix; - this.scopedKeyframes.set(originalName, scopedName); - } - } - - /** - * 移除CSS注释 - * @param {string} cssText - * @returns {string} - */ - removeComments(cssText) { - return cssText.replace(/\/\*[\s\S]*?\*\//g, ''); - } - - /** - * 解析CSS规则 - * @param {string} cssText - * @returns {string} - */ - parseRules(cssText) { - let result = ''; - let i = 0; - - while (i < cssText.length) { - // 跳过空白字符 - while (i < cssText.length && /\s/.test(cssText[i])) { - result += cssText[i]; - i++; - } - - if (i >= cssText.length) break; - - // 检查是否是@规则 - if (cssText[i] === '@') { - const atRule = this.parseAtRule(cssText, i); - result += atRule.content; - i = atRule.endIndex; - } else { - // 普通CSS规则 - const rule = this.parseNormalRule(cssText, i); - result += rule.content; - i = rule.endIndex; - } - } - - return result; - } - - /** - * 解析@规则(如@media, @keyframes等) - * @param {string} cssText - * @param {number} startIndex - * @returns {object} - */ - parseAtRule(cssText, startIndex) { - let i = startIndex; - let atRuleContent = ''; - - // 读取@规则名称和参数 - while (i < cssText.length && cssText[i] !== '{') { - atRuleContent += cssText[i]; - i++; - } - - if (i >= cssText.length) { - return { content: atRuleContent, endIndex: i }; - } - - // 检查是否是@keyframes或@media - const atRuleName = atRuleContent.toLowerCase().trim(); - - if (atRuleName.startsWith('@keyframes')) { - return this.parseKeyframes(cssText, startIndex); - } else if (atRuleName.startsWith('@media')) { - return this.parseMedia(cssText, startIndex); - } else if (atRuleName.startsWith('@supports')) { - return this.parseSupports(cssText, startIndex); - } else { - // 其他@规则,查找匹配的大括号 - const braceContent = this.findMatchingBrace(cssText, i); - return { - content: atRuleContent + braceContent.content, - endIndex: braceContent.endIndex - }; - } - } - - /** - * 解析@keyframes规则 - * @param {string} cssText - * @param {number} startIndex - * @returns {object} - */ - parseKeyframes(cssText, startIndex) { - let i = startIndex; - let result = ''; - - // 读取@keyframes声明 - while (i < cssText.length && cssText[i] !== '{') { - result += cssText[i]; - i++; - } - - if (i >= cssText.length) { - return { content: result, endIndex: i }; - } - - // 处理keyframes内容 - const braceContent = this.findMatchingBrace(cssText, i); - const keyframesContent = braceContent.content; - - // 为keyframes名称添加作用域 - const keyframeName = this.extractKeyframeName(result); - const scopedKeyframeName = this.scopedKeyframes.get(keyframeName); - if (scopedKeyframeName) { - result = result.replace(keyframeName, scopedKeyframeName); - } - - result += keyframesContent; - - return { - content: result, - endIndex: braceContent.endIndex - }; - } - - /** - * 提取keyframe名称 - * @param {string} keyframeDeclaration - * @returns {string} - */ - extractKeyframeName(keyframeDeclaration) { - const match = keyframeDeclaration.match(/@keyframes\s+([^\s{]+)/i); - return match ? match[1] : ''; - } - - /** - * 解析@media规则 - * @param {string} cssText - * @param {number} startIndex - * @returns {object} - */ - parseMedia(cssText, startIndex) { - let i = startIndex; - let result = ''; - - // 读取@media声明 - while (i < cssText.length && cssText[i] !== '{') { - result += cssText[i]; - i++; - } - - if (i >= cssText.length) { - return { content: result, endIndex: i }; - } - - result += '{'; - i++; // 跳过开始的 '{' - - // 解析@media内部的CSS规则 - let braceLevel = 1; - let innerCss = ''; - - while (i < cssText.length && braceLevel > 0) { - if (cssText[i] === '{') { - braceLevel++; - } else if (cssText[i] === '}') { - braceLevel--; - if (braceLevel === 0) { - break; - } - } - innerCss += cssText[i]; - i++; - } - - // 递归处理@media内部的CSS规则 - const processedInnerCss = this.parseRules(innerCss); - result += processedInnerCss; - - if (i < cssText.length && cssText[i] === '}') { - result += '}'; - i++; - } - - return { - content: result, - endIndex: i - }; - } - - /** - * 解析@supports规则 - * @param {string} cssText - * @param {number} startIndex - * @returns {object} - */ - parseSupports(cssText, startIndex) { - return this.parseMedia(cssText, startIndex); - } - - /** - * 解析普通CSS规则 - * @param {string} cssText - * @param {number} startIndex - * @returns {object} - */ - parseNormalRule(cssText, startIndex) { - let i = startIndex; - let selector = ''; - - // 读取选择器 - while (i < cssText.length && cssText[i] !== '{') { - selector += cssText[i]; - i++; - } - - if (i >= cssText.length) { - return { content: selector, endIndex: i }; - } - - // 处理选择器添加作用域 - const scopedSelector = this.addScopeToSelector(selector.trim()); - - // 读取CSS规则体并处理动画名称 - const braceContent = this.findMatchingBrace(cssText, i); - const processedContent = this.processRuleContent(braceContent.content); - - return { - content: scopedSelector + processedContent, - endIndex: braceContent.endIndex - }; - } - - /** - * 处理CSS规则内容,替换动画名称 - * @param {string} content - * @returns {string} - */ - processRuleContent(content) { - let processedContent = content; - - // 处理 animation 属性 - processedContent = processedContent.replace( - /animation\s*:\s*([^;]+);/gi, - (match, animationValue) => { - const processedValue = this.processAnimationValue(animationValue); - return `animation: ${processedValue};`; - } - ); - - // 处理 animation-name 属性 - processedContent = processedContent.replace( - /animation-name\s*:\s*([^;]+);/gi, - (match, animationNames) => { - const processedNames = this.processAnimationNames(animationNames); - return `animation-name: ${processedNames};`; - } - ); - - return processedContent; - } - - /** - * 处理animation属性值 - * @param {string} animationValue - * @returns {string} - */ - processAnimationValue(animationValue) { - // animation 可能包含多个动画,用逗号分隔 - const animations = animationValue.split(',').map(anim => anim.trim()); - - return animations.map(animation => { - const parts = animation.split(/\s+/); - - // 第一个非时间、非数字、非关键字的值通常是动画名称 - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - - // 跳过时间值 (如 0.3s, 200ms) - if (/^\d+(\.\d+)?(s|ms)$/.test(part)) continue; - - // 跳过数字值 (如 iteration count) - if (/^\d+(\.\d+)?$/.test(part)) continue; - - // 跳过CSS关键字 - if (['ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear', 'infinite', - 'normal', 'reverse', 'alternate', 'alternate-reverse', 'forwards', - 'backwards', 'both', 'running', 'paused'].includes(part)) continue; - - // 跳过贝塞尔曲线 - if (part.startsWith('cubic-bezier(')) continue; - - // 这应该是动画名称 - const scopedName = this.scopedKeyframes.get(part); - if (scopedName) { - parts[i] = scopedName; - } - break; - } - - return parts.join(' '); - }).join(', '); - } - - /** - * 处理animation-name属性值 - * @param {string} animationNames - * @returns {string} - */ - processAnimationNames(animationNames) { - return animationNames.split(',').map(name => { - const trimmedName = name.trim(); - const scopedName = this.scopedKeyframes.get(trimmedName); - return scopedName || trimmedName; - }).join(', '); - } - - /** - * 为选择器添加作用域 - * @param {string} selector - * @returns {string} - */ - addScopeToSelector(selector) { - if (!selector.trim()) return selector; - - // 分割多个选择器(用逗号分隔) - const selectors = selector.split(',').map(sel => sel.trim()); - - const scopedSelectors = selectors.map(sel => { - return this.addScopeToSingleSelector(sel); - }); - - return scopedSelectors.join(', '); - } - - /** - * 为单个选择器添加作用域 - * @param {string} selector - * @returns {string} - */ - addScopeToSingleSelector(selector) { - if (!selector.trim()) return selector; - - // 处理伪元素选择器 - 在伪元素前添加作用域 - if (selector.includes('::')) { - const parts = selector.split('::'); - const mainPart = parts[0]; - const pseudoElement = '::' + parts.slice(1).join('::'); - - // 为主要部分添加作用域 - const scopedMain = this.addScopeToSelectorPart(mainPart); - return scopedMain + pseudoElement; - } - - // 处理伪类选择器 - 在伪类前添加作用域 - if (selector.includes(':') && !selector.includes('::')) { - const pseudoMatch = selector.match(/^([^:]+)(:.+)$/); - if (pseudoMatch) { - const mainPart = pseudoMatch[1]; - const pseudoClass = pseudoMatch[2]; - - // 为主要部分添加作用域 - const scopedMain = this.addScopeToSelectorPart(mainPart); - return scopedMain + pseudoClass; - } - } - - // 处理特殊选择器 - if (selector === '*' || selector.startsWith('@')) { - return selector; - } - - // 处理复合选择器 - return this.addScopeToSelectorPart(selector); - } - - /** - * 为选择器部分添加作用域 - * @param {string} selectorPart - * @returns {string} - */ - addScopeToSelectorPart(selectorPart) { - // 处理组合器选择器(>、+、~、空格) - const combinatorRegex = /(\s*[>+~]\s*|\s+)/; - - if (combinatorRegex.test(selectorPart)) { - // 分割选择器,但保留组合器 - const parts = selectorPart.split(combinatorRegex); - if (/^body(?:$|[:\[ ])/.test(parts[0])) { - parts[0] = this.scopeBody + parts[0].slice(4) - return parts.join(''); - } - if (/^:root(?:$|[:\[ ])/.test(parts[0])) { - parts[0] = this.scopeBody + parts[0].slice(5) - return parts.join(''); - } - - // 只在最后一个选择器部分添加作用域 - for (let i = parts.length - 1; i >= 0; i--) { - if (parts[i].trim() && !combinatorRegex.test(parts[i])) { - let tag = parts[i].trim() - if (/^body(?:$|[:\[ ])/.test(tag)) { - parts[i] = this.scopeBody + tag.slice(4) - } else if (/^:root(?:$:[$:\[ ])/.test(tag)) { - parts[i] = this.scopeBody + tag.slice(5) - } else { - parts[i] = parts[i].trim() + this.scopeAttribute; - } - break; - } - } - - return parts.join(''); - } - selectorPart = selectorPart.trim() - let tag = selectorPart.trim() - if (/^body(?:$|[:\[ ])/.test(tag)) { - return this.scopeBody + tag.slice(4) - } else if (/^:root(?:$:[$:\[ ])/.test(tag)) { - return this.scopeBody + tag.slice(5) - } else { - return tag + this.scopeAttribute; - } - } - - /** - * 查找匹配的大括号 - * @param {string} cssText - * @param {number} startIndex - * @returns {object} - */ - findMatchingBrace(cssText, startIndex) { - let i = startIndex; - let content = ''; - let braceLevel = 0; - - while (i < cssText.length) { - content += cssText[i]; - - if (cssText[i] === '{') { - braceLevel++; - } else if (cssText[i] === '}') { - braceLevel--; - if (braceLevel === 0) { - i++; - break; - } - } - i++; - } - - return { - content: content, - endIndex: i - }; - } -} - -// 使用示例 -const parser = new CSSParser(); - -// 示例CSS -const cssText = ` - body{} - .container::before, .a { - content: ""; - animation-name: fadeIn, slideUp; - animation: slideIn 0.5s infinite alternate; - } - - @keyframes slideIn { - from { - transform: translateX(-100%); - } - to { - transform: translateX(0); - } - } - - @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } - } - - @keyframes slideUp { - from { transform: translateY(100%); } - to { transform: translateY(0); } - } - - @media (max-width: 768px) { - .container { - font-size: 14px; - animation: slideIn 0.2s ease-out; - } - - .item { - margin: 5px; - } - } - - .box > .child { - color: green; - } - - .box + .sibling { - margin-top: 20px; - } -`; - -// 解析并添加作用域 -// console.log(parser.parse(cssText, 'comasd-123')); - -// 导出类 -export default parser; - -export { CSSParser } diff --git a/ui/vhtml/vdev.js b/ui/vhtml/vdev.js deleted file mode 100644 index 0052b3b..0000000 --- a/ui/vhtml/vdev.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * vdev.js - * Copyright (C) 2025 veypi - * - * Distributed under terms of the MIT license. - */ - -import DivSelectorPlugin from './vdevselect.js' - -let isSetup = false; -let divSelector = null; - -const postMessage = (typ, args) => { - if (!isSetup) return; // 未初始化前不发送消息 - if (typeof args === 'string' || typeof args === 'number') { - args = { value: args } - } - window.parent.postMessage(Object.assign({ type: typ, from: 'vdev' }, args), '*') -} - -// 延迟执行的初始化函数 -const initializeVdev = () => { - // 在初始化时创建 divSelector 实例 - divSelector = new DivSelectorPlugin(); - - postMessage('iframe-loaded') - divSelector.postMessage = postMessage - - window.addEventListener('keyup', (event) => { - if (event.key === 'Escape') { - postMessage('key-esc') - } - }) - - setTimeout(() => { - if (window.$vhtml && window.$vhtml.$router) { - $vhtml.$router.onChange((url) => { - postMessage('url-change', url.fullPath) - }) - } - }, 100) - - window.addEventListener('message', (event) => { - const data = event.data; - console.log(data) - if (data.from != 'vhtml') { - return - } - switch (data.type) { - case 'reload': - window.location.reload() - break; - case 'magic': - if (divSelector && divSelector.isActive) { - divSelector.deactivate() - } else if (divSelector) { - divSelector.activate() - } - break - } - }); -} - -function setup() { - if (isSetup) return; // 防止重复初始化 - isSetup = true; - initializeVdev(); -} - -export default setup diff --git a/ui/vhtml/vdevselect.js b/ui/vhtml/vdevselect.js deleted file mode 100644 index c59f518..0000000 --- a/ui/vhtml/vdevselect.js +++ /dev/null @@ -1,620 +0,0 @@ -/* - * vdev-select.js - * Copyright (C) 2025 veypi - * - * Distributed under terms of the MIT license. - */ - -const copyToClipboard = async (text) => { - if (navigator.clipboard && navigator.clipboard.writeText) { - return navigator.clipboard.writeText(text) - } - prompt('http环境无法自动复制,请手动复制内容到剪贴板:', text) - return new Promise((resolve) => { }) -} - -/** - * Div Selector Plugin - * 用于选择页面div元素并高亮显示的插件 - */ -class DivSelectorPlugin { - postMessage = (typ, args) => { } - constructor(options = {}) { - this.options = { - highlightColor: '#007bff', - overlayColor: 'rgba(0, 123, 255, 0.1)', - borderWidth: '2px', - zIndex: 10000, - showTagName: true, - ...options - }; - - this.isActive = false; - this.selectedElement = null; - this.currentHoverElement = null; - this.overlay = null; - this.selectedOverlay = null; - this.tooltip = null; - this.selectedTooltip = null; - this.actionPanel = null; - this.originalCursor = ''; - - this.init(); - } - - init() { - this.createStyles(); - this.bindEvents(); - } - - createStyles() { - const style = document.createElement('style'); - style.textContent = ` - .div-selector-overlay { - position: fixed; - pointer-events: none; - border: ${this.options.borderWidth} solid ${this.options.highlightColor}; - background: ${this.options.overlayColor}; - z-index: ${this.options.zIndex}; - transition: all 0.2s ease; - box-sizing: border-box; - } - - .div-selector-selected { - position: fixed; - pointer-events: none; - border: ${this.options.borderWidth} solid #28a745; - background: rgba(40, 167, 69, 0.1); - z-index: ${this.options.zIndex - 1}; - box-sizing: border-box; - } - - .div-selector-tooltip { - position: fixed; - background: rgba(51, 51, 51, 0.95); - cursor:pointer; - color: white; - padding: 8px 12px; - border-radius: 6px; - font-size: 12px; - font-family: 'Consolas', 'Monaco', 'Courier New', monospace; - z-index: ${this.options.zIndex + 1}; - user-select: none; - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - border: 1px solid rgba(255,255,255,0.1); - backdrop-filter: blur(4px); - } - - .div-selector-tooltip.hover { - background: rgba(0, 123, 255, 0.9); - } - - .div-selector-tooltip.selected { - background: rgba(40, 167, 69, 0.9); - border-color: rgba(40, 167, 69, 0.3); - } - - .div-selector-actions { - position: fixed; - background: white; - border: 1px solid #ddd; - border-radius: 8px; - padding: 8px; - z-index: ${this.options.zIndex + 2}; - box-shadow: 0 4px 16px rgba(0,0,0,0.15); - display: flex; - gap: 6px; - min-width: 30px; - backdrop-filter: blur(8px); - } - - .div-selector-btn { - padding: 4px 6px; - border: 1px solid #ddd; - background: white; - border-radius: 6px; - cursor: pointer; - font-size: 12px; - transition: all 0.2s ease; - text-align: left; - white-space: nowrap; - } - - .div-selector-btn:hover { - background: #f8f9fa; - transform: translateX(2px); - } - - .div-selector-btn.danger { - color: #dc3545; - border-color: #dc3545; - } - - .div-selector-btn.danger:hover { - background: #dc3545; - color: white; - } - - .div-selector-btn.primary { - background: #007bff; - color: white; - border-color: #007bff; - } - - .div-selector-btn.primary:hover { - background: #0056b3; - } - - .div-selector-active { - cursor: crosshair !important; - } - `; - document.head.appendChild(style); - } - - bindEvents() { - this.handleMouseMove = this.handleMouseMove.bind(this); - this.handleClick = this.handleClick.bind(this); - this.handleScroll = this.handleScroll.bind(this); - this.handleResize = this.handleResize.bind(this); - } - - activate() { - if (this.isActive) return; - - this.isActive = true; - this.originalCursor = document.body.style.cursor; - document.body.classList.add('div-selector-active'); - - document.addEventListener('mousemove', this.handleMouseMove); - document.addEventListener('click', this.handleClick); - window.addEventListener('scroll', this.handleScroll); - window.addEventListener('resize', this.handleResize); - - this.createOverlay(); - this.createTooltip(); - - } - - deactivate() { - if (!this.isActive) return; - - this.isActive = false; - document.body.style.cursor = this.originalCursor; - document.body.classList.remove('div-selector-active'); - - document.removeEventListener('mousemove', this.handleMouseMove); - document.removeEventListener('click', this.handleClick); - window.removeEventListener('scroll', this.handleScroll); - window.removeEventListener('resize', this.handleResize); - - this.removeOverlay(); - this.removeTooltip(); - // this.clearSelection(); - - console.log('Div Selector Plugin deactivated.'); - } - - handleMouseMove(e) { - if (!this.isActive) return; - - const element = document.elementFromPoint(e.clientX, e.clientY); - if (!element) return; - - // 忽略插件自身的元素 - if (this.isPluginElement(element)) return; - - // 查找最近的div元素 - const divElement = element.tagName === 'DIV' ? element : element.closest('div,[vref]'); - if (!divElement || this.isPluginElement(divElement)) return; - - // 如果已选中元素,不要高亮显示它 - if (this.selectedElement === divElement) return; - - this.currentHoverElement = divElement; - this.highlightElement(divElement); - this.updateHoverTooltip(divElement); - } - - handleClick(e) { - if (!this.isActive) return; - - const element = document.elementFromPoint(e.clientX, e.clientY); - if (!element) return; - - // 忽略插件自身的元素 - if (this.isPluginElement(element)) return; - - e.preventDefault(); - e.stopPropagation(); - - const divElement = element.tagName === 'DIV' ? element : element.closest('div'); - if (!divElement || this.isPluginElement(divElement)) return; - - this.selectElement(divElement); - this.deactivate() - } - - handleScroll() { - if (this.selectedElement) { - this.updateSelectedOverlay(); - this.updateSelectedTooltip(); - this.updateActionPanelPosition(); - } - if (this.currentHoverElement && !this.selectedElement) { - this.highlightElement(this.currentHoverElement); - this.updateHoverTooltip(this.currentHoverElement); - } - } - - handleResize() { - if (this.selectedElement) { - this.updateSelectedOverlay(); - this.updateSelectedTooltip(); - this.updateActionPanelPosition(); - } - } - - isPluginElement(element) { - return element.closest('.div-selector-overlay, .div-selector-selected, .div-selector-tooltip, .div-selector-actions'); - } - - createOverlay() { - this.overlay = document.createElement('div'); - this.overlay.className = 'div-selector-overlay'; - this.overlay.style.display = 'none'; - document.body.appendChild(this.overlay); - } - - createSelectedOverlay() { - this.selectedOverlay = document.createElement('div'); - this.selectedOverlay.className = 'div-selector-selected'; - document.body.appendChild(this.selectedOverlay); - } - - createTooltip() { - this.tooltip = document.createElement('div'); - this.tooltip.className = 'div-selector-tooltip hover'; - this.tooltip.style.display = 'none'; - document.body.appendChild(this.tooltip); - } - - createSelectedTooltip() { - this.selectedTooltip = document.createElement('div'); - this.selectedTooltip.className = 'div-selector-tooltip selected'; - document.body.appendChild(this.selectedTooltip); - } - - highlightElement(element) { - if (!this.overlay) return; - - const rect = element.getBoundingClientRect(); - - this.overlay.style.display = 'block'; - this.overlay.style.left = rect.left + 'px'; - this.overlay.style.top = rect.top + 'px'; - this.overlay.style.width = rect.width + 'px'; - this.overlay.style.height = rect.height + 'px'; - } - - updateHoverTooltip(element) { - if (!this.tooltip) return; - - const elementInfo = this.getElementInfo(element); - const rect = element.getBoundingClientRect(); - - this.tooltip.textContent = elementInfo; - this.tooltip.style.display = 'block'; - - // 计算tooltip位置 - 左上角,稍微偏移避免遮挡 - let left = rect.left - 8; - let top = rect.top - this.tooltip.offsetHeight - 8; - - // 确保不超出视窗 - if (left < 0) left = 8; - if (top < 0) top = rect.top + 8; - - this.tooltip.style.left = left + 'px'; - this.tooltip.style.top = top + 'px'; - } - - selectElement(element) { - // 清除之前的选择 - this.clearSelection(); - - this.selectedElement = element; - - // 创建选中状态的覆盖层和tooltip - this.createSelectedOverlay(); - this.createSelectedTooltip(); - this.updateSelectedOverlay(); - this.updateSelectedTooltip(); - - // 隐藏悬停效果 - if (this.overlay) { - this.overlay.style.display = 'none'; - } - if (this.tooltip) { - this.tooltip.style.display = 'none'; - } - - // 显示操作面板 - this.showActionPanel(); - - console.log('Element selected:', element); - } - - updateSelectedOverlay() { - if (!this.selectedOverlay || !this.selectedElement) return; - - const rect = this.selectedElement.getBoundingClientRect(); - - this.selectedOverlay.style.left = rect.left + 'px'; - this.selectedOverlay.style.top = rect.top + 'px'; - this.selectedOverlay.style.width = rect.width + 'px'; - this.selectedOverlay.style.height = rect.height + 'px'; - } - - updateSelectedTooltip() { - if (!this.selectedTooltip || !this.selectedElement) return; - - const elementInfo = this.getElementInfo(this.selectedElement); - let vref = this.getFilePath(this.selectedElement) - const rect = this.selectedElement.getBoundingClientRect(); - this.selectedTooltip.addEventListener('click', e => { - if (vref) { - this.postMessage('fs-open', vref) - } - e.preventDefault() - e.stopPropagation() - }) - - this.selectedTooltip.textContent = `✓ ${elementInfo}`; - - // 计算tooltip位置 - 选中框的左上角 - let left = rect.left - 8; - let top = rect.top - this.selectedTooltip.offsetHeight - 8; - - // 确保不超出视窗 - if (left < 0) left = 8; - if (top < 0) top = rect.top + 8; - - this.selectedTooltip.style.left = left + 'px'; - this.selectedTooltip.style.top = top + 'px'; - } - - showActionPanel() { - this.removeActionPanel(); - - const rect = this.selectedElement.getBoundingClientRect(); - - this.actionPanel = document.createElement('div'); - this.actionPanel.className = 'div-selector-actions'; - - // 计算位置 - 放在右上角 - const panelWidth = 180; // 预估宽度 - const panelHeight = 40; // 预估高度 - - let left = rect.right - panelWidth; - let top = rect.top + 6; - - // 确保不超出视窗 - if (left + panelWidth > window.innerWidth) { - left = rect.left - panelWidth - 8; - } - if (left < 0) left = 8; - - if (top + panelHeight > window.innerHeight) { - top = window.innerHeight - panelHeight - 8; - } - if (top < 0) top = 8; - - this.actionPanel.style.left = left + 'px'; - this.actionPanel.style.top = top + 'px'; - - // 创建操作按钮 - const buttons = [ - { text: '📋', action: () => this.copySelector(this.selectedElement) }, - // { text: '🔍', action: () => this.copyXPath(this.selectedElement) }, - // { text: '👁️', action: () => this.viewStyles(this.selectedElement) }, - // { text: '📍', action: () => this.clearSelection() }, - { text: 'x', action: () => this.clearSelection() }, - ]; - - buttons.forEach(btn => { - const button = document.createElement('button'); - button.className = `div-selector-btn ${btn.class || ''}`; - button.textContent = btn.text; - button.onclick = btn.action; - this.actionPanel.appendChild(button); - }); - - document.body.appendChild(this.actionPanel); - } - - updateActionPanelPosition() { - if (!this.actionPanel || !this.selectedElement) return; - - const rect = this.selectedElement.getBoundingClientRect(); - const panelWidth = this.actionPanel.offsetWidth; - const panelHeight = this.actionPanel.offsetHeight; - - let left = rect.right - panelWidth + 8; - let top = rect.top - 8; - - // 确保不超出视窗 - if (left + panelWidth > window.innerWidth) { - left = rect.left - panelWidth - 8; - } - if (left < 0) left = 8; - - if (top + panelHeight > window.innerHeight) { - top = window.innerHeight - panelHeight - 8; - } - if (top < 0) top = 8; - - this.actionPanel.style.left = left + 'px'; - this.actionPanel.style.top = top + 'px'; - } - - clearSelection() { - this.selectedElement = null; - - if (this.selectedOverlay) { - this.selectedOverlay.remove(); - this.selectedOverlay = null; - } - - if (this.selectedTooltip) { - this.selectedTooltip.remove(); - this.selectedTooltip = null; - } - - this.removeActionPanel(); - - console.log('Selection cleared'); - } - - getElementInfo(element) { - const rect = element.getBoundingClientRect(); - let vref = element.getAttribute('vref') - if (!vref) { - vref = element.closest('[vref]').getAttribute('vref') - vref += "." + element.tagName.toLowerCase(); - } - return `${vref} (${Math.round(rect.width)}×${Math.round(rect.height)})`; - } - - getFilePath(element) { - let vref = element.getAttribute('vref') - if (!vref) { - vref = element.closest('[vref]').getAttribute('vref') - } - if (vref) { - vref = '/ui' + vref + '.html' - } - return vref - } - - copySelector(element) { - let vref = this.getFilePath(element) - copyToClipboard(vref).then(() => { - console.log('CSS Selector copied:', selector); - this.showNotification('📋 CSS选择器已复制到剪贴板'); - }); - } - - copyXPath(element) { - const xpath = this.generateXPath(element); - copyToClipboard(xpath).then(() => { - console.log('XPath copied:', xpath); - this.showNotification('🔍 XPath已复制到剪贴板'); - }); - } - - viewStyles(element) { - const styles = window.getComputedStyle(element); - const importantStyles = [ - 'display', 'position', 'width', 'height', 'margin', 'padding', - 'background-color', 'color', 'font-size', 'border', 'z-index', 'opacity' - ]; - - let styleInfo = '📊 重要样式属性:\n\n'; - importantStyles.forEach(prop => { - styleInfo += `${prop}: ${styles.getPropertyValue(prop)}\n`; - }); - - alert(styleInfo); - } - - - showNotification(message) { - const notification = document.createElement('div'); - notification.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - background: linear-gradient(135deg, #28a745, #20c997); - color: white; - padding: 12px 18px; - border-radius: 8px; - z-index: ${this.options.zIndex + 10}; - font-size: 14px; - box-shadow: 0 4px 12px rgba(0,0,0,0.2); - transform: translateX(100%); - transition: transform 0.3s ease; - `; - notification.textContent = message; - document.body.appendChild(notification); - - // 动画显示 - setTimeout(() => { - notification.style.transform = 'translateX(0)'; - }, 100); - - // 自动隐藏 - setTimeout(() => { - notification.style.transform = 'translateX(100%)'; - setTimeout(() => { - notification.remove(); - }, 300); - }, 2500); - } - - generateXPath(element) { - if (element.id) { - return `//*[@id="${element.id}"]`; - } - - const path = []; - while (element && element.nodeType === Node.ELEMENT_NODE) { - let index = 0; - let hasFollowingSiblings = false; - let hasPrecedingSiblings = false; - - for (let sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) { - if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) { - hasPrecedingSiblings = true; - index++; - } - } - - for (let sibling = element.nextSibling; sibling && !hasFollowingSiblings; sibling = sibling.nextSibling) { - if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) { - hasFollowingSiblings = true; - } - } - - const tagName = element.nodeName.toLowerCase(); - const pathIndex = (hasPrecedingSiblings || hasFollowingSiblings) ? `[${index + 1}]` : ''; - path.unshift(tagName + pathIndex); - element = element.parentNode; - if (element === document.body) break; - } - - return path.length ? '/' + path.join('/') : null; - } - - removeOverlay() { - if (this.overlay) { - this.overlay.remove(); - this.overlay = null; - } - } - - removeTooltip() { - if (this.tooltip) { - this.tooltip.remove(); - this.tooltip = null; - } - } - - removeActionPanel() { - if (this.actionPanel) { - this.actionPanel.remove(); - this.actionPanel = null; - } - } -} - -export default DivSelectorPlugin diff --git a/ui/vhtml/verror.js b/ui/vhtml/verror.js deleted file mode 100644 index ddd74aa..0000000 --- a/ui/vhtml/verror.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * verror.js - * Copyright (C) 2025 veypi - * - * Distributed under terms of the MIT license. - */ -class ErrorCollector { - constructor() { - this.errorQueue = []; - this.init(); - } - - init() { - // 监听未处理的 Promise 拒绝 - window.addEventListener('unhandledrejection', (event) => { - this.handleError({ - type: 'unhandledrejection', - message: event.reason.message || String(event.reason), - stack: event.reason.stack, - timestamp: new Date().toISOString(), - userAgent: navigator.userAgent, - url: window.location.href, - promiseRejection: true - }); - }); - - // 监听 JavaScript 错误 - window.addEventListener('error', (event) => { - if (event.target === window) { - this.handleError({ - type: 'javascript-error', - message: event.message, - source: event.filename, - line: event.lineno, - column: event.colno, - stack: event.error ? event.error.stack : null, - timestamp: new Date().toISOString(), - userAgent: navigator.userAgent, - url: window.location.href - }); - } - }); - - } - - handleError(errorInfo) { - console.error('Error collected:', errorInfo); - this.queueError(errorInfo); - } - - queueError(errorInfo) { - this.errorQueue.push(errorInfo); - - // 限制队列大小 - if (this.errorQueue.length > 100) { - this.errorQueue.shift(); - } - - // 尝试存储到 localStorage - try { - localStorage.setItem('errorQueue', JSON.stringify(this.errorQueue)); - } catch (e) { - console.warn('Failed to store error queue in localStorage'); - } - } - - // 手动报告错误 - reportError(error, context = {}) { - this.handleError({ - type: 'manual-report', - message: error.message, - stack: error.stack, - context: context, - timestamp: new Date().toISOString(), - userAgent: navigator.userAgent, - url: window.location.href - }); - } -} - -// 初始化错误收集器 -const errorCollector = new ErrorCollector(); - -// 导出实例供其他模块使用 -window.errorCollector = errorCollector; - -export default errorCollector - - diff --git a/ui/vhtml/vget.js b/ui/vhtml/vget.js deleted file mode 100644 index b5c6df3..0000000 --- a/ui/vhtml/vget.js +++ /dev/null @@ -1,322 +0,0 @@ -/* - * vget.js - * Copyright (C) 2024 veypi - * - * 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 = ` -
-
404
-

${url}

-
-` - 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 } diff --git a/ui/vhtml/vmessage.js b/ui/vhtml/vmessage.js deleted file mode 100644 index 2d2a5cf..0000000 --- a/ui/vhtml/vmessage.js +++ /dev/null @@ -1,531 +0,0 @@ -/* - * vmessage.js - * Copyright (C) 2025 veypi - * - * Distributed under terms of the MIT license. - */ - -class Message { - constructor() { - this.container = null; - this.init(); - } - - /** - * 初始化容器 - */ - init() { - this.container = document.createElement('div'); - this.container.className = 'message-container'; - document.body.appendChild(this.container); - - // 添加基础样式 - this.addBaseStyles(); - } - - /** - * 添加基础样式 - */ - addBaseStyles() { - const style = document.createElement('style'); - style.textContent = ` - .message-container { - position: fixed; - top: 60px; - left: 50%; - transform: translateX(-50%); - z-index: 9999; - width: 300px; - } - - .message-item { - margin-bottom: 10px; - padding: 15px; - border-radius: 4px; - box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); - transform: translateY(100%); - opacity: 0; - transition: all 0.3s ease; - display: flex; - align-items: flex-start; - } - - .message-item.show { - transform: translateY(0); - opacity: 1; - } - - .message-icon { - margin-right: 10px; - font-size: 16px; - line-height: 1; - } - - .message-content { - flex: 1; - font-size: 14px; - line-height: 1.4; - } - - .message-close { - margin-left: 10px; - cursor: pointer; - font-size: 16px; - opacity: 0.7; - transition: opacity 0.2s; - } - - .message-close:hover { - opacity: 1; - } - - .message-success { - background-color: #f0f9eb; - border: 1px solid #e1f3d8; - color: var(--color-success, #67c23a); - } - - .message-warning { - background-color: #fdf6ec; - border: 1px solid #faecd8; - color: #e6a23c; - } - - .message-error { - background-color: #fef0f0; - border: 1px solid #fde2e2; - color: #f56c6c; - } - - .message-info { - background-color: #edf2fc; - border: 1px solid #ebeef5; - color: #409eff; - } - - .prompt-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 10000; - display: flex; - justify-content: center; - align-items: center; - opacity: 0; - transition: opacity 0.3s ease; - } - - .prompt-overlay.show { - opacity: 1; - } - - .prompt-dialog { - background: white; - border-radius: 8px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); - min-width: 400px; - max-width: 500px; - transform: scale(0.8); - transition: transform 0.3s ease; - } - - .prompt-overlay.show .prompt-dialog { - transform: scale(1); - } - - .prompt-header { - padding: 20px 20px 10px; - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid #eee; - } - - .prompt-title { - font-size: 18px; - font-weight: 500; - margin: 0; - } - - .prompt-close { - cursor: pointer; - font-size: 20px; - color: #999; - border: none; - background: none; - padding: 0; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - } - - .prompt-close:hover { - color: #666; - } - - .prompt-body { - padding: 20px; - } - - .prompt-content { - margin-bottom: 20px; - font-size: 14px; - line-height: 1.5; - color: #333; - } - - .prompt-input { - width: 100%; - padding: 10px 12px; - border: 1px solid #dcdfe6; - border-radius: 4px; - font-size: 14px; - box-sizing: border-box; - transition: border-color 0.2s; - } - - .prompt-input:focus { - outline: none; - border-color: #409eff; - } - - .prompt-footer { - padding: 15px 20px; - text-align: right; - border-top: 1px solid #eee; - } - - .prompt-btn { - padding: 8px 16px; - border: 1px solid #dcdfe6; - border-radius: 4px; - font-size: 14px; - cursor: pointer; - margin-left: 10px; - transition: all 0.2s; - } - - .prompt-btn-cancel { - background: white; - color: #606266; - } - - .prompt-btn-cancel:hover { - background: #f5f7fa; - border-color: #c0c4cc; - } - - .prompt-btn-confirm { - background: #409eff; - color: white; - border-color: #409eff; - } - - .prompt-btn-confirm:hover { - background: #66b1ff; - border-color: #66b1ff; - } - `; - document.head.appendChild(style); - } - - /** - * 创建消息元素 - * @param {string} type - 消息类型 (success, warning, error, info) - * @param {string} content - 消息内容 - * @param {Object} options - 配置选项 - */ - createMessage(type, content, options = {}) { - const { - duration = 3000, - showClose = true, - onClose = null - } = options; - - const messageItem = document.createElement('div'); - messageItem.className = `message-item message-${type}`; - - // 消息图标 - const icons = { - success: '✓', - warning: '⚠', - error: '✕', - info: 'ℹ' - }; - - const icon = document.createElement('span'); - icon.className = 'message-icon'; - icon.textContent = icons[type] || icons.info; - - // 消息内容 - const contentEl = document.createElement('div'); - contentEl.className = 'message-content'; - contentEl.textContent = content; - - messageItem.appendChild(icon); - messageItem.appendChild(contentEl); - - // 关闭按钮 - if (showClose) { - const closeBtn = document.createElement('span'); - closeBtn.className = 'message-close'; - closeBtn.innerHTML = '×'; - closeBtn.onclick = () => { - this.removeMessage(messageItem); - if (onClose) onClose(); - }; - messageItem.appendChild(closeBtn); - } - - this.container.appendChild(messageItem); - - // 显示动画 - setTimeout(() => { - messageItem.classList.add('show'); - }, 10); - - // 自动关闭 - if (duration > 0) { - setTimeout(() => { - this.removeMessage(messageItem); - if (onClose) onClose(); - }, duration); - } - - return messageItem; - } - - /** - * 移除消息 - * @param {HTMLElement} messageItem - 消息元素 - */ - removeMessage(messageItem) { - if (messageItem && messageItem.parentNode) { - messageItem.classList.remove('show'); - setTimeout(() => { - if (messageItem.parentNode) { - messageItem.parentNode.removeChild(messageItem); - } - }, 300); - } - } - - /** - * 成功消息 - * @param {string} content - 消息内容 - * @param {Object} options - 配置选项 - */ - success(content, options = {}) { - return this.createMessage('success', content, options); - } - - /** - * 警告消息 - * @param {string} content - 消息内容 - * @param {Object} options - 配置选项 - */ - warning(content, options = {}) { - return this.createMessage('warning', content, options); - } - - /** - * 错误消息 - * @param {string} content - 消息内容 - * @param {Object} options - 配置选项 - */ - error(content, options = {}) { - return this.createMessage('error', content, options); - } - - /** - * 信息消息 - * @param {string} content - 消息内容 - * @param {Object} options - 配置选项 - */ - info(content, options = {}) { - return this.createMessage('info', content, options); - } - - /** - * 显示提示框 - * @param {string} content - 提示内容 - * @param {Object} options - 配置选项 - */ - _prompt(content, options = {}) { - return new Promise((resolve, reject) => { - const { - title = '提示', - type = 'confirm', // confirm, input - inputValue = '', - confirmText = '确定', - cancelText = '取消', - onConfirm = null, - onCancel = () => { resolve('') } - } = options; - - // 创建遮罩层 - const overlay = document.createElement('div'); - overlay.className = 'prompt-overlay'; - - // 创建对话框 - const dialog = document.createElement('div'); - dialog.className = 'prompt-dialog'; - - // 头部 - const header = document.createElement('div'); - header.className = 'prompt-header'; - - const titleEl = document.createElement('h3'); - titleEl.className = 'prompt-title'; - titleEl.textContent = title; - - const closeBtn = document.createElement('button'); - closeBtn.className = 'prompt-close'; - closeBtn.innerHTML = '×'; - - header.appendChild(titleEl); - header.appendChild(closeBtn); - - // 主体 - const body = document.createElement('div'); - body.className = 'prompt-body'; - - const contentEl = document.createElement('div'); - contentEl.className = 'prompt-content'; - contentEl.textContent = content; - - body.appendChild(contentEl); - - // 输入框(如果是input类型) - let inputEl = null; - if (type === 'input') { - inputEl = document.createElement('input'); - inputEl.className = 'prompt-input'; - inputEl.type = 'text'; - inputEl.value = inputValue; - body.appendChild(inputEl); - } - - // 底部按钮 - const footer = document.createElement('div'); - footer.className = 'prompt-footer'; - - const cancelBtn = document.createElement('button'); - cancelBtn.className = 'prompt-btn prompt-btn-cancel'; - cancelBtn.textContent = cancelText; - - const confirmBtn = document.createElement('button'); - confirmBtn.className = 'prompt-btn prompt-btn-confirm'; - confirmBtn.textContent = confirmText; - - footer.appendChild(cancelBtn); - footer.appendChild(confirmBtn); - - dialog.appendChild(header); - dialog.appendChild(body); - dialog.appendChild(footer); - overlay.appendChild(dialog); - - document.body.appendChild(overlay); - - // 显示动画 - setTimeout(() => { - overlay.classList.add('show'); - }, 10); - - // 如果是input类型,自动聚焦 - if (inputEl) { - setTimeout(() => { - inputEl.focus(); - }, 300); - } - - // 事件处理 - const closeDialog = (result = null) => { - overlay.classList.remove('show'); - setTimeout(() => { - if (overlay.parentNode) { - overlay.parentNode.removeChild(overlay); - } - }, 300); - return result; - }; - - // 关闭按钮 - closeBtn.onclick = () => { - closeDialog(); - onCancel ? onCancel() : reject(new Error('cancelled')) - }; - - // 取消按钮 - cancelBtn.onclick = () => { - closeDialog(); - onCancel ? onCancel() : reject(new Error('cancelled')) - }; - - // 确认按钮 - confirmBtn.onclick = () => { - const value = inputEl ? inputEl.value : true; - closeDialog(); - resolve(value); - if (onConfirm) onConfirm(value); - }; - - // ESC键关闭 - const handleEsc = (e) => { - if (e.key === 'Escape') { - closeDialog(); - onCancel ? onCancel() : reject(new Error('cancelled')) - document.removeEventListener('keydown', handleEsc); - } - }; - - document.addEventListener('keydown', handleEsc); - - // 点击遮罩层关闭 - overlay.onclick = (e) => { - if (e.target === overlay) { - closeDialog(); - onCancel ? onCancel() : reject(new Error('cancelled')) - } - }; - }); - } - - /** - * 确认框 - * @param {string} content - 确认内容 - * @param {Object} options - 配置选项 - */ - confirm(content, options = {}) { - return this._prompt(content, { - ...options, - type: 'confirm' - }); - } - - /** - * 输入框 - * @param {string} content - 提示内容 - * @param {Object} options - 配置选项 - */ - prompt(message, content, options = {}) { - return this._prompt(message, { - ...options, - type: 'input', - inputValue: content, - }); - } -} - -// 创建单例实例 -const message = new Message(); - -// 导出默认实例和类 -export default message; -export { Message }; diff --git a/ui/vhtml/vproxy.js b/ui/vhtml/vproxy.js deleted file mode 100644 index 0a35ba6..0000000 --- a/ui/vhtml/vproxy.js +++ /dev/null @@ -1,553 +0,0 @@ -/* - * proxy.js - * Copyright (C) 2024 veypi - * - * Distributed under terms of the MIT license. - */ - - -/** @type {([boolean, ()=>void])[]} */ -const callbackList = [] -/** @type {number[]} */ -const cacheUpdateList = [] -// 界面更新响应频率40hz -const sync = () => { - let list = new Set(cacheUpdateList.splice(0)) - let c = 0 - for (let l of list) { - if (callbackList[l]) { - callbackList[l]() - c++ - } - } - if (c > 0) { - console.log(`update ${c}`) - // sync() - } - return c -} -setInterval(sync, 25) - -function GenUniqueID() { - const timestamp = performance.now().toString(36); - const random = Math.random().toString(36).substring(2, 5); - return `${timestamp}-${random}`; -} - -function ForceUpdate() { - for (let c of callbackList) { - if (c) { - c() - } - } -} - -window.$vupdate = (id) => { - console.log('update', id) - callbackList[id]() -} - -function deepAccess(obj, seen = new Set()) { - if (obj && typeof obj === 'object' && !seen.has(obj)) { - seen.add(obj) - for (let key in obj) { - deepAccess(obj[key], seen) - } - } - return obj -} - -/** @type {number[]} */ -var listen_tags = [] -/** -* @param {()=>void} callback -* @returns number -*/ -function Watch(target, callback, options) { - let idx = callbackList.length - listen_tags.push(idx) - if (typeof callback === 'function') { - callbackList.push(() => { - callback(target()) - }) - } else { - callbackList.push(target) - } - let res - try { - res = target() - if (options && options.deep) { - deepAccess(res) - } - } catch (e) { - console.warn('running \n%s\n failed:', target, e) - } finally { - listen_tags.pop() - } - if (typeof callback === 'function') { - callback(res) - } - return idx -} - -function Cancel(idx) { - if (idx < 0 || idx >= callbackList.length) { - return - } - callbackList[idx] = null -} - -const isProxy = Symbol("isProxy") -const DataID = Symbol("DataID") -const DataBind = Symbol("bind") -const rootObj = Symbol("root") -const rootArg = Symbol("root arg") - - -function SetDataRoot(data, root) { - data[rootObj] = root - Object.keys(root).forEach(k => { - if (k in data) { - } else { - data[k] = rootArg - } - }) -} - -function isProxyType(v) { - if (!v || typeof v !== 'object') { - return false - } - if (v instanceof Node || v instanceof Date || v instanceof RegExp || v instanceof Event) { - return false - } - if (v.__noproxy) { - return false - } - if (v.constructor !== Object && v.constructor !== Array) { - return false - } - return true -} - -// oldValue只集成值, 不继承事件 -function copyBind(oldValue, newValue) { - if (!oldValue || !oldValue[isProxy] || !isProxyType(newValue)) { - return newValue - } - let binds = oldValue[DataBind] - if (newValue[isProxy]) { - // 新值也是代理对象,继承旧值的事件绑定, 使用新的代理对象 - if (newValue[DataID] === oldValue[DataID]) { - return newValue - } - for (let k in binds) { - if (newValue[DataBind][k]?.indexOf) { - const currentBinds = newValue[DataBind][k] - const bindSet = new Set(currentBinds) - for (let i of binds[k]) { - if (!bindSet.has(i)) { - currentBinds.push(i) - bindSet.add(i) - } - } - } else { - newValue[DataBind][k] = binds[k] - } - } - } else { - // 新值不是代理对象,继承值,使用旧的代理对象 - if (Array.isArray(newValue) && Array.isArray(oldValue)) { - oldValue.length = 0 - for (let i = 0; i < newValue.length; i++) { - oldValue.push(newValue[i]) - } - return oldValue - } - Object.keys(oldValue).forEach(k => { - if (!newValue.hasOwnProperty(k)) { - delete oldValue[k] - } - }) - Object.keys(newValue).forEach(k => { - if (oldValue[k]?.[isProxy]) { - oldValue[k] = copyBind(oldValue[k], newValue[k]) - } else { - oldValue[k] = newValue[k] - } - }) - return oldValue - } - for (let k in newValue) { - if (k in oldValue && oldValue[k]?.[isProxy]) { - newValue[k] = copyBind(oldValue[k], newValue[k]) - } - } - return newValue -} - - -let stopChecking = false -function Wrap(data, root = undefined) { - const did = GenUniqueID() - let isArray = false - if (Object.prototype.toString.call(data) === '[object Array]') { - isArray = true - } - if (root) { - SetDataRoot(data, root) - } - // data[DataID] = did - const listeners = {} - const handler = { - /** - * @param {Object} target - * @param {string|symbol} key - * - * */ - get(target, key, receiver) { - if (key === DataID) { - return did - } else if (key === isProxy) { - return true - } else if (key === DataBind) { - return listeners - } - const value = Reflect.get(target, key, receiver) - if (value === rootArg) { - return target[rootObj][key] - } - if (typeof key === 'symbol' && stopChecking) { - return value - } else if (typeof value === 'function') { - return value - } - let idx = -1 - if (listen_tags.length > 0) { - let lkey = key - idx = listen_tags[listen_tags.length - 1] - if (isArray) { - lkey = '' - } - if (!listeners.hasOwnProperty(lkey)) { - listeners[lkey] = [idx] - } else if (listeners[lkey].indexOf(idx) == -1) { - listeners[lkey].push(idx) - } - } - if (window.vdev) { - console.log(`${did} get ${key.toString()}:|${value}| `) - } - if (isProxyType(value) && !value[isProxy]) { - let newValue = Wrap(value, undefined) - Reflect.set(target, key, newValue, receiver) - return newValue - } - return value; - }, - set(target, key, newValue, receiver) { - const oldValue = Reflect.get(target, key, receiver) - if (oldValue === rootArg) { - target[rootObj][key] = newValue - return true - } - if (oldValue === newValue) { - return true - } else if (stopChecking) { - return Reflect.set(target, key, newValue, receiver) - } - let result = true - if (Array.isArray(newValue) && Array.isArray(oldValue)) { - stopChecking = true - oldValue.length = 0 - for (let i = 0; i < newValue.length; i++) { - oldValue.push(newValue[i]) - } - stopChecking = false - } else if (oldValue && oldValue[isProxy] && isProxyType(newValue)) { - // 监听对象只赋值可迭代属性 - newValue = copyBind(oldValue, newValue) - result = Reflect.set(target, key, newValue, receiver); - } else { - result = Reflect.set(target, key, newValue, receiver); - } - if (result && listen_tags.length === 0) { - let lkey = key - if (isArray) { - lkey = '' - } - if (listeners[lkey]) { - let i = 0 - if (window.vdev) { - console.log(`before set ${key} listeners:`, listeners[lkey], target) - } - while (i < listeners[lkey].length) { - let cb = listeners[lkey][i] - if (!callbackList[cb]) { - listeners[lkey].splice(i, 1); - } else { - i++ - if (window.vdev) { - console.log(`${did} set ${key}:`, '\n', callbackList[cb], '\n', oldValue, newValue) - } - cacheUpdateList.push(cb) - } - } - } - } - return result; - }, - deleteProperty(target, key) { - if (window.vdev) { - console.log(`del ${key}`) - } - const result = Reflect.deleteProperty(target, key); - if (result && listen_tags.length === 0) { - let lkey = key - if (isArray) { - lkey = '' - } - if (listeners[lkey]) { - let i = 0 - while (i < listeners[lkey].length) { - let cb = listeners[lkey][i] - if (!callbackList[cb]) { - listeners[lkey].splice(i, 1); - } else { - i++ - cacheUpdateList.push(cb) - if (window.vdev) { - console.log(`${did} del ${key}:`, '\n', callbackList[cb], '\n') - } - } - } - } - } - return result - }, - }; - - let res = new Proxy(data, handler); - return res -} - -const expose = { - 'console': console, - 'window': window, - 'prompt': prompt.bind(window), - 'alert': alert.bind(window), - 'confirm': confirm.bind(window), - 'RegExp': RegExp, - 'document': document, - 'Array': Array, - 'Object': Object, - 'Math': Math, - 'Date': Date, - 'JSON': JSON, - 'Symbol': Symbol, - 'Number': Number, - 'eval': eval, - 'isNaN': isNaN, - 'parseInt': parseInt, - 'parseFloat': parseFloat, - 'setTimeout': setTimeout.bind(window), - 'setInterval': setInterval.bind(window), - 'clearTimeout': clearTimeout.bind(window), - 'clearInterval': clearInterval.bind(window), - 'encodeURIComponent': encodeURIComponent, - 'btoa': btoa.bind(window), - 'fetch': fetch.bind(window), - 'TextDecoder': TextDecoder, - 'history': history, - 'requestAnimationFrame': requestAnimationFrame.bind(window), -} - -function newProxy(data, env, tmpenv) { - const proxy = new Proxy(data, { - // 拦截所有属性,防止到 Proxy 对象以外的作用域链查找。 - has(target, key) { - return true; - }, - get(target, key, receiver) { - let v - if (key === '$data') { - v = data - } else if (key === '$env') { - v = env - } else if (key in target) { - v = Reflect.get(target, key, receiver); - } else if (key in env) { - v = env[key] - } else if (tmpenv && key in tmpenv) { - v = tmpenv[key] - } else if (key in expose) { - v = expose[key] - } else if (key in window) { - v = window[key] - } - return v - }, - set(target, key, newValue, receiver) { - // code global variable set will work on "data" - return Reflect.set(target, key, newValue, receiver); - } - }); - return proxy -} -// 运行dom属性绑定等小代码语句 -// for code snapshot -function Run(originCode, data, env, tmpenv) { - let code = originCode.trim() - const cleanCode = code.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '').trim() - const isStatement = /^(var|let|const|if|for|while|switch|try|throw|class|function|return|debugger)\b/.test(cleanCode) - if (!isStatement && (code.indexOf('\n') === -1 || !cleanCode.includes(';'))) { - code = 'return ' + code - } - code = ` -with (sandbox) { -${code} -}` - let res - try { - const fn = new Function('sandbox', code); - res = fn(newProxy(data, env, tmpenv)) - } catch (error) { - console.warn(`Run error:`, originCode, '\n', error) - } - return res -} - -const AsyncFunction = Object.getPrototypeOf(async function() { }).constructor - -// 运行大段代码库 -async function AsyncRun(originCode, data, env, tmpenv) { - let code = originCode.trim() - if (code.indexOf('\n') === -1) { - code = 'return ' + code - } - code = ` -with (sandbox) { -${code} -}` - // try { - const fn = new AsyncFunction('sandbox', code); - return await fn(newProxy(data, env, tmpenv)) - // } catch (error) { - // console.warn('AsyncRun error:', error, '\n', originCode) - // } -} - -function resolvePath(relativePath, currentPath) { - // 如果相对路径已经是绝对路径,直接返回 - if (relativePath.startsWith('/')) { - return relativePath; - } - - // 获取当前路径的目录部分(去掉文件名) - const currentDir = currentPath.substring(0, currentPath.lastIndexOf('/')); - - // 分割路径段 - const currentSegments = currentDir.split('/').filter(segment => segment !== ''); - const relativeSegments = relativePath.split('/').filter(segment => segment !== ''); - - // 处理相对路径段 - for (const segment of relativeSegments) { - if (segment === '..') { - // 返回上一级目录 - if (currentSegments.length > 0) { - currentSegments.pop(); - } - } else if (segment === '.') { - // 当前目录,不做任何操作 - continue; - } else { - // 普通目录或文件名 - currentSegments.push(segment); - } - } - - // 构建绝对路径 - return '/' + currentSegments.join('/'); -} - - -async function ParseImport(code, data, env, src) { - data = data || {} - let scoped = env.scoped || '' - let codeCopy = code - let match; - src = src.startsWith('http') ? src : scoped + src - - const awaitImportRegex = /await import\(['"]([^'"]+)['"]\)/gm; - while ((match = awaitImportRegex.exec(code)) !== null) { - let url = match[1] - if (!url.startsWith('http')) { - url = resolvePath(url, src) - url = window.location.origin + url - } - codeCopy = codeCopy.replace(match[0], `await import('${url}')`) - } - - const importRegex = /^[\s/]*import\s+([\w{},\s]+)\s+from\s+['"]([^'"]+)['"][;\s]*$/gm; - - // 提取所有匹配的模块路径 - while ((match = importRegex.exec(code)) !== null) { - codeCopy = codeCopy.replace(match[0], '') - if (match[0].trim().startsWith('//')) { - continue - } - let url = match[2] - if (!url.startsWith('http') && !url.startsWith('@')) { - if (url.startsWith('/') && scoped) { - url = scoped + url - } else { - url = resolvePath(url, src) - } - } - if (url.startsWith('@')) { - url = url.slice(1) - } - if (!url.endsWith('.js')) { - url += '.js' - } - if (!url.startsWith('http')) { - url = window.location.origin + url - } - let packname = match[1].trim() - try { - let packs = null - if (/^\w+$/.test(packname)) { - packs = packname - } else if (/^{[\w\s,]+}$/.test(packname)) { - packs = packname.slice(1, -1).split(',').map(p => p.trim()) - } else { - throw new Error('unsupported import: ' + match[0]) - } - // window.$env = env - const module = await import(url) - if (typeof packs === 'string') { - if (module.default) { - data[packs] = module.default - } else { - data[packs] = module - } - } else { - packs.forEach((p) => { - if (p in module) { - data[p] = module[p] - } else if (p in module.default) { - data[p] = module.default[p] - } - }) - } - } catch (error) { - console.error(`模块加载失败 (${match[0]}):`, error.message); - } - } - return codeCopy.trim() -} - -export default { - Wrap, Watch, Cancel, ForceUpdate, SetDataRoot, - DataID, GenUniqueID, Run, AsyncRun, ParseImport -} diff --git a/ui/vhtml/vrouter.js b/ui/vhtml/vrouter.js deleted file mode 100644 index c331276..0000000 --- a/ui/vhtml/vrouter.js +++ /dev/null @@ -1,624 +0,0 @@ -import vproxy from './vproxy.js' -import vget from './vget.js' - -// 解析URL字符串,提取路径、查询参数和hash -function parseUrlString(urlString, scoped) { - let url - - let path - // 判断是否为完整URL(包含协议) - if (urlString.startsWith('http://') || urlString.startsWith('https://')) { - url = new URL(urlString) - // 如果是外部URL,返回null(不处理外部链接) - if (url.origin !== window.location.origin) { - return null - } - if (url.pathname.startsWith(scoped)) { - path = url.pathname.slice(scoped.length) // 去掉根路径 - } - } else { - // 相对路径,基于当前origin构建完整URL - url = new URL(urlString, window.location.href) - path = url.pathname - } - - // 解析查询参数 - const query = {} - url.searchParams.forEach((value, key) => { - query[key] = value - }) - - return { - path: path, - query, - hash: url.hash - } -} - - -class VRouter { - #routes = [] - #history = [] - #current = null - #scoped = '' - #listeners = [] - #pageCache = new Map() - #node = null - #env = null - #originContent = [] - #loaded = false - #vhtml = null - #routesByName = new Map() // 添加按名称索引的路由缓存 - - constructor() { - this.init() - } - - get routes() { return this.#routes.slice() } - get history() { return this.#history.slice() } - get current() { return this.#current } - get query() { return this.#current?.query || {} } - get params() { return this.#current?.params || {} } - get scoped() { return this.#scoped } - - onChange(fc) { - this.#listeners.push(fc) - } - - addRoute(route) { - if (!route.path) throw new Error('Route must have a path') - if (route.path != '/' && route.path.endsWith('/')) { - route.path = route.path.slice(0, -1) - } - - const routeConfig = { - path: route.path, - component: route.component, - name: route.name, - meta: route.meta || {}, - children: route.children || [], - matcher: new RouteMatcher(route.path, route.name), - description: route.description || '', - layout: route.layout || '', - } - - this.#routes.push(routeConfig) - - // 如果有名称,添加到名称索引中 - if (route.name) { - this.#routesByName.set(route.name, routeConfig) - } - - // 递归处理子路由 - if (route.children?.length > 0) { - route.children.forEach(child => { - const childPath = route.path + (child.path.startsWith('/') ? child.path : '/' + child.path) - const layout = child.layout || route.layout || '' - const meta = { ...route.meta, ...child.meta } - this.addRoute({ ...child, path: childPath, parent: routeConfig, layout, meta }) - }) - } - } - - addRoutes(routes) { - routes.forEach(route => this.addRoute(route)) - } - - #notifyListeners(to, from) { - this.#listeners.forEach(listener => { - if (typeof listener === 'function') { - try { - listener(to, from) - } catch (error) { - console.error('Error in router listener:', error) - } - } - }) - } - - #setRouterPath(matchedRoute) { - const oldRoute = this.#current - - this.#current = { - path: matchedRoute.path, - fullPath: matchedRoute.fullPath, - params: matchedRoute.params || {}, - query: matchedRoute.query || {}, - hash: new URL(matchedRoute.fullPath, window.location.origin).hash, - meta: matchedRoute.route?.meta || {}, - description: matchedRoute.route?.description || '', - layout: matchedRoute.route?.layout || '', - name: matchedRoute.route?.name, - matched: matchedRoute.route ? [matchedRoute.route] : [] - } - - this.#history.push(this.#current) - if (this.#scoped && !matchedRoute.fullPath.startsWith('http')) { - history.pushState({}, '', this.#scoped + matchedRoute.fullPath) - } else { - history.pushState({}, '', matchedRoute.fullPath) - } - this.#notifyListeners(this.#current, oldRoute) - } - - // 优化后的路由匹配方法,支持多种参数类型 - matchRoute(to) { - // 处理不同类型的路由参数 - const routeInfo = this.normalizeRouteTarget(to) - if (!routeInfo) return null - - const { path, query, params, name } = routeInfo - - // 如果是按名称匹配 - if (name) { - const route = this.#routesByName.get(name) - if (!route) return null - - // 构建带参数的路径 - let resolvedPath = route.path - Object.entries(params).forEach(([key, value]) => { - resolvedPath = resolvedPath.replace(`:${key}`, value) - }) - - const match = route.matcher.match(resolvedPath) - if (match) { - return { - route, - params: { ...match.params, ...params }, - matched: match.matched, - path: resolvedPath, - query, - name - } - } - return null - } - - // 按路径匹配 - for (const route of this.#routes) { - const match = route.matcher.match(path) - if (match && route.component) { - return { - route, - params: { ...match.params, ...params }, - matched: match.matched, - description: route.description, - layout: route.layout, - path, - query, - name: route.name - } - } - } - return null - } - - // 标准化路由目标参数 - normalizeRouteTarget(to) { - let path, query = {}, params = {}, hash = '', name - - if (typeof to === 'string') { - // 字符串类型:解析可能包含的URL、query、hash - const parsed = parseUrlString(to, this.#scoped) - if (!parsed) return null // 外部URL或解析失败 - - path = parsed.path - query = { ...parsed.query } - hash = parsed.hash - } else if (to && typeof to === 'object') { - if (to.path) { - // {path} 类型:path可能也包含query和hash - const parsed = parseUrlString(to.path, this.#scoped) - if (!parsed) return null - - path = parsed.path - // 合并query参数,对象中的query优先级更高 - query = { ...parsed.query, ...(to.query || {}) } - hash = to.hash || parsed.hash - params = to.params || {} - } else if (to.name) { - // {name} 类型 - name = to.name - query = to.query || {} - params = to.params || {} - hash = to.hash || '' - } else { - return null - } - } else { - return null - } - - // 标准化路径 - if (path && !path.startsWith('/')) { - path = '/' + path - } - if (this.#scoped) { - path = path.startsWith(this.#scoped) ? path.slice(this.#scoped.length) : path - } - - if (!path.startsWith('/')) { - path = '/' + path - } - - if (path != '/' && path.endsWith('/')) { - path = path.slice(0, -1) - } - - return { path, query, params, hash, name } - } - - matchTo(to) { - const matchResult = this.matchRoute(to) - if (!matchResult) return null - - const { route, params, query, path, name } = matchResult - - // 构建查询字符串 - let search = '' - if (query && Object.keys(query).length > 0) { - search = '?' + Object.entries(query) - .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) - .join('&') - } - - const fullPath = (path || matchResult.path) + search - - return { - route, - params, - query, - name: name || route.name, - path: path || matchResult.path, - fullPath, - matched: [route] - } - } - - buildUrl(baseUrl, additionalQuery = {}) { - const url = new URL(baseUrl, window.location.origin) - Object.entries(additionalQuery).forEach(([key, value]) => { - url.searchParams.append(key, value) - }) - return url - } - - resolveRoutePath(route, params = {}) { - let path = route.component || route.path - - Object.entries(params).forEach(([key, value]) => { - path = path.replace(`:${key}`, value) - }) - - if (path === '/' || path === '') path = '/index' - if (!path.startsWith('/')) path = '/' + path - if (path.endsWith('.html')) path = path.slice(0, -5) - if (path.endsWith('/')) path = path.slice(0, -1) - - return path - } - - async #navigateTo(matchedRoute) { - if (!matchedRoute) { - console.warn(`No route matched`) - return - } - - const { route, params, query } = matchedRoute - - const to = { - path: matchedRoute.path, - fullPath: matchedRoute.fullPath, - params, - query, - hash: new URL(matchedRoute.fullPath, window.location.origin).hash, - meta: route.meta, - description: route.description, - layout: route.layout, - name: route.name, - matched: [route] - } - - if (this.beforeEnter) { - try { - let shouldContinue = true - const result = await this.beforeEnter(to, this.#current, (next) => { - if (next) { - shouldContinue = false - this.push(next) - } - }) - if (result === false || !shouldContinue) return - } catch (error) { - console.error('Error in beforeEnter guard:', error) - return - } - } - - const cacheKey = matchedRoute.fullPath - let page = this.#pageCache.get(cacheKey) - - this.#setRouterPath(matchedRoute) - if (page) { - page.activate() - } else { - page = new Page(this.#vhtml, this.#node, matchedRoute) - await page.mount(this.#env, this.#originContent, to.layout) - this.#pageCache.set(cacheKey, page) - } - - } - - async push(to) { - const matchedRoute = this.matchTo(to) - - if (!matchedRoute) { - const target = typeof to === 'string' ? to : (to.path || `name: ${to.name}`) - console.warn(`No route matched for ${target}`) - return - } - - await this.#navigateTo(matchedRoute) - } - - replace(to) { - this.push(to) - if (this.#history.length > 1) { - this.#history.splice(-2, 1) - } - } - - go(n) { history.go(n) } - back() { history.back() } - forward() { history.forward() } - - init() { - if (this.#loaded) return - this.#loaded = true - - document.body.addEventListener('click', (event) => { - const linkElement = event.target.closest('a') - if (!linkElement) return - - const href = linkElement.getAttribute('href') - if (!href || href.startsWith('http') || href.startsWith('#')) return - - - event.preventDefault() - const reload = linkElement.hasAttribute('reload') - if (reload) { - window.location.href = href - } else { - this.push(href) - } - }, true) - - window.addEventListener('popstate', () => { - this.push(window.location.href) - }) - } - - ParseVrouter($vhtml, $node, env) { - this.#node = $node - this.#env = env - this.#scoped = env.scoped || '' - this.#originContent = Array.from($node.childNodes) - this.#vhtml = $vhtml - this.push(window.location.href) - } -} - -// 优化后的路由匹配器 -class RouteMatcher { - constructor(path, name) { - this.originalPath = path - this.name = name - this.keys = [] - this.regexp = this.pathToRegexp(path) - } - - pathToRegexp(path) { - const paramPattern = /:([^(/]+)/g - let regexpStr = path.replace(paramPattern, (match, key) => { - this.keys.push(key) - return `(?<${key}>[^/]+)` - }) - - // 处理 *path 形式的通配符 - regexpStr = regexpStr.replace(/\*(\w+)/g, (match, key) => { - this.keys.push(key) - return `(?<${key}>.*)` - }); - - // 如果有未处理的*号,将其替换为允许匹配任意数量字符的正则表达式 - regexpStr = regexpStr.replace(/\*/g, '.*') - return new RegExp(`^${regexpStr}$`) - } - - // 优化的匹配方法,支持多种参数类型 - match(target) { - let path - - // 处理不同类型的输入 - if (typeof target === 'string') { - path = target - } else if (target && typeof target === 'object') { - if (target.path) { - path = target.path - } else if (target.name && target.name === this.name) { - // 如果按名称匹配且名称相符,返回基本匹配 - return { - path: this.originalPath, - params: target.params || {}, - matched: this.originalPath - } - } else { - return null - } - } else { - return null - } - - const match = this.regexp.exec(path) - if (!match) return null - - const params = {} - this.keys.forEach(key => { - if (match.groups?.[key]) { - params[key] = match.groups[key] - } - }) - - return { - path: this.originalPath, - params, - matched: match[0] - } - } -} - -const layoutCache = new Map() - -class Page { - constructor(vhtml, node, matchedRoute) { - this.vhtml = vhtml - this.node = node - this.layoutDom = undefined - this.matchedRoute = matchedRoute - this.htmlPath = this.resolveHtmlPath(matchedRoute) - } - - resolveHtmlPath(matchedRoute) { - let path = matchedRoute.route.component || matchedRoute.route.path - if (typeof path === 'function') { - path = path(matchedRoute.path) - } - - Object.entries(matchedRoute.params).forEach(([key, value]) => { - path = path.replace(`:${key}`, value) - }) - - if (!path.startsWith('/')) path = '/' + path - if (path.endsWith('/')) path = path.slice(0, -1) - if (!path.endsWith('.html')) path = path + '.html' - - return path - } - - async mount(env, originContent, layout) { - - const parser = await vget.FetchUI(this.htmlPath, env) - if (parser.err) { - console.warn(parser.err) - let dom = document.createElement('div') - Object.assign(dom.style, { width: '100%', height: '100%' }) - dom.append(...originContent) - this.node.innerHTML = '' - this.node.append(dom) - this.vhtml.parseRef(this.htmlPath, dom, {}, env, null, true) - return - } - this.title = parser.title || '' - - - const slots = {} - const dom = document.createElement("div") - dom.setAttribute('vsrc', this.htmlPath) - slots[''] = [dom] - this.slots = slots - - if (!layout) { - this.node.innerHTML = '' - this.node.append(dom) - this.vhtml.parseRef(this.htmlPath, dom, {}, env, null) - return - } - - let layoutDom = layoutCache.get(layout) - if (!layoutDom) { - let layoutUrl = layout - if (!layoutUrl.startsWith('/')) { - layoutUrl = '/' + layout - } - if (!layoutUrl.endsWith('.html')) { - layoutUrl += '.html' - } - if (!layoutUrl.startsWith('/layout')) { - layoutUrl = '/layout' + layoutUrl - } - const layoutParser = await vget.FetchUI(layoutUrl, env) - if (layoutParser.err) { - console.warn(`get layout ${layoutUrl} failed.`, layoutParser.err) - this.node.innerHTML = '' - this.node.append(dom) - this.vhtml.parseRef(this.htmlPath, dom, {}, env, null) - return - } - layoutDom = layoutParser.body.cloneNode(true) - layoutCache.set(layout, layoutDom) - dom.$refData = vproxy.Wrap({}) - layoutDom.$refSlots = vproxy.Wrap({ ...slots }) - this.node.innerHTML = '' - this.node.append(layoutDom) - this.layoutDom = layoutDom - this.vhtml.parseRef('/layout/' + layout, layoutDom, {}, env, null, true) - } else { - - this.layoutDom = layoutDom - this.activate() - } - } - - activate() { - if (this.title) document.title = this.title - const layoutDom = this.layoutDom - - if (layoutDom) { - layoutDom.querySelectorAll("vslot").forEach(e => { - if (e.closest('[vref]') === layoutDom && this.slots[e.getAttribute('name') || '']) { - e.innerHTML = '' - } - }) - Object.keys(layoutDom.$refSlots).forEach(key => { - delete layoutDom.$refSlots[key] - }) - Object.assign(layoutDom.$refSlots, this.slots) - if (!layoutDom.isConnected) { - this.node.innerHTML = '' - } - this.node.append(layoutDom) - } else { - this.node.innerHTML = '' - const dom = this.slots[''] - if (dom instanceof Array) { - this.node.append(...dom) - } else { - this.node.append(dom) - } - } - } -} - -const $router = new VRouter() - -const DefaultRoutes = [ - { - path: '/', - component: '/page/index.html', - name: 'home', - }, - { - path: '/404', - component: '/page/404.html', - name: '404' - }, - { - path: '*', - component: (path) => { - if (path.endsWith('.html')) return path - return '/page' + path + '.html' - }, - } -] - -export default { $router, DefaultRoutes }