diff --git a/dist/base.js b/dist/base.js index 810031a..d64dc70 100644 --- a/dist/base.js +++ b/dist/base.js @@ -362,14 +362,8 @@ const VirtualScroll = () => { const groupHeights = /* @__PURE__ */ new Map(); let groupItemCount = 1; const avg = Util.newAvg(); - let padTop = 0; - let rowGap = 0; - let topMargin = 0; - let itemMarginTop = null; - let itemMarginBottom = null; - let listInited = false; + let padTop = 0, rowGap = 0, topMargin = 0, itemMarginTop = null, itemMarginBottom = null, listInited = false; return { - // 核心1:重置与首屏预渲染 reset: (list, container) => { listInited = false; itemHeights.clear(); @@ -386,25 +380,18 @@ const VirtualScroll = () => { rowGap = parseFloat(style.rowGap) || 0; return list.slice(0, Math.min(30, size)); }, - // 核心2:无痛初始化缓存 (通过 rAF 调用) init: (list, refreshCallback) => { if (listInited) return; - const size = list.length; - const defaultHeight = avg.get() || 32; - for (let i = 0; i < size; i++) { - if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight); - } + const size = list.length, defaultHeight = avg.get() || 32; + for (let i = 0; i < size; i++) if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight); for (let i = 0; i < size; i += groupItemCount) { let sum = 0; - for (let j = i; j < Math.min(i + groupItemCount, size); j++) { - sum += itemHeights.get(j); - } + for (let j = i; j < Math.min(i + groupItemCount, size); j++) sum += itemHeights.get(j); groupHeights.set(i, sum); } listInited = true; refreshCallback(); }, - // 核心3:原汁原味的极速高度更新 update: (absoluteIndex, node) => { if (itemMarginTop === null) { const style = window.getComputedStyle(node); @@ -417,21 +404,14 @@ const VirtualScroll = () => { if (newHeight !== oldHeight) { itemHeights.set(absoluteIndex, newHeight); avg.add(newHeight); - const offset = newHeight - (oldHeight || 0); - const groupIndex = absoluteIndex - absoluteIndex % groupItemCount; + const offset = newHeight - (oldHeight || 0), groupIndex = absoluteIndex - absoluteIndex % groupItemCount; if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset); } }, - // 核心4:原汁原味的状态机滚动计算 calc: (container, list) => { if (!listInited || !list) return null; - const size = list.length; - const visibleCount = Math.ceil((container.clientHeight || 100) / (avg.get() || 32)); - let prev = padTop + topMargin + rowGap; - let post = 0; - let status = 0; - let listStartIndex = 0; - let listEndIndex = 0; + const size = list.length, visibleCount = Math.ceil((container.clientHeight || 100) / (avg.get() || 32)); + let prev = padTop + topMargin + rowGap, post = 0, status = 0, listStartIndex = 0, listEndIndex = 0; for (let i = 0; i < size; i++) { if (status === 0) { const gh = groupHeights.get(i); @@ -440,9 +420,8 @@ const VirtualScroll = () => { i += Math.min(groupItemCount, size - i) - 1; } else { const ih = itemHeights.get(i); - if (prev + ih < container.scrollTop) { - prev += ih; - } else { + if (prev + ih < container.scrollTop) prev += ih; + else { status = 1; let visibleStartIndex = Math.max(0, i); listStartIndex = Math.max(0, visibleStartIndex - visibleCount); @@ -456,246 +435,128 @@ const VirtualScroll = () => { if (gh) { post += gh; i += groupItemCount - 1; - } else { - post += itemHeights.get(i); - } + } else post += itemHeights.get(i); } } - return { - prevHeight: prev - padTop - topMargin - rowGap, - postHeight: post, - renderedList: list.slice(listStartIndex, listEndIndex), - listStartIndex - }; + return { prevHeight: Math.max(0, prev - padTop - topMargin - rowGap), postHeight: post, renderedList: list.slice(listStartIndex, listEndIndex), listStartIndex }; } }; }; -const FastListComponent = Component.register("FastList", (container) => { - const vs = VirtualScroll(); - container.state.renderedList = []; - container.onItemUpdate = (index2, node) => { - vs.update(index2 + (container.state._listStartIndex || 0), node); +Component.register("List", (container) => { + container.mode = container.getAttribute("mode") || "normal"; + container.fast = container.hasAttribute("fast"); + container.collapsible = container.hasAttribute("collapsible"); + const defaultSets = { + idfield: "id", + labelfield: "label", + summaryfield: "summary", + groupidfield: "id", + grouplabelfield: "label", + groupsummaryfield: "summary", + groupfield: "group", + parentfield: "parent", + groupicon: "folder", + itemicon: "file" }; + container.collapsed = NewState({}); + const updateFlatList = () => { + Util.updateDefaults(container, defaultSets); + const list = container.state.list || [], flatList = []; + if (container.mode === "group") { + const itemMap = {}; + list.forEach((item) => { + var _a; + return (itemMap[_a = item[container.groupfield]] ?? (itemMap[_a] = [])).push(item); + }); + (container.state.groups || []).forEach((group) => { + flatList.push({ type: "group", ...group }); + const items = itemMap[group[container.groupidfield]]; + if (items) items.forEach((item) => flatList.push({ type: "item", ...item })); + }); + } else if (container.mode === "tree") { + const childrenMap = {}; + list.forEach((item) => { + var _a; + return (childrenMap[_a = item[container.parentfield] || ""] ?? (childrenMap[_a] = [])).push(item); + }); + const traverse = (items, level, parents) => items.forEach((item) => { + var _a; + const id = item[container.idfield], hasChildren = !!((_a = childrenMap[id]) == null ? void 0 : _a.length); + const isCollapsed = container.collapsed[id]; + flatList.push({ type: "item", ...item, _level: level, _hasChildren: hasChildren, _parents: parents }); + if (hasChildren && !isCollapsed) traverse(childrenMap[id], level + 1, [...parents, id]); + }); + traverse(childrenMap[""] || [], 0, []); + } else list.forEach((item) => flatList.push({ type: "item", ...item })); + container.state._flatList = flatList; + }; + container.state.__watch("list", updateFlatList); + const vs = container.fast ? VirtualScroll() : null; + container.state._renderedList = []; container.refresh = () => { - const res = vs.calc(container, container.state.list); + if (!container.fast) return; + const res = vs.calc(container, container.state._flatList); if (res) { container.state.prevHeight = res.prevHeight; container.state.postHeight = res.postHeight; container.state._listStartIndex = res.listStartIndex; - container.state.renderedList = res.renderedList; + container.state._renderedList = res.renderedList; } }; - container.state.__watch("list", (list) => { - container.state._listStartIndex = 0; - container.state.renderedList = vs.reset(list, container) || []; - requestAnimationFrame(() => { - if (container.state.list !== list) return; - vs.init(list, container.refresh); - }); - }); -}, Util.makeDom( - /*html*/ - ` -
-
-
-
-
-` -)); -Component.attachSelectFeature = (container, eventName = "itemclick", selectActionName = "selectItem", activeActionName = "itemActiveTag", selectedItemKey = "selectedItem", idfieldKey = "idfield", selectableAttr = "auto-select") => { - const selectable = selectableAttr && container.hasAttribute(selectableAttr); - container[selectActionName] = (item, index2) => { - if (selectable) container.state[selectedItemKey] = container.state[selectedItemKey] === item[container[idfieldKey]] ? null : item[container[idfieldKey]]; - container.dispatchEvent(new CustomEvent(eventName, { bubbles: false, detail: !selectable || container.state[selectedItemKey] ? { item, index: index2 } : { item: {}, index: null } })); + container.onItemUpdate = (index2, node) => { + if (container.fast) vs.update(index2 + (container.state._listStartIndex || 0), node); }; - container[activeActionName] = (item) => container.state[selectedItemKey] !== null && container.state[selectedItemKey] === item[container[idfieldKey]] ? "active" : ""; -}; -Component.register("List", (container) => { - Component.attachSelectFeature(container); - const defaultSets = { idfield: "id", labelfield: "label", summaryfield: "summary" }; - container.state.__watch("list", (oldList) => { - Util.updateDefaults(container, defaultSets); - }); -}, Util.makeDom( - /*html*/ - ` -
-
- -
\${item[this.labelfield]}
-
-
-
-
-` -)); -Component.register("GroupedList", (container) => { - Component.getSetupFunction("List")(container); - container._selectItem = container.selectItem; - container.selectItem = (item, index2) => { - container.state.selectedGroup = null; - container._selectItem(item, index2); - }; - Component.attachSelectFeature(container, "groupclick", "selectGroup", "groupActiveTag", "selectedGroup", "groupidfield", container.hasAttribute("auto-select-group") || container.hasAttribute("auto-select") && "group-selectable"); - container._selectGroup = container.selectGroup; - container.selectGroup = (item, index2) => { - container.state.selectedItem = null; - container._selectGroup(item, index2); - }; - const defaultSets = { typefield: "type", groupidfield: "id", grouplabelfield: "label", groupsummaryfield: "summary", groupfield: "group" }; - container.state.__watch("list", (oldList) => { - Util.updateDefaults(container, defaultSets); - const newList = []; - (container.state.groups || []).forEach((group) => { - newList.push({ [container.typefield]: "group", ...group }); - if (oldList) oldList.forEach((item) => { - if (item[container.groupfield] === group[container.groupidfield]) { - newList.push({ [container.typefield]: "item", ...item }); - } + container.state.__watch("_flatList", (flatList) => { + if (container.fast) { + container.state._listStartIndex = 0; + container.state._renderedList = vs.reset(flatList, container) || []; + requestAnimationFrame(() => { + if (container.state._flatList === flatList) vs.init(flatList, container.refresh); }); - }); - return newList; + } else container.state._renderedList = flatList; }); -}, Util.makeDom( - /*html*/ - ` -
- -
-` -)); -Component.register("FastGroupedList", (container) => { - Component.getSetupFunction("List")(container); - Component.getSetupFunction("GroupedList")(container); - container.refresh = () => { - var _a; - return (_a = container.querySelector("FastList")) == null ? void 0 : _a.refresh(); + container.selectItem = (item, index2) => { + if (container.hasAttribute("auto-select")) container.state.selectedItem = container.state.selectedItem === item[container.idfield] ? null : item[container.idfield]; + container.dispatchEvent(new CustomEvent("itemclick", { bubbles: false, detail: { item, index: index2 + (container.fast ? container.state._listStartIndex || 0 : 0) } })); }; -}, Util.makeDom( - /*html*/ - ` - -
-
- -
\${item[this.grouplabelfield]}
-
-
-
-
- -
\${item[this.labelfield]}
-
-
-
-
-
-` -)); -Component.register("Tree", (container) => { - const defaultSets = { parentfield: "parent", groupicon: "folder", itemicon: "file" }; - container.state.__watch("list", () => { - Util.updateDefaults(container, defaultSets); - const childrenMap = {}; - (container.state.list || []).forEach((item) => { - var _a; - return (childrenMap[_a = item[container.parentfield] || ""] ?? (childrenMap[_a] = [])).push(item); - }); - const newList = []; - const traverse = (list, level) => list.forEach((item) => { - var _a; - const id = item[container.idfield]; - const hasChildren = !!((_a = childrenMap[id]) == null ? void 0 : _a.length); - newList.push({ ...item, _level: level, _hasChildren: hasChildren }); - if (hasChildren) traverse(childrenMap[id], level + 1); - }); - traverse(childrenMap[""] || [], 0); - return newList; - }); -}, Util.makeDom( - /*html*/ - ` - -
-
-
-
-
-
-
-
-` -)); -const FastTreeComponent = Component.register("FastTree", (container) => { - Component.getSetupFunction("List")(container); - Component.getSetupFunction("Tree")(container); - container.refresh = () => { - var _a; - return (_a = container.querySelector("FastList")) == null ? void 0 : _a.refresh(); + container.selectGroup = (item, index2) => { + if (container.hasAttribute("auto-select-group")) container.state.selectedGroup = container.state.selectedGroup === item[container.groupidfield] ? null : item[container.groupidfield]; + container.dispatchEvent(new CustomEvent("groupclick", { bubbles: false, detail: { item, index: index2 } })); }; + container.toggleCollapse = (item) => { + if (container.collapsible && item._hasChildren) { + container.collapsed[item[container.idfield]] = !container.collapsed[item[container.idfield]]; + updateFlatList(); + } + }; + updateFlatList(); }, Util.makeDom( /*html*/ ` - -
-
-
-
-
-
-
-
-` -)); -Component.register("CollapseTree", (container) => { - Component.attachSelectFeature(container); - const defaultSets = { idfield: "id", parentfield: "parent", labelfield: "label", summaryfield: "summary" }; - container.collapsed = NewState({}); - container.state.__watch("list", () => { - Util.updateDefaults(container, defaultSets); - const childrenMap = {}; - (container.state.list || []).forEach((item) => { - var _a; - return (childrenMap[_a = item[container.parentfield] || ""] ?? (childrenMap[_a] = [])).push(item); - }); - const newList = []; - const traverse = (list, level, parents) => list.forEach((item) => { - var _a; - const id = item[container.idfield]; - const hasChildren = !!((_a = childrenMap[id]) == null ? void 0 : _a.length); - newList.push({ ...item, _level: level, _hasChildren: hasChildren, _parents: parents }); - if (hasChildren) traverse(childrenMap[id], level + 1, [...parents, id]); - }); - traverse(childrenMap[""] || [], 0, []); - return newList; - }); -}, Util.makeDom( - /*html*/ - ` - -
-
- +
+
+ +
+
` )); Component.register("Nav", (container) => { @@ -818,8 +679,6 @@ if (typeof document !== "undefined") { export { APIComponent, AutoForm, - FastListComponent, - FastTreeComponent, HTTP, MouseMover, State, diff --git a/dist/base.min.js b/dist/base.min.js index b62e3f4..b338501 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 l={get:({url:t,...e})=>l.request({url:t,method:"GET",...e}),post:({url:t,data:e,...i})=>l.request({url:t,method:"POST",data:e,...i}),put:({url:t,data:e,...i})=>l.request({url:t,method:"PUT",data:e,...i}),delete:({url:t,...e})=>l.request({url:t,method:"DELETE",...e}),head:({url:t,...e})=>l.request({url:t,method:"HEAD",...e}),request:async({url:t,method:e="POST",data:i,headers:s={},responseType:n,timeout:a=1e4})=>{var l;const o={method:e=e.toUpperCase(),signal:null==(l=AbortSignal.timeout)?void 0:l.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")),o.body=i}Object.keys(s).length&&(o.headers=s);const r={error:null,ok:null,status:0,headers:{},responseType:"",result:null};try{const e=await fetch(t,o);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}},o=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,l.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=l.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,l=0,o=0,r=null,d=null,c=!1;return{reset:(i,m)=>{if(c=!1,t.clear(),e.clear(),n.clear(),o=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,l=parseFloat(p.rowGap)||0,i.slice(0,Math.min(30,u))},init:(i,a)=>{if(c)return;const l=i.length,o=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&&(o=r);const c=a.offsetHeight+r+d+l,m=t.get(i);if(c!==m){t.set(i,c),n.add(c);const a=c-(m||0),l=i-i%s;e.has(l)&&e.set(l,e.get(l)+a)}},calc:(i,r)=>{if(!c||!r)return null;const d=r.length,m=Math.ceil((i.clientHeight||100)/(n.get()||32));let u=a+o+l,p=0,h=0,v=0,f=0;for(let n=0;n{const e=u();t.state.renderedList=[],t.onItemUpdate=(i,s)=>{e.update(i+(t.state._listStartIndex||0),s)},t.refresh=()=>{const i=e.calc(t,t.state.list);i&&(t.state.prevHeight=i.prevHeight,t.state.postHeight=i.postHeight,t.state._listStartIndex=i.listStartIndex,t.state.renderedList=i.renderedList)},t.state.__watch("list",i=>{t.state._listStartIndex=0,t.state.renderedList=e.reset(i,t)||[],requestAnimationFrame(()=>{t.state.list===i&&e.init(i,t.refresh)})})},i.makeDom('\n
\n
\n
\n
\n
\n'));t.attachSelectFeature=(t,e="itemclick",i="selectItem",s="itemActiveTag",n="selectedItem",a="idfield",l="auto-select")=>{const o=l&&t.hasAttribute(l);t[i]=(i,s)=>{o&&(t.state[n]=t.state[n]===i[t[a]]?null:i[t[a]]),t.dispatchEvent(new CustomEvent(e,{bubbles:!1,detail:!o||t.state[n]?{item:i,index:s}:{item:{},index:null}}))},t[s]=e=>null!==t.state[n]&&t.state[n]===e[t[a]]?"active":""},t.register("List",e=>{t.attachSelectFeature(e);const s={idfield:"id",labelfield:"label",summaryfield:"summary"};e.state.__watch("list",t=>{i.updateDefaults(e,s)})},i.makeDom('\n
\n
\n \n
${item[this.labelfield]}
\n
\n
\n
\n
\n')),t.register("GroupedList",e=>{t.getSetupFunction("List")(e),e._selectItem=e.selectItem,e.selectItem=(t,i)=>{e.state.selectedGroup=null,e._selectItem(t,i)},t.attachSelectFeature(e,"groupclick","selectGroup","groupActiveTag","selectedGroup","groupidfield",e.hasAttribute("auto-select-group")||e.hasAttribute("auto-select")&&"group-selectable"),e._selectGroup=e.selectGroup,e.selectGroup=(t,i)=>{e.state.selectedItem=null,e._selectGroup(t,i)};const s={typefield:"type",groupidfield:"id",grouplabelfield:"label",groupsummaryfield:"summary",groupfield:"group"};e.state.__watch("list",t=>{i.updateDefaults(e,s);const n=[];return(e.state.groups||[]).forEach(i=>{n.push({[e.typefield]:"group",...i}),t&&t.forEach(t=>{t[e.groupfield]===i[e.groupidfield]&&n.push({[e.typefield]:"item",...t})})}),n})},i.makeDom('\n
\n \n
\n')),t.register("FastGroupedList",e=>{t.getSetupFunction("List")(e),t.getSetupFunction("GroupedList")(e),e.refresh=()=>{var t;return null==(t=e.querySelector("FastList"))?void 0:t.refresh()}},i.makeDom('\n\n
\n
\n \n
${item[this.grouplabelfield]}
\n
\n
\n
\n
\n \n
${item[this.labelfield]}
\n
\n
\n
\n
\n
\n')),t.register("Tree",t=>{const e={parentfield:"parent",groupicon:"folder",itemicon:"file"};t.state.__watch("list",()=>{i.updateDefaults(t,e);const s={};(t.state.list||[]).forEach(e=>{var i;return(s[i=e[t.parentfield]||""]??(s[i]=[])).push(e)});const n=[],a=(e,i)=>e.forEach(e=>{var l;const o=e[t.idfield],r=!!(null==(l=s[o])?void 0:l.length);n.push({...e,_level:i,_hasChildren:r}),r&&a(s[o],i+1)});return a(s[""]||[],0),n})},i.makeDom('\n\n\t
\n\t\t
\n\t\t
\n\t\t
\n\t\t
\n\t\t
\n\t
\n
\n'));const h=t.register("FastTree",e=>{t.getSetupFunction("List")(e),t.getSetupFunction("Tree")(e),e.refresh=()=>{var t;return null==(t=e.querySelector("FastList"))?void 0:t.refresh()}},i.makeDom('\n\n\t
\n\t\t
\n\t\t
\n\t\t
\n\t\t
\n\t\t
\n\t
\n
\n'));t.register("CollapseTree",s=>{t.attachSelectFeature(s);const n={idfield:"id",parentfield:"parent",labelfield:"label",summaryfield:"summary"};s.collapsed=e({}),s.state.__watch("list",()=>{i.updateDefaults(s,n);const t={};(s.state.list||[]).forEach(e=>{var i;return(t[i=e[s.parentfield]||""]??(t[i]=[])).push(e)});const e=[],a=(i,n,l)=>i.forEach(i=>{var o;const r=i[s.idfield],d=!!(null==(o=t[r])?void 0:o.length);e.push({...i,_level:n,_hasChildren:d,_parents:l}),d&&a(t[r],n+1,[...l,r])});return a(t[""]||[],0,[]),e})},i.makeDom('\n\n\t
\n\t\t
\n\t\t\t\n\t\t
\n\t\t
\n\t\t
\n\t\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 v=!1,f={},b={};const g={start:(t,{onmousemove:e,onmouseup:i})=>{f={x:t.clientX,y:t.clientY,w:0,h:0},b={onmousemove:e,onmouseup:i},v=!0}};"undefined"!=typeof document&&(document.addEventListener("mouseup",t=>{var e;v&&(v=!1,null==(e=b.onmouseup)||e.call(b,{event:t,...f}))}),document.addEventListener("mousemove",t=>{var e;v&&(f.w=t.clientX-f.x,f.h=t.clientY-f.y,null==(e=b.onmousemove)||e.call(b,{event:t,...f}))})),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 l=s+(t.isVertical?a:n);return li?i:l};t.addEventListener("mousedown",e=>{const i=t.isVertical?s.offsetHeight:s.offsetWidth;g.start(e,{onmousemove:({w:e,h:a})=>{const l=n(i,e,a);s.style[t.isVertical?"height":"width"]=l+"px",t.dispatchEvent(new CustomEvent("resizing",{detail:{oldSize:i,newSize:l},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 x=e({exitBlocks:0});globalThis.State=x,"undefined"!=typeof window&&window.addEventListener("beforeunload",t=>{x.exitBlocks>0&&t.preventDefault()});const $=document.documentElement;if($.hasAttribute("$data-bs-theme")||$.hasAttribute("data-bs-theme")||$.setAttribute("$data-bs-theme","LocalStorage.darkMode?'dark':'light'"),globalThis.HTTP=l,globalThis.UI=r,globalThis.AutoForm=m,globalThis.MouseMover=g,"undefined"!=typeof document){const t=()=>a(document.documentElement);"loading"!==document.readyState?t():document.addEventListener("DOMContentLoaded",t,!0)}export{o as APIComponent,m as AutoForm,p as FastListComponent,h as FastTreeComponent,l as HTTP,g as MouseMover,x 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;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=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,m=Math.ceil((i.clientHeight||100)/(n.get()||32));let u=a+l+o,p=0,h=0,b=0,f=0;for(let n=0;n{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)||[],requestAnimationFrame(()=>{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}; diff --git a/src/list.js b/src/list.js index e1aee30..9d0e68d 100644 --- a/src/list.js +++ b/src/list.js @@ -5,334 +5,166 @@ export const VirtualScroll = () => { const groupHeights = new Map() let groupItemCount = 1 const avg = Util.newAvg() - - let padTop = 0 - let rowGap = 0 - let topMargin = 0 - let itemMarginTop = null - let itemMarginBottom = null - let listInited = false + let padTop = 0, rowGap = 0, topMargin = 0, itemMarginTop = null, itemMarginBottom = null, listInited = false return { - // 核心1:重置与首屏预渲染 reset: (list, container) => { - listInited = false - itemHeights.clear() - groupHeights.clear() - avg.clear() - topMargin = 0 - itemMarginTop = null - itemMarginBottom = null - - if (!list?.length) return [] - - const size = list.length - groupItemCount = Math.ceil(Math.sqrt(size)) || 10 - - const style = window.getComputedStyle(container) - padTop = parseFloat(style.paddingTop) || 0 - rowGap = parseFloat(style.rowGap) || 0 - - return list.slice(0, Math.min(30, size)) + listInited = false; itemHeights.clear(); groupHeights.clear(); avg.clear(); topMargin = 0; itemMarginTop = null; itemMarginBottom = null; + if (!list?.length) return []; + const size = list.length; groupItemCount = Math.ceil(Math.sqrt(size)) || 10; + const style = window.getComputedStyle(container); + padTop = parseFloat(style.paddingTop) || 0; rowGap = parseFloat(style.rowGap) || 0; + return list.slice(0, Math.min(30, size)); }, - - // 核心2:无痛初始化缓存 (通过 rAF 调用) init: (list, refreshCallback) => { - if (listInited) return - const size = list.length - const defaultHeight = avg.get() || 32 - - for (let i = 0; i < size; i++) { - if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight) - } + 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); for (let i = 0; i < size; i += groupItemCount) { - let sum = 0 - for (let j = i; j < Math.min(i + groupItemCount, size); j++) { - sum += itemHeights.get(j) - } - groupHeights.set(i, sum) + let sum = 0; + for (let j = i; j < Math.min(i + groupItemCount, size); j++) sum += itemHeights.get(j); + groupHeights.set(i, sum); } - listInited = true - refreshCallback() + listInited = true; refreshCallback(); }, - - // 核心3:原汁原味的极速高度更新 update: (absoluteIndex, node) => { if (itemMarginTop === null) { - const style = window.getComputedStyle(node) - itemMarginTop = parseFloat(style.marginTop) || 0 - itemMarginBottom = parseFloat(style.marginBottom) || 0 + const style = window.getComputedStyle(node); + itemMarginTop = parseFloat(style.marginTop) || 0; itemMarginBottom = parseFloat(style.marginBottom) || 0; } - if (absoluteIndex === 0) topMargin = itemMarginTop - - const newHeight = node.offsetHeight + itemMarginTop + itemMarginBottom + rowGap - const oldHeight = itemHeights.get(absoluteIndex) - + if (absoluteIndex === 0) topMargin = itemMarginTop; + const newHeight = node.offsetHeight + itemMarginTop + itemMarginBottom + rowGap; + const oldHeight = itemHeights.get(absoluteIndex); if (newHeight !== oldHeight) { - itemHeights.set(absoluteIndex, newHeight) - avg.add(newHeight) - const offset = newHeight - (oldHeight || 0) - const groupIndex = absoluteIndex - (absoluteIndex % groupItemCount) - if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset) + itemHeights.set(absoluteIndex, newHeight); avg.add(newHeight); + const offset = newHeight - (oldHeight || 0), groupIndex = absoluteIndex - (absoluteIndex % groupItemCount); + if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset); } }, - - // 核心4:原汁原味的状态机滚动计算 calc: (container, list) => { - if (!listInited || !list) return null - - const size = list.length - const visibleCount = Math.ceil((container.clientHeight || 100) / (avg.get() || 32)) - - let prev = padTop + topMargin + rowGap - let post = 0 - let status = 0 - let listStartIndex = 0 - let listEndIndex = 0 - + if (!listInited || !list) return null; + const size = list.length, visibleCount = Math.ceil((container.clientHeight || 100) / (avg.get() || 32)); + let prev = padTop + topMargin + rowGap, post = 0, status = 0, listStartIndex = 0, listEndIndex = 0; for (let i = 0; i < size; i++) { if (status === 0) { - const gh = groupHeights.get(i) - if (gh && prev + gh < container.scrollTop) { - prev += gh - i += Math.min(groupItemCount, size - i) - 1 - } else { - const ih = itemHeights.get(i) - if (prev + ih < container.scrollTop) { - prev += ih - } else { - status = 1 - let visibleStartIndex = Math.max(0, i) - listStartIndex = Math.max(0, visibleStartIndex - visibleCount) - listEndIndex = Math.min(listStartIndex + visibleCount * 3, size) - i = listEndIndex - 1 - for (let j = listStartIndex; j < visibleStartIndex; j++) prev -= itemHeights.get(j) + const gh = groupHeights.get(i); + if (gh && prev + gh < container.scrollTop) { prev += gh; i += Math.min(groupItemCount, size - i) - 1; } + else { + const ih = itemHeights.get(i); + if (prev + ih < container.scrollTop) prev += ih; + else { + status = 1; let visibleStartIndex = Math.max(0, i); + listStartIndex = Math.max(0, visibleStartIndex - visibleCount); + listEndIndex = Math.min(listStartIndex + visibleCount * 3, size); + i = listEndIndex - 1; for (let j = listStartIndex; j < visibleStartIndex; j++) prev -= itemHeights.get(j); } } } else if (status === 1) { - const gh = groupHeights.get(i) - if (gh) { - post += gh - i += groupItemCount - 1 - } else { - post += itemHeights.get(i) - } + const gh = groupHeights.get(i); + if (gh) { post += gh; i += groupItemCount - 1; } else post += itemHeights.get(i); } } - - return { - prevHeight: prev - padTop - topMargin - rowGap, - postHeight: post, - renderedList: list.slice(listStartIndex, listEndIndex), - listStartIndex - } + return { prevHeight: Math.max(0, prev - padTop - topMargin - rowGap), postHeight: post, renderedList: list.slice(listStartIndex, listEndIndex), listStartIndex }; } } } -export const FastListComponent = Component.register('FastList', container => { - const vs = VirtualScroll() - container.state.renderedList = [] - - container.onItemUpdate = (index, node) => { - vs.update(index + (container.state._listStartIndex || 0), node) +Component.register('List', container => { + container.mode = container.getAttribute('mode') || 'normal' + container.fast = container.hasAttribute('fast') + container.collapsible = container.hasAttribute('collapsible') + + const defaultSets = { + idfield: 'id', labelfield: 'label', summaryfield: 'summary', + groupidfield: 'id', grouplabelfield: 'label', groupsummaryfield: 'summary', groupfield: 'group', + parentfield: 'parent', groupicon: 'folder', itemicon: 'file' } + container.collapsed = NewState({}) + + const updateFlatList = () => { + Util.updateDefaults(container, defaultSets) + const list = container.state.list || [], flatList = [] + if (container.mode === 'group') { + const itemMap = {} + list.forEach(item => (itemMap[item[container.groupfield]] ??= []).push(item)); + (container.state.groups || []).forEach(group => { + flatList.push({ type: 'group', ...group }) + const items = itemMap[group[container.groupidfield]] + if (items) items.forEach(item => flatList.push({ type: 'item', ...item })) + }) + } else if (container.mode === 'tree') { + const childrenMap = {} + list.forEach(item => (childrenMap[item[container.parentfield] || ''] ??= []).push(item)) + const traverse = (items, level, parents) => items.forEach(item => { + const id = item[container.idfield], hasChildren = !!childrenMap[id]?.length + const isCollapsed = container.collapsed[id] + flatList.push({ type: 'item', ...item, _level: level, _hasChildren: hasChildren, _parents: parents }) + if (hasChildren && !isCollapsed) traverse(childrenMap[id], level + 1, [...parents, id]) + }) + traverse(childrenMap[''] || [], 0, []) + } else list.forEach(item => flatList.push({ type: 'item', ...item })) + container.state._flatList = flatList + } + + container.state.__watch('list', updateFlatList) + const vs = container.fast ? VirtualScroll() : null + container.state._renderedList = [] container.refresh = () => { - const res = vs.calc(container, container.state.list) + if (!container.fast) return + const res = vs.calc(container, container.state._flatList) if (res) { container.state.prevHeight = res.prevHeight container.state.postHeight = res.postHeight container.state._listStartIndex = res.listStartIndex - container.state.renderedList = res.renderedList + container.state._renderedList = res.renderedList } } - container.state.__watch('list', list => { - container.state._listStartIndex = 0 - container.state.renderedList = vs.reset(list, container) || [] + container.onItemUpdate = (index, node) => { if (container.fast) vs.update(index + (container.state._listStartIndex || 0), node) } - requestAnimationFrame(() => { - if (container.state.list !== list) return - vs.init(list, container.refresh) - }) + container.state.__watch('_flatList', flatList => { + if (container.fast) { + container.state._listStartIndex = 0 + container.state._renderedList = vs.reset(flatList, container) || [] + requestAnimationFrame(() => { if (container.state._flatList === flatList) vs.init(flatList, container.refresh) }) + } else container.state._renderedList = flatList }) -}, Util.makeDom(/*html*/` -
-
-
-
-
-`)) - -Component.attachSelectFeature = (container, eventName = 'itemclick', selectActionName = 'selectItem', activeActionName = 'itemActiveTag', selectedItemKey = 'selectedItem', idfieldKey = 'idfield', selectableAttr = 'auto-select') => { - const selectable = selectableAttr && container.hasAttribute(selectableAttr) - container[selectActionName] = (item, index) => { - if (selectable) container.state[selectedItemKey] = container.state[selectedItemKey] === item[container[idfieldKey]] ? null : item[container[idfieldKey]] - container.dispatchEvent(new CustomEvent(eventName, { bubbles: false, detail: !selectable || container.state[selectedItemKey] ? { item, index } : { item: {}, index: null } })) - } - container[activeActionName] = (item) => container.state[selectedItemKey] !== null && container.state[selectedItemKey] === item[container[idfieldKey]] ? 'active' : '' -} - -Component.register('List', container => { - Component.attachSelectFeature(container) - const defaultSets = { idfield: 'id', labelfield: 'label', summaryfield: 'summary' } - container.state.__watch('list', oldList => { - Util.updateDefaults(container, defaultSets) - }) -}, Util.makeDom(/*html*/` -
-
- -
\${item[this.labelfield]}
-
-
-
-
-`)) - -Component.register('GroupedList', container => { - Component.getSetupFunction('List')(container) - container._selectItem = container.selectItem container.selectItem = (item, index) => { - container.state.selectedGroup = null - container._selectItem(item, index) + if (container.hasAttribute('auto-select')) container.state.selectedItem = container.state.selectedItem === item[container.idfield] ? null : item[container.idfield] + container.dispatchEvent(new CustomEvent('itemclick', { bubbles: false, detail: { item, index: index + (container.fast ? (container.state._listStartIndex || 0) : 0) } })) } - Component.attachSelectFeature(container, 'groupclick', 'selectGroup', 'groupActiveTag', 'selectedGroup', 'groupidfield', container.hasAttribute('auto-select-group') || container.hasAttribute('auto-select') && 'group-selectable') - container._selectGroup = container.selectGroup container.selectGroup = (item, index) => { - container.state.selectedItem = null - container._selectGroup(item, index) + if (container.hasAttribute('auto-select-group')) container.state.selectedGroup = container.state.selectedGroup === item[container.groupidfield] ? null : item[container.groupidfield] + container.dispatchEvent(new CustomEvent('groupclick', { bubbles: false, detail: { item, index } })) } + container.toggleCollapse = (item) => { if (container.collapsible && item._hasChildren) { container.collapsed[item[container.idfield]] = !container.collapsed[item[container.idfield]]; updateFlatList(); } } - const defaultSets = { typefield: 'type', groupidfield: 'id', grouplabelfield: 'label', groupsummaryfield: 'summary', groupfield: 'group' } - container.state.__watch('list', oldList => { - Util.updateDefaults(container, defaultSets) - const newList = []; - (container.state.groups || []).forEach(group => { - newList.push({ [container.typefield]: 'group', ...group }) - if (oldList) oldList.forEach(item => { - if (item[container.groupfield] === group[container.groupidfield]) { - newList.push({ [container.typefield]: 'item', ...item }) - } - }) - }) - return newList - }) + updateFlatList() }, Util.makeDom(/*html*/` -
- +
+
+ +
`)) - -Component.register('FastGroupedList', container => { - Component.getSetupFunction('List')(container) - Component.getSetupFunction('GroupedList')(container) - container.refresh = () => container.querySelector('FastList')?.refresh() -}, Util.makeDom(/*html*/` - -
-
- -
\${item[this.grouplabelfield]}
-
-
-
-
- -
\${item[this.labelfield]}
-
-
-
-
-
-`)) - -Component.register('Tree', container => { - const defaultSets = { parentfield: 'parent', groupicon: 'folder', itemicon: 'file' } - container.state.__watch('list', () => { - Util.updateDefaults(container, defaultSets) - const childrenMap = {}; - (container.state.list || []).forEach(item => (childrenMap[item[container.parentfield] || ''] ??= []).push(item)) - const newList = [] - const traverse = (list, level) => list.forEach(item => { - const id = item[container.idfield] - const hasChildren = !!childrenMap[id]?.length - newList.push({ ...item, _level: level, _hasChildren: hasChildren }) - if (hasChildren) traverse(childrenMap[id], level + 1) - }) - traverse(childrenMap[''] || [], 0) - return newList - }) -}, Util.makeDom(/*html*/` - -
-
-
-
-
-
-
-
-`)) - -export const FastTreeComponent = Component.register('FastTree', container => { - Component.getSetupFunction('List')(container) - Component.getSetupFunction('Tree')(container) - container.refresh = () => container.querySelector('FastList')?.refresh() -}, Util.makeDom(/*html*/` - -
-
-
-
-
-
-
-
-`)) - -Component.register('CollapseTree', container => { - Component.attachSelectFeature(container) - const defaultSets = { idfield: 'id', parentfield: 'parent', labelfield: 'label', summaryfield: 'summary' } - container.collapsed = NewState({}) - container.state.__watch('list', () => { - Util.updateDefaults(container, defaultSets) - const childrenMap = {}; - (container.state.list || []).forEach(item => (childrenMap[item[container.parentfield] || ''] ??= []).push(item)) - const newList = [] - const traverse = (list, level, parents) => list.forEach(item => { - const id = item[container.idfield] - const hasChildren = !!childrenMap[id]?.length - newList.push({ ...item, _level: level, _hasChildren: hasChildren, _parents: parents }) - if (hasChildren) traverse(childrenMap[id], level + 1, [...parents, id]) - }) - traverse(childrenMap[''] || [], 0, []) - return newList - }) -}, Util.makeDom(/*html*/` - -
-
- -
-
-
-
-
-
-`)) diff --git a/test/base.test.js b/test/base.test.js index fe3c00b..5f2df60 100644 --- a/test/base.test.js +++ b/test/base.test.js @@ -67,6 +67,7 @@ export async function runTests() { const time = performance.now() - start; window.benchResults[name] = time; console.log(`BENCHMARK: ${name} scroll & refresh: ${time.toFixed(2)}ms`); + console.log(`DEBUG [${id}]: _renderedList len: ${el.state._renderedList?.length}, _flatList len: ${el.state._flatList?.length}`); }; await measure('ll', 'FastList'); diff --git a/test/index.html b/test/index.html index dce1be6..0c079d0 100644 --- a/test/index.html +++ b/test/index.html @@ -28,17 +28,17 @@ }
- + - - + - - - - + + + +