diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dee6a9..8261c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,41 +1,9 @@ -# CHANGELOG +# Changelog -## v1.0.4 (2026-05-20) +## [1.0.5] - 2026-05-23 +### Added +- Documented project-wide "Manual DOM Sync" design principle for performance critical components. -### 修复 -- **FastList**: 修复了在绝对底部滚动时,由于 Flex 布局的 `rowGap` 被重复计算到虚拟占位高度中,导致实际内容出现偏移(底部出现无法消除的空白区域)的问题。 -- **FastList**: 修复了极端情况下,滚动高度超过预估总高度时导致列表无法渲染(空白)的边界 Bug。 -- **性能优化**: 在不破坏原有精确滚动计算逻辑的前提下,优化了 `calc` 中的高度累加性能,并修正了测试用例因未触发 `scroll` 事件而导致更新失败的问题。 - -## v1.0.3 (2026-05-18) - -### 修复 -- **渲染引擎兼容**: 适配了最新的 State.js 架构,解决了由于组件扫描时序导致的 `FastList`、`FastTree` 等组件无法正确初始化的问题。确保了 `RefreshState` 的正确执行。 -- **模板合并增强**: 解决了当 `slot` 内容为 `TEMPLATE` 标签时,内容无法正确合并至组件内部的问题。 -- **状态透传**: 修复了 `FastGroupedList` 和 `FastTree` 内部嵌套 `FastList` 时,列表数据未正确透传的 Bug。 -- **接口对齐**: 统一了 `FastTree` 和 `FastGroupedList` 的 `refresh` 方法,支持外部通过组件实例直接触发重绘。 - -## v1.0.2 (2026-05-18) - -### 新特性 -- **AutoForm**: 新增 `inline` 模式,支持紧凑的单行表单布局,并增强了与 `DataTable` 的联动能力(数据变化自动刷新)。 -- **Resizer**: - - 支持 `$bind` 指令,实现尺寸的双向绑定。 - - 新增 `resizing` 和 `change` 事件,便于监听实时与最终缩放结果。 - -### 优化 -- **UI**: - - `Modal`, `Dialog`, `Toast` 默认主题色由 `body` 改为 `primary`,提升视觉引导。 - - `Toast` 增加 `rounded` 圆角样式。 - - 统一所有表单控件(input/select/textarea)在 `AutoForm` 中的设置默认为 `{}`。 -- **FastList**: 重构虚拟滚动逻辑为独立的 `VirtualScroll` 模块,提升代码复用性与可维护性。 - -## v1.0.1 (2026-05-14) -- **Perf**: 优化 `FastList` 渲染逻辑,消除 Layout Thrashing,提升复杂列表(GroupedList/Tree)滚动性能达 10 倍以上。 -- **Refactor**: 采用 `requestAnimationFrame` 优化初始化高度计算,减少闪烁。 - -## v1.0.0 (2026-05-14) -- **Feat**: 初始化项目结构,建立 ESM 开发环境。 -- **Refactor**: 将 `base.js` 迁移至 `src/index.js`,采用 ESM 导出。 -- **Build**: 配置 Vite 双轨构建系统。 -- **Test**: 集成 Playwright 自动化测试。 +### Fixed +- Verified VirtualScroll stability and timing. +- Synchronized component registration lifecycle during rapid DOM mutations. diff --git a/dist/base.js b/dist/base.js index bb3dea3..c02d54c 100644 --- a/dist/base.js +++ b/dist/base.js @@ -216,19 +216,14 @@ Component.register("AutoForm", (container) => { container.request = { method: "POST" }; container.response = {}; container.result = null; - const setupData = () => { - if (!container.data || !container.data.__watch) { - container.data = NewState(container.data || {}); - } - container.data.__watch("*", () => { - if (container.inline) { - const dt = container.closest("DataTable"); - if (dt && dt.refresh) dt.refresh(); - } + if (container.inline) { + State.__watch("editingData", (data) => { + container.data = data; }); - }; - if (container.data) setupData(); - else requestAnimationFrame(setupData); + State.__watch("editingSchema", (schema) => { + container.state.schema = schema; + }); + } container.form = $(container, "form"); container.submit = (opt = {}) => { var _a, _b; @@ -253,27 +248,27 @@ Component.register("AutoForm", (container) => { }, Util.makeDom( /*html*/ ` -
-
+
+
-
- - -
- +
+ + +
+ +
+
+ +
+
-
- -
- -
@@ -302,10 +297,12 @@ const AutoForm = { } }, _addAutoFormComponent: (name) => { - var _a; const template = Component.getTemplate("AutoForm"); if (template) { - (_a = $(template.content, "[control-wrapper]")) == null ? void 0 : _a.appendChild(Util.makeDom(`<${name} $if="item.type?.toUpperCase() === '${name.toUpperCase()}'" $name="item.name" $.="item.setting || {}" $bind="this.data[item.name]">`)); + const container = $(template.content, "[control-wrapper]"); + if (container && !container.querySelector(name)) { + container.appendChild(Util.makeDom(`<${name} $if="item.type?.toLowerCase() === '${name.toLowerCase()}'" $name="item.name" $.="item.setting || {}" $bind="thisNode.closest('AutoForm').data[item.name]">`)); + } } } }; @@ -318,34 +315,49 @@ if (typeof document !== "undefined") { else document.addEventListener("DOMContentLoaded", () => setTimeout(initAutoForm, 100), true); } Component.register("TagsInput", (container) => { + container.state = NewState({ tags: [] }); container.addEventListener("bind", (e) => { - container.state.tags = e.detail || []; + container.state.tags = Array.isArray(e.detail) ? e.detail : []; + }); + Object.defineProperty(container, "value", { + get: () => container.state.tags, + set: (v) => { + container.state.tags = Array.isArray(v) ? v : []; + } }); }, Util.makeDom( /*html*/ ` -
- - { ` ), Util.makeDom( /*html*/ - `` + `` )); AutoForm.register("TagsInput"); -const VirtualScroll = () => { +const VirtualScroll = (options = {}) => { const itemHeights = /* @__PURE__ */ new Map(); const groupHeights = /* @__PURE__ */ new Map(); let groupItemCount = 1; const avg = Util.newAvg(); let padTop = 0, rowGap = 0, topMargin = 0, itemMarginTop = null, itemMarginBottom = null, listInited = false; + const providedItemHeight = options.itemHeight || null; return { reset: (list, container) => { listInited = false; @@ -378,12 +394,27 @@ const VirtualScroll = () => { const style = window.getComputedStyle(container); padTop = parseFloat(style.paddingTop) || 0; rowGap = parseFloat(style.rowGap) || 0; - return list.slice(0, Math.min(30, size)); + const visibleCount = Math.max(10, Math.ceil((container.clientHeight || 100) / (providedItemHeight || 32))); + return list.slice(0, Math.min(visibleCount * 3, size)); }, init: (list, refreshCallback) => { if (listInited) return; - const size = list.length, defaultHeight = avg.get() || 32; - for (let i = 0; i < size; i++) if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight); + const size = list.length; + let defaultHeight = providedItemHeight || avg.get() || 32; + if (size > 0 && typeof list[0] === "object" && list[0] !== null && list[0]._itemHeight) { + defaultHeight = list[0]._itemHeight; + } + avg.add(defaultHeight); + if (itemMarginTop === null) { + itemMarginTop = 0; + itemMarginBottom = 0; + } + for (let i = 0; i < size; i++) { + if (!itemHeights.has(i)) { + const ih = typeof list[i] === "object" && list[i] !== null && list[i]._itemHeight ? list[i]._itemHeight : defaultHeight; + itemHeights.set(i, ih); + } + } for (let i = 0; i < size; i += groupItemCount) groupHeights.set(i, Math.min(groupItemCount, size - i) * defaultHeight); listInited = true; refreshCallback(); diff --git a/dist/base.min.js b/dist/base.min.js index 485878f..b53a1b6 100644 --- a/dist/base.min.js +++ b/dist/base.min.js @@ -1 +1 @@ -import{Component as t,NewState as e,Util as i,$ as s,Hash as n,RefreshState as a}from"@web/state";const o={get:({url:t,...e})=>o.request({url:t,method:"GET",...e}),post:({url:t,data:e,...i})=>o.request({url:t,method:"POST",data:e,...i}),put:({url:t,data:e,...i})=>o.request({url:t,method:"PUT",data:e,...i}),delete:({url:t,...e})=>o.request({url:t,method:"DELETE",...e}),head:({url:t,...e})=>o.request({url:t,method:"HEAD",...e}),request:async({url:t,method:e="POST",data:i,headers:s={},responseType:n,timeout:a=1e4})=>{var o;const l={method:e=e.toUpperCase(),signal:null==(o=AbortSignal.timeout)?void 0:o.call(AbortSignal,a)};if(void 0!==i&&"GET"!==e&&"HEAD"!==e){if(i instanceof HTMLFormElement&&(i=new FormData(i)),i&&"object"==typeof i&&!(i instanceof FormData)&&!(i instanceof ArrayBuffer||ArrayBuffer.isView(i))&&Object.values(i).some(t=>t instanceof File||t instanceof Blob||t instanceof FileList||Array.isArray(t)&&t.some(t=>t instanceof File||t instanceof Blob))){const t=new FormData;for(const[e,s]of Object.entries(i))s instanceof FileList||Array.isArray(s)?Array.from(s).forEach(i=>t.append(e,i)):null!=s&&t.append(e,s);i=t}i instanceof FormData?delete s["Content-Type"]:"string"==typeof i||i instanceof ArrayBuffer||ArrayBuffer.isView(i)||(i=JSON.stringify(i),s["Content-Type"]||(s["Content-Type"]="application/json")),l.body=i}Object.keys(s).length&&(l.headers=s);const r={error:null,ok:null,status:0,headers:{},responseType:"",result:null};try{const e=await fetch(t,l);if(Object.assign(r,{ok:e.ok,status:e.status,headers:Object.fromEntries(e.headers.entries())}),!n){const t=e.headers.get("Content-Type")||"";n=t.includes("application/json")?"json":/image|video|audio|pdf|zip|octet-stream/.test(t)?"binary":"text",r.responseType=n}!1===r.ok&&(r.error=(r.statusText||"HTTP "+r.status+" error")+" for "+t),r.result="json"===n?await e.json():"binary"===n?await e.arrayBuffer():await e.text()}catch(t){Object.assign(r,{error:t.message||String(t),ok:!1})}return r}},l=t.register("API",t=>{t.request=e({url:"",method:"GET",headers:{},data:null,timeout:1e4,responseType:""}),t.response=e({loading:!1,ok:null,status:null,error:null,headers:{},responseType:"",result:null}),t.result=e(),t.do=(e={})=>new Promise((i,s)=>{const n={...t.request,...e};if(!n.url)throw new Error(".url is required");n.headers={...t.request.headers,...e.headers},t.response.loading=!0,o.request(n).then(e=>{if(Object.keys(e).forEach(i=>{"result"!==i&&(t.response[i]=e[i])}),e.result&&"object"==typeof e.result&&t.result&&"object"==typeof t.result?Object.assign(t.result,e.result):t.result=e.result,t.response.loading=!1,!1===e.ok)throw new Error(e.error);if("object"==typeof e.result&&e.result.error)throw new Error(e.result.error);t.dispatchEvent(new CustomEvent("response",{detail:e,bubbles:!1})),i(e)}).catch(i=>{var n;!e.noui&&(null==(n=globalThis.UI)?void 0:n.toast)&&UI.toast(i.message,{type:"danger"}),t.dispatchEvent(new CustomEvent("error",{detail:i,bubbles:!0})),s(i)})});let i=null;t.request.__watch(null,()=>{t.hasAttribute("auto")&&t.request.url&&(i||(i=Promise.resolve().then(()=>{t.do(),i=null})))})}),r={};t.register("Modal",t=>{t.modal=new bootstrap.Modal(t),t.addEventListener("bind",e=>{e.detail?t.modal.show():t.modal.hide()}),t.addEventListener("hide.bs.modal",()=>{var e;null==(e=document.activeElement)||e.blur(),t.dispatchEvent(new CustomEvent("change",{bubbles:!1,detail:!1}))}),i.copyFunction(t,t.modal,"show","hide")},i.makeDom('\n\n')),t.register("Dialog",t.getSetupFunction("Modal"),i.makeDom('\n\n'));let d=0;r.showDialog=function({title:t="",message:e="",buttons:i=["{#Close#}"],type:s="body"}){const n=document.body.appendChild(document.createElement("Dialog"));return n.style.zIndex=2e3+ ++d,Promise.resolve().then(()=>{Object.assign(n.state,{message:e,title:t,type:s,buttons:i}),n.show()}),new Promise(t=>{n.addEventListener("change",e=>{d--,t(n.result||0),n.remove()})})},r.alert=function(t,e={}){return r.showDialog({message:t,...e})},r.confirm=function(t,e={}){return new Promise(i=>r.showDialog({message:t,buttons:["{#Cancel#}","{#Confirm#}"],...e}).then(t=>i(t>=2)).catch(()=>i(!1)))},t.register("Toast",t=>{t.toast=new bootstrap.Toast(t,{autohide:t.state.delay>0}),i.copyFunction(t,t.toast,"show","hide"),t.addEventListener("show.bs.toast",()=>{if(t.state.delay>0){let e;const i=()=>{t.state.left=t.state.delay/1e3,e=setInterval(()=>{(!t.isConnected||--t.state.left<=0)&&clearInterval(e)},1e3)};i(),t.addEventListener("mouseenter",()=>{clearInterval(e),t.state.left=void 0}),t.addEventListener("mouseleave",i)}})},i.makeDom('\n
\n
\n
\n
\n \n \n
\n \n
\n
\n \n
\n
\n
\n'),i.makeDom('
')),r.toast=function(t,e={}){const i=e.delay??5e3,n=document.createElement("Toast");n.state={delay:i,left:i?i/1e3:void 0,type:e.type||"primary",message:t,buttons:e.buttons||[]},s(`[toast-container="${e.container||"default"}"]`).appendChild(n),Promise.resolve().then(()=>n.show())},r.toastConfirm=function(t,e={}){return new Promise(i=>r.toast(t,{buttons:["{#Confirm#}"],...e}).then(t=>i(1===t)).catch(()=>i(!1)))},t.register("AutoForm",t=>{t.state.schema||(t.state.schema=[]),t.vertical=t.hasAttribute("vertical"),t.inline=t.hasAttribute("inline"),t.request={method:"POST"},t.response={},t.result=null;const i=()=>{t.data&&t.data.__watch||(t.data=e(t.data||{})),t.data.__watch("*",()=>{if(t.inline){const e=t.closest("DataTable");e&&e.refresh&&e.refresh()}})};t.data?i():requestAnimationFrame(i),t.form=s(t,"form"),t.submit=(e={})=>{var i,s;if(!t.form.reportValidity())return null==(s=null==(i=globalThis.UI)?void 0:i.toast)?void 0:s.call(i,"{#verify failed#}",{type:"danger"});if(!t.dispatchEvent(new CustomEvent("submit",{detail:t.data,cancelable:!0,bubbles:!1})))return;const n={...t.request,data:t.data,noui:!0,...e};let a=null;if(t.api)a=t.api.do(n);else{if(!t.request.url)return console.warn("{#please config .api or .request.url to auto submit#}");a=o.request(n)}a.then(e=>{if(t.response=e,t.result=e.result,"object"==typeof e.result&&e.result.error)throw new Error(e.result.error);t.dispatchEvent(new CustomEvent("response",{detail:e,bubbles:!1}))}).catch(e=>{var i;(null==(i=globalThis.UI)?void 0:i.toast)&&UI.toast(e.message,{type:"danger"}),t.dispatchEvent(new CustomEvent("error",{detail:e,bubbles:!0}))})}},i.makeDom('\n
\n \n
\n \n
\n \n \n
\n \n
\n
\n \n
\n \n
\n
\n
\n
\n \n
\n \n
\n'),i.makeDom(""));const c=[],m={register:e=>{"undefined"!=typeof document&&("loading"!==document.readyState&&t.getTemplate("AutoForm")?m._addAutoFormComponent(e):c.push(e))},_addAutoFormComponent:e=>{var n;const a=t.getTemplate("AutoForm");a&&(null==(n=s(a.content,"[control-wrapper]"))||n.appendChild(i.makeDom(`<${e} $if="item.type?.toUpperCase() === '${e.toUpperCase()}'" $name="item.name" $.="item.setting || {}" $bind="this.data[item.name]">`)))}};if("undefined"!=typeof document){const t=()=>{c.forEach(t=>m._addAutoFormComponent(t)),c.length=0};"loading"!==document.readyState?setTimeout(t,100):document.addEventListener("DOMContentLoaded",()=>setTimeout(t,100),!0)}t.register("TagsInput",t=>{t.addEventListener("bind",e=>{t.state.tags=e.detail||[]})},i.makeDom(`\n
\n \n \n
\n`),i.makeDom("")),m.register("TagsInput");const u=()=>{const t=new Map,e=new Map;let s=1;const n=i.newAvg();let a=0,o=0,l=0,r=null,d=null,c=!1;return{reset:(i,m)=>{if(c=!1,t.clear(),e.clear(),n.clear(),l=0,r=null,d=null,!(null==i?void 0:i.length))return[];const u=i.length;s=Math.ceil(Math.sqrt(u))||10;const p=window.getComputedStyle(m);return a=parseFloat(p.paddingTop)||0,o=parseFloat(p.rowGap)||0,i.slice(0,Math.min(30,u))},init:(i,a)=>{if(c)return;const o=i.length,l=n.get()||32;for(let e=0;e{if(null===r){const t=window.getComputedStyle(a);r=parseFloat(t.marginTop)||0,d=parseFloat(t.marginBottom)||0}0!==i||l||(l=r);const c=a.offsetHeight+r+d+o,m=t.get(i);if(c!==m){t.set(i,c),n.add(c);const a=c-(m||0),o=i-i%s;e.has(o)&&e.set(o,e.get(o)+a)}},calc:(i,r)=>{if(!c||!r)return null;const d=r.length;let m=Math.max(10,Math.ceil((i.clientHeight||100)/(n.get()||32))),u=a+l+o,p=0,h=0,b=0,f=0,g=[];const v=i.scrollTop;for(let i=0;i0?o:0)),postHeight:p>0?Math.max(0,p-2*o):0,renderedList:g,listStartIndex:b}}}};t.register("List",t=>{t.mode=t.getAttribute("mode")||"normal",t.fast=t.hasAttribute("fast"),t.collapsible=t.hasAttribute("collapsible");const s={idfield:"id",labelfield:"label",summaryfield:"summary",groupidfield:"id",grouplabelfield:"label",groupsummaryfield:"summary",groupfield:"group",parentfield:"parent",groupicon:"folder",itemicon:"file"};t.collapsed=e({});const n=()=>{i.updateDefaults(t,s);const e=t.state.list||[],n=[];if("group"===t.mode){const i={};e.forEach(e=>{var s;return(i[s=e[t.groupfield]]??(i[s]=[])).push(e)}),(t.state.groups||[]).forEach(e=>{n.push({type:"group",...e});const s=i[e[t.groupidfield]];s&&s.forEach(t=>n.push({type:"item",...t}))})}else if("tree"===t.mode){const i={};e.forEach(e=>{var s;return(i[s=e[t.parentfield]||""]??(i[s]=[])).push(e)});const s=(e,a,o)=>e.forEach(e=>{var l;const r=e[t.idfield],d=!!(null==(l=i[r])?void 0:l.length),c=t.collapsed[r];n.push({type:"item",...e,_level:a,_hasChildren:d,_parents:o}),d&&!c&&s(i[r],a+1,[...o,r])});s(i[""]||[],0,[])}else e.forEach(t=>n.push({type:"item",...t}));t.state._flatList=n};t.state.__watch("list",n);const a=t.fast?u():null;t.state._renderedList=[],t.refresh=()=>{if(!t.fast)return;const e=a.calc(t,t.state._flatList);e&&(t.state.prevHeight=e.prevHeight,t.state.postHeight=e.postHeight,t.state._listStartIndex=e.listStartIndex,t.state._renderedList=e.renderedList)},t.onItemUpdate=(e,i)=>{t.fast&&a.update(e+(t.state._listStartIndex||0),i)},t.state.__watch("_flatList",e=>{t.fast?(t.state._listStartIndex=0,t.state._renderedList=a.reset(e,t)||[],setTimeout(()=>{t.state._flatList===e&&a.init(e,t.refresh)})):t.state._renderedList=e}),t.selectItem=(e,i)=>{t.hasAttribute("auto-select")&&(t.state.selectedItem=t.state.selectedItem===e[t.idfield]?null:e[t.idfield]),t.dispatchEvent(new CustomEvent("itemclick",{bubbles:!1,detail:{item:e,index:i+(t.fast&&t.state._listStartIndex||0)}}))},t.selectGroup=(e,i)=>{t.hasAttribute("auto-select-group")&&(t.state.selectedGroup=t.state.selectedGroup===e[t.groupidfield]?null:e[t.groupidfield]),t.dispatchEvent(new CustomEvent("groupclick",{bubbles:!1,detail:{item:e,index:i}}))},t.toggleCollapse=e=>{t.collapsible&&e._hasChildren&&(t.collapsed[e[t.idfield]]=!t.collapsed[e[t.idfield]],n())},n()},i.makeDom('\n
\n\t
\n\t\n\t
\n
\n')),t.register("Nav",t=>{t.click=(e,i)=>{e.noselect||i||(n.nav=e.name),t.dispatchEvent(new CustomEvent("nav",{detail:{item:e},bubbles:!1}))}},i.makeDom('\n\n'));let p=!1,h={},b={};const f={start:(t,{onmousemove:e,onmouseup:i})=>{h={x:t.clientX,y:t.clientY,w:0,h:0},b={onmousemove:e,onmouseup:i},p=!0}};"undefined"!=typeof document&&(document.addEventListener("mouseup",t=>{var e;p&&(p=!1,null==(e=b.onmouseup)||e.call(b,{event:t,...h}))}),document.addEventListener("mousemove",t=>{var e;p&&(h.w=t.clientX-h.x,h.h=t.clientY-h.y,null==(e=b.onmousemove)||e.call(b,{event:t,...h}))})),t.register("Resizer",t=>{t.isVertical=t.hasAttribute("vertical");const e=parseInt(t.getAttribute("min"))||10,i=parseInt(t.getAttribute("max"))||1e3,s=t.target||t.previousElementSibling;t.addEventListener("bind",e=>{void 0!==e.detail&&null!==e.detail&&(s.style[t.isVertical?"height":"width"]=e.detail+"px")});const n=(s,n,a)=>{const o=s+(t.isVertical?a:n);return oi?i:o};t.addEventListener("mousedown",e=>{const i=t.isVertical?s.offsetHeight:s.offsetWidth;f.start(e,{onmousemove:({w:e,h:a})=>{const o=n(i,e,a);s.style[t.isVertical?"height":"width"]=o+"px",t.dispatchEvent(new CustomEvent("resizing",{detail:{oldSize:i,newSize:o},bubbles:!1}))},onmouseup:({w:e,h:s})=>{const a=n(i,e,s);t.dispatchEvent(new CustomEvent("resize",{detail:{oldSize:i,newSize:a},bubbles:!1})),t.dispatchEvent(new CustomEvent("change",{detail:a,bubbles:!1}))}})})},i.makeDom("\n
\n"));const g=e({exitBlocks:0});globalThis.State=g,"undefined"!=typeof window&&window.addEventListener("beforeunload",t=>{g.exitBlocks>0&&t.preventDefault()});const v=document.documentElement;if(v.hasAttribute("$data-bs-theme")||v.hasAttribute("data-bs-theme")||v.setAttribute("$data-bs-theme","LocalStorage.darkMode?'dark':'light'"),globalThis.HTTP=o,globalThis.UI=r,globalThis.AutoForm=m,globalThis.MouseMover=f,"undefined"!=typeof document){const t=()=>a(document.documentElement);"loading"!==document.readyState?t():document.addEventListener("DOMContentLoaded",t,!0)}export{l as APIComponent,m as AutoForm,o as HTTP,f as MouseMover,g as State,r as UI,u as VirtualScroll}; +import{Component as t,NewState as e,Util as i,$ as s,Hash as n,RefreshState as a}from"@web/state";const o={get:({url:t,...e})=>o.request({url:t,method:"GET",...e}),post:({url:t,data:e,...i})=>o.request({url:t,method:"POST",data:e,...i}),put:({url:t,data:e,...i})=>o.request({url:t,method:"PUT",data:e,...i}),delete:({url:t,...e})=>o.request({url:t,method:"DELETE",...e}),head:({url:t,...e})=>o.request({url:t,method:"HEAD",...e}),request:async({url:t,method:e="POST",data:i,headers:s={},responseType:n,timeout:a=1e4})=>{var o;const l={method:e=e.toUpperCase(),signal:null==(o=AbortSignal.timeout)?void 0:o.call(AbortSignal,a)};if(void 0!==i&&"GET"!==e&&"HEAD"!==e){if(i instanceof HTMLFormElement&&(i=new FormData(i)),i&&"object"==typeof i&&!(i instanceof FormData)&&!(i instanceof ArrayBuffer||ArrayBuffer.isView(i))&&Object.values(i).some(t=>t instanceof File||t instanceof Blob||t instanceof FileList||Array.isArray(t)&&t.some(t=>t instanceof File||t instanceof Blob))){const t=new FormData;for(const[e,s]of Object.entries(i))s instanceof FileList||Array.isArray(s)?Array.from(s).forEach(i=>t.append(e,i)):null!=s&&t.append(e,s);i=t}i instanceof FormData?delete s["Content-Type"]:"string"==typeof i||i instanceof ArrayBuffer||ArrayBuffer.isView(i)||(i=JSON.stringify(i),s["Content-Type"]||(s["Content-Type"]="application/json")),l.body=i}Object.keys(s).length&&(l.headers=s);const r={error:null,ok:null,status:0,headers:{},responseType:"",result:null};try{const e=await fetch(t,l);if(Object.assign(r,{ok:e.ok,status:e.status,headers:Object.fromEntries(e.headers.entries())}),!n){const t=e.headers.get("Content-Type")||"";n=t.includes("application/json")?"json":/image|video|audio|pdf|zip|octet-stream/.test(t)?"binary":"text",r.responseType=n}!1===r.ok&&(r.error=(r.statusText||"HTTP "+r.status+" error")+" for "+t),r.result="json"===n?await e.json():"binary"===n?await e.arrayBuffer():await e.text()}catch(t){Object.assign(r,{error:t.message||String(t),ok:!1})}return r}},l=t.register("API",t=>{t.request=e({url:"",method:"GET",headers:{},data:null,timeout:1e4,responseType:""}),t.response=e({loading:!1,ok:null,status:null,error:null,headers:{},responseType:"",result:null}),t.result=e(),t.do=(e={})=>new Promise((i,s)=>{const n={...t.request,...e};if(!n.url)throw new Error(".url is required");n.headers={...t.request.headers,...e.headers},t.response.loading=!0,o.request(n).then(e=>{if(Object.keys(e).forEach(i=>{"result"!==i&&(t.response[i]=e[i])}),e.result&&"object"==typeof e.result&&t.result&&"object"==typeof t.result?Object.assign(t.result,e.result):t.result=e.result,t.response.loading=!1,!1===e.ok)throw new Error(e.error);if("object"==typeof e.result&&e.result.error)throw new Error(e.result.error);t.dispatchEvent(new CustomEvent("response",{detail:e,bubbles:!1})),i(e)}).catch(i=>{var n;!e.noui&&(null==(n=globalThis.UI)?void 0:n.toast)&&UI.toast(i.message,{type:"danger"}),t.dispatchEvent(new CustomEvent("error",{detail:i,bubbles:!0})),s(i)})});let i=null;t.request.__watch(null,()=>{t.hasAttribute("auto")&&t.request.url&&(i||(i=Promise.resolve().then(()=>{t.do(),i=null})))})}),r={};t.register("Modal",t=>{t.modal=new bootstrap.Modal(t),t.addEventListener("bind",e=>{e.detail?t.modal.show():t.modal.hide()}),t.addEventListener("hide.bs.modal",()=>{var e;null==(e=document.activeElement)||e.blur(),t.dispatchEvent(new CustomEvent("change",{bubbles:!1,detail:!1}))}),i.copyFunction(t,t.modal,"show","hide")},i.makeDom('\n\n')),t.register("Dialog",t.getSetupFunction("Modal"),i.makeDom('\n\n'));let d=0;r.showDialog=function({title:t="",message:e="",buttons:i=["{#Close#}"],type:s="body"}){const n=document.body.appendChild(document.createElement("Dialog"));return n.style.zIndex=2e3+ ++d,Promise.resolve().then(()=>{Object.assign(n.state,{message:e,title:t,type:s,buttons:i}),n.show()}),new Promise(t=>{n.addEventListener("change",e=>{d--,t(n.result||0),n.remove()})})},r.alert=function(t,e={}){return r.showDialog({message:t,...e})},r.confirm=function(t,e={}){return new Promise(i=>r.showDialog({message:t,buttons:["{#Cancel#}","{#Confirm#}"],...e}).then(t=>i(t>=2)).catch(()=>i(!1)))},t.register("Toast",t=>{t.toast=new bootstrap.Toast(t,{autohide:t.state.delay>0}),i.copyFunction(t,t.toast,"show","hide"),t.addEventListener("show.bs.toast",()=>{if(t.state.delay>0){let e;const i=()=>{t.state.left=t.state.delay/1e3,e=setInterval(()=>{(!t.isConnected||--t.state.left<=0)&&clearInterval(e)},1e3)};i(),t.addEventListener("mouseenter",()=>{clearInterval(e),t.state.left=void 0}),t.addEventListener("mouseleave",i)}})},i.makeDom('\n
\n
\n
\n
\n \n \n
\n \n
\n
\n \n
\n
\n
\n'),i.makeDom('
')),r.toast=function(t,e={}){const i=e.delay??5e3,n=document.createElement("Toast");n.state={delay:i,left:i?i/1e3:void 0,type:e.type||"primary",message:t,buttons:e.buttons||[]},s(`[toast-container="${e.container||"default"}"]`).appendChild(n),Promise.resolve().then(()=>n.show())},r.toastConfirm=function(t,e={}){return new Promise(i=>r.toast(t,{buttons:["{#Confirm#}"],...e}).then(t=>i(1===t)).catch(()=>i(!1)))},t.register("AutoForm",t=>{t.state.schema||(t.state.schema=[]),t.vertical=t.hasAttribute("vertical"),t.inline=t.hasAttribute("inline"),t.request={method:"POST"},t.response={},t.result=null,t.inline&&(g.__watch("editingData",e=>{t.data=e}),g.__watch("editingSchema",e=>{t.state.schema=e})),t.form=s(t,"form"),t.submit=(e={})=>{var i,s;if(!t.form.reportValidity())return null==(s=null==(i=globalThis.UI)?void 0:i.toast)?void 0:s.call(i,"{#verify failed#}",{type:"danger"});if(!t.dispatchEvent(new CustomEvent("submit",{detail:t.data,cancelable:!0,bubbles:!1})))return;const n={...t.request,data:t.data,noui:!0,...e};let a=null;if(t.api)a=t.api.do(n);else{if(!t.request.url)return console.warn("{#please config .api or .request.url to auto submit#}");a=o.request(n)}a.then(e=>{if(t.response=e,t.result=e.result,"object"==typeof e.result&&e.result.error)throw new Error(e.result.error);t.dispatchEvent(new CustomEvent("response",{detail:e,bubbles:!1}))}).catch(e=>{var i;(null==(i=globalThis.UI)?void 0:i.toast)&&UI.toast(e.message,{type:"danger"}),t.dispatchEvent(new CustomEvent("error",{detail:e,bubbles:!0}))})}},i.makeDom('\n
\n
\n
\n \n
\n \n \n
\n \n
\n
\n \n
\n \n
\n
\n
\n
\n \n
\n
\n
\n'),i.makeDom(""));const c=[],m={register:e=>{"undefined"!=typeof document&&("loading"!==document.readyState&&t.getTemplate("AutoForm")?m._addAutoFormComponent(e):c.push(e))},_addAutoFormComponent:e=>{const n=t.getTemplate("AutoForm");if(n){const t=s(n.content,"[control-wrapper]");t&&!t.querySelector(e)&&t.appendChild(i.makeDom(`<${e} $if="item.type?.toLowerCase() === '${e.toLowerCase()}'" $name="item.name" $.="item.setting || {}" $bind="thisNode.closest('AutoForm').data[item.name]">`))}}};if("undefined"!=typeof document){const t=()=>{c.forEach(t=>m._addAutoFormComponent(t)),c.length=0};"loading"!==document.readyState?setTimeout(t,100):document.addEventListener("DOMContentLoaded",()=>setTimeout(t,100),!0)}t.register("TagsInput",t=>{t.state=e({tags:[]}),t.addEventListener("bind",e=>{t.state.tags=Array.isArray(e.detail)?e.detail:[]}),Object.defineProperty(t,"value",{get:()=>t.state.tags,set:e=>{t.state.tags=Array.isArray(e)?e:[]}})},i.makeDom(`\n
\n \n \n
\n`),i.makeDom("")),m.register("TagsInput");const u=(t={})=>{const e=new Map,s=new Map;let n=1;const a=i.newAvg();let o=0,l=0,r=0,d=null,c=null,m=!1;const u=t.itemHeight||null;return{reset:(t,i)=>{if(m=!1,e.clear(),s.clear(),a.clear(),r=0,d=null,c=null,!(null==t?void 0:t.length))return[];const h=t.length;n=Math.ceil(Math.sqrt(h))||10;const p=window.getComputedStyle(i);o=parseFloat(p.paddingTop)||0,l=parseFloat(p.rowGap)||0;const b=Math.max(10,Math.ceil((i.clientHeight||100)/(u||32)));return t.slice(0,Math.min(3*b,h))},init:(t,i)=>{if(m)return;const o=t.length;let l=u||a.get()||32;o>0&&"object"==typeof t[0]&&null!==t[0]&&t[0]._itemHeight&&(l=t[0]._itemHeight),a.add(l),null===d&&(d=0,c=0);for(let i=0;i{if(null===d){const t=window.getComputedStyle(i);d=parseFloat(t.marginTop)||0,c=parseFloat(t.marginBottom)||0}0!==t||r||(r=d);const o=i.offsetHeight+d+c+l,m=e.get(t);if(o!==m){e.set(t,o),a.add(o);const i=o-(m||0),l=t-t%n;s.has(l)&&s.set(l,s.get(l)+i)}},calc:(t,i)=>{if(!m||!i)return null;const d=i.length;let c=Math.max(10,Math.ceil((t.clientHeight||100)/(a.get()||32))),u=o+r+l,h=0,p=0,b=0,f=0,g=[];const v=t.scrollTop;for(let t=0;t0?l:0)),postHeight:h>0?Math.max(0,h-2*l):0,renderedList:g,listStartIndex:b}}}};t.register("List",t=>{t.mode=t.getAttribute("mode")||"normal",t.fast=t.hasAttribute("fast"),t.collapsible=t.hasAttribute("collapsible");const s={idfield:"id",labelfield:"label",summaryfield:"summary",groupidfield:"id",grouplabelfield:"label",groupsummaryfield:"summary",groupfield:"group",parentfield:"parent",groupicon:"folder",itemicon:"file"};t.collapsed=e({});const n=()=>{i.updateDefaults(t,s);const e=t.state.list||[],n=[];if("group"===t.mode){const i={};e.forEach(e=>{var s;return(i[s=e[t.groupfield]]??(i[s]=[])).push(e)}),(t.state.groups||[]).forEach(e=>{n.push({type:"group",...e});const s=i[e[t.groupidfield]];s&&s.forEach(t=>n.push({type:"item",...t}))})}else if("tree"===t.mode){const i={};e.forEach(e=>{var s;return(i[s=e[t.parentfield]||""]??(i[s]=[])).push(e)});const s=(e,a,o)=>e.forEach(e=>{var l;const r=e[t.idfield],d=!!(null==(l=i[r])?void 0:l.length),c=t.collapsed[r];n.push({type:"item",...e,_level:a,_hasChildren:d,_parents:o}),d&&!c&&s(i[r],a+1,[...o,r])});s(i[""]||[],0,[])}else e.forEach(t=>n.push({type:"item",...t}));t.state._flatList=n};t.state.__watch("list",n);const a=t.fast?u():null;t.state._renderedList=[],t.refresh=()=>{if(!t.fast)return;const e=a.calc(t,t.state._flatList);e&&(t.state.prevHeight=e.prevHeight,t.state.postHeight=e.postHeight,t.state._listStartIndex=e.listStartIndex,t.state._renderedList=e.renderedList)},t.onItemUpdate=(e,i)=>{t.fast&&a.update(e+(t.state._listStartIndex||0),i)},t.state.__watch("_flatList",e=>{t.fast?(t.state._listStartIndex=0,t.state._renderedList=a.reset(e,t)||[],setTimeout(()=>{t.state._flatList===e&&a.init(e,t.refresh)})):t.state._renderedList=e}),t.selectItem=(e,i)=>{t.hasAttribute("auto-select")&&(t.state.selectedItem=t.state.selectedItem===e[t.idfield]?null:e[t.idfield]),t.dispatchEvent(new CustomEvent("itemclick",{bubbles:!1,detail:{item:e,index:i+(t.fast&&t.state._listStartIndex||0)}}))},t.selectGroup=(e,i)=>{t.hasAttribute("auto-select-group")&&(t.state.selectedGroup=t.state.selectedGroup===e[t.groupidfield]?null:e[t.groupidfield]),t.dispatchEvent(new CustomEvent("groupclick",{bubbles:!1,detail:{item:e,index:i}}))},t.toggleCollapse=e=>{t.collapsible&&e._hasChildren&&(t.collapsed[e[t.idfield]]=!t.collapsed[e[t.idfield]],n())},n()},i.makeDom('\n
\n\t
\n\t\n\t
\n
\n')),t.register("Nav",t=>{t.click=(e,i)=>{e.noselect||i||(n.nav=e.name),t.dispatchEvent(new CustomEvent("nav",{detail:{item:e},bubbles:!1}))}},i.makeDom('\n\n'));let h=!1,p={},b={};const f={start:(t,{onmousemove:e,onmouseup:i})=>{p={x:t.clientX,y:t.clientY,w:0,h:0},b={onmousemove:e,onmouseup:i},h=!0}};"undefined"!=typeof document&&(document.addEventListener("mouseup",t=>{var e;h&&(h=!1,null==(e=b.onmouseup)||e.call(b,{event:t,...p}))}),document.addEventListener("mousemove",t=>{var e;h&&(p.w=t.clientX-p.x,p.h=t.clientY-p.y,null==(e=b.onmousemove)||e.call(b,{event:t,...p}))})),t.register("Resizer",t=>{t.isVertical=t.hasAttribute("vertical");const e=parseInt(t.getAttribute("min"))||10,i=parseInt(t.getAttribute("max"))||1e3,s=t.target||t.previousElementSibling;t.addEventListener("bind",e=>{void 0!==e.detail&&null!==e.detail&&(s.style[t.isVertical?"height":"width"]=e.detail+"px")});const n=(s,n,a)=>{const o=s+(t.isVertical?a:n);return oi?i:o};t.addEventListener("mousedown",e=>{const i=t.isVertical?s.offsetHeight:s.offsetWidth;f.start(e,{onmousemove:({w:e,h:a})=>{const o=n(i,e,a);s.style[t.isVertical?"height":"width"]=o+"px",t.dispatchEvent(new CustomEvent("resizing",{detail:{oldSize:i,newSize:o},bubbles:!1}))},onmouseup:({w:e,h:s})=>{const a=n(i,e,s);t.dispatchEvent(new CustomEvent("resize",{detail:{oldSize:i,newSize:a},bubbles:!1})),t.dispatchEvent(new CustomEvent("change",{detail:a,bubbles:!1}))}})})},i.makeDom("\n
\n"));const g=e({exitBlocks:0});globalThis.State=g,"undefined"!=typeof window&&window.addEventListener("beforeunload",t=>{g.exitBlocks>0&&t.preventDefault()});const v=document.documentElement;if(v.hasAttribute("$data-bs-theme")||v.hasAttribute("data-bs-theme")||v.setAttribute("$data-bs-theme","LocalStorage.darkMode?'dark':'light'"),globalThis.HTTP=o,globalThis.UI=r,globalThis.AutoForm=m,globalThis.MouseMover=f,"undefined"!=typeof document){const t=()=>a(document.documentElement);"loading"!==document.readyState?t():document.addEventListener("DOMContentLoaded",t,!0)}export{l as APIComponent,m as AutoForm,o as HTTP,f as MouseMover,g as State,r as UI,u as VirtualScroll}; diff --git a/package-lock.json b/package-lock.json index e42e0d9..a8a478c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@web/base", - "version": "1.0.2", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@web/base", - "version": "1.0.2", + "version": "1.0.5", "devDependencies": { "@playwright/test": "^1.40.0", "@rollup/plugin-terser": "^1.0.0", diff --git a/package.json b/package.json index 639702b..8e5a7e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@web/base", - "version": "1.0.4", "type": "module", + "version": "1.0.5", + "type": "module", "main": "dist/base.js", "module": "dist/base.js", "files": [ diff --git a/src/form.js b/src/form.js index 58285c1..889bcdb 100644 --- a/src/form.js +++ b/src/form.js @@ -1,5 +1,6 @@ import { Component, NewState, Util, $ } from '@web/state' import { HTTP } from './http.js' +import { State } from './index.js' Component.register('AutoForm', container => { if (!container.state.schema) container.state.schema = [] @@ -9,23 +10,16 @@ Component.register('AutoForm', container => { container.response = {} container.result = null - // 初始化数据代理与联动逻辑 - const setupData = () => { - if (!container.data || !container.data.__watch) { - container.data = NewState(container.data || {}) - } - container.data.__watch('*', () => { - if (container.inline) { - const dt = container.closest('DataTable') - if (dt && dt.refresh) dt.refresh() - } + // 初始化数据联动:如果处于行编辑模式(inline),监听全局编辑状态 + if (container.inline) { + State.__watch('editingData', (data) => { + container.data = data + }) + State.__watch('editingSchema', (schema) => { + container.state.schema = schema }) } - // 如果 data 已经存在(可能是同步绑定的),立即设置;否则延迟一帧等待属性绑定完成 - if (container.data) setupData() - else requestAnimationFrame(setupData) - container.form = $(container, 'form') container.submit = (opt = {}) => { if (!container.form.reportValidity()) return globalThis.UI?.toast?.('{#verify failed#}', { type: 'danger' }) @@ -46,27 +40,27 @@ Component.register('AutoForm', container => { }) } }, Util.makeDom(/*html*/` -
-
+
+
-
- - -
- +
+ + +
+ +
+
+ +
+
-
- -
- -
@@ -94,52 +88,69 @@ export const AutoForm = { _addAutoFormComponent: name => { const template = Component.getTemplate('AutoForm') if (template) { - $(template.content, '[control-wrapper]')?.appendChild(Util.makeDom(`<${name} $if="item.type?.toUpperCase() === '${name.toUpperCase()}'" $name="item.name" $.="item.setting || {}" $bind="this.data[item.name]">`)) + const container = $(template.content, '[control-wrapper]') + if (container && !container.querySelector(name)) { + container.appendChild(Util.makeDom(`<${name} $if="item.type?.toLowerCase() === '${name.toLowerCase()}'" $name="item.name" $.="item.setting || {}" $bind="thisNode.closest('AutoForm').data[item.name]">`)) + } } } + } if (typeof document !== 'undefined') { - const initAutoForm = () => { + const initAutoForm = () => { _pendingAutoFormComponents.forEach(name => AutoForm._addAutoFormComponent(name)) _pendingAutoFormComponents.length = 0 } if (document.readyState !== 'loading') setTimeout(initAutoForm, 100) - else document.addEventListener('DOMContentLoaded', () => setTimeout(initAutoForm, 100), true) + else document.addEventListener('DOMContentLoaded', () => setTimeout(initAutoForm, 100), true) } Component.register('TagsInput', container => { + container.state = NewState({ tags: [] }) container.addEventListener('bind', e => { - container.state.tags = e.detail || [] + container.state.tags = Array.isArray(e.detail) ? e.detail : [] + }) + // Handle direct property access + Object.defineProperty(container, 'value', { + get: () => container.state.tags, + set: v => { container.state.tags = Array.isArray(v) ? v : []; } }) }, Util.makeDom(/*html*/` -
- -
-`), Util.makeDom(/*html*/``)) +`), Util.makeDom(/*html*/``)) AutoForm.register('TagsInput') +