diff --git a/README.md b/README.md index 37bca93..429d009 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,14 @@ A mandatory attribute `name` is used by the `` component to construct ``` +### `nohover` + +By default, items in the `` component grab focus and get highlighted when pointer hovers over them, similarly to options in the `` with the `multiple` attribute specified). + +```html + +``` + ## Instance properties The `CbxTree` interface also inherits properties from its parent, [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). @@ -212,6 +220,10 @@ console.log('Checked values:', readingList.formData.getAll('reading-list[]')); Reflects the value of the element’s [`name` attribute](#name). +### `CbxTree.noHover` + +Reflects the value of the element’s [`nohover` attribute](#nohover). + ### `CbxTree.subtreeProvider` The `subtreeProvider` property is used in cases where on-demand subtree loading is required. If your initial tree doesn’t contain data for some nested subtrees, you may define your custom function for subtree generation/fetching which will be called when the user expands the target item for the first time. @@ -317,8 +329,8 @@ The `` element provides a few CSS custom properties (variables) that y | `--cbx-tree-toggle-closed-mask` | ``¹ | Mask image for the toggle button in the collapsed state | | `--cbx-tree-toggle-open-mask` | `` | Mask image for the toggle button in the expanded state | | `--cbx-tree-toggle-pending-mask` | `` | Mask image for the toggle button in the pending state | -| `--cbx-tree-label-hover-bg` | ``² | Background color for the highlighted item’s label | -| `--cbx-tree-label-hover-fg` | `` | Text color for the highlighted item’s label | +| `--cbx-tree-label-focus-bg` | ``² | Background color for the highlighted item’s label | +| `--cbx-tree-label-focus-fg` | `` | Text color for the highlighted item’s label | | `--cbx-tree-nesting-indent` | ``³ | Indentation size for nested subtrees | ¹ https://developer.mozilla.org/en-US/docs/Web/CSS/url_value \ diff --git a/dist/cbx-tree.d.mts b/dist/cbx-tree.d.mts index b5c42a4..2641bf2 100644 --- a/dist/cbx-tree.d.mts +++ b/dist/cbx-tree.d.mts @@ -11,6 +11,7 @@ export default class CbxTree extends HTMLElement { #private; static get formAssociated(): true; + static get observedAttributes(): string[]; subtreeProvider: ((parentValue: string) => Promise) | null; get formData(): FormData; get form(): HTMLFormElement | null; @@ -18,10 +19,13 @@ export default class CbxTree extends HTMLElement { set name(value: string); get disabled(): boolean; set disabled(value: boolean); + get noHover(): boolean; + set noHover(value: boolean); get type(): string; constructor(); + attributeChangedCallback(name: string): void; formDisabledCallback(disabled: boolean): void; formResetCallback(): void; formStateRestoreCallback(state: string, mode: string): void; diff --git a/dist/cbx-tree.mjs b/dist/cbx-tree.mjs index dc53a89..af1f8a7 100644 --- a/dist/cbx-tree.mjs +++ b/dist/cbx-tree.mjs @@ -1,6 +1,6 @@ /*! -cbx-tree v1.1.1 +cbx-tree v2.0.0 https://amphiluke.github.io/cbx-tree/ (c) 2025 Amphiluke */ -const e=(e,i=!0)=>`\n
    \n ${[...e.values()].reduce((e,i)=>e+t(i),"")}\n
`,t=({id:t,title:r,icon:a,collapsed:s,children:n})=>`\n
  • \n ${void 0!==n?'':""}\n \n ${n?.size>0?e(n,!1):""}\n
  • `,i=e=>e?e.startsWith("")?'e.replaceAll(t,`&#${t.charCodeAt(0)};`),e))(e)}" alt="" part="icon">`:"",r=e=>e?.slice(e.indexOf("_")+1),a=e=>{if(!Array.isArray(e))throw new TypeError("Tree data must be an array of tree items")},s=new CSSStyleSheet;s.replaceSync(':host{--cbx-tree-toggle-closed-mask: url(\'data:image/svg+xml,\');--cbx-tree-toggle-open-mask: url(\'data:image/svg+xml,\');--cbx-tree-toggle-pending-mask: url(\'data:image/svg+xml,\');--cbx-tree-label-hover-bg: SelectedItem;--cbx-tree-label-hover-fg: SelectedItemText;--cbx-tree-nesting-indent: 1em}:host(:dir(rtl)){--cbx-tree-toggle-closed-mask: url(\'data:image/svg+xml,\')}:host(:not([hidden])){display:block}[part=tree]{list-style:none;margin:0;padding:0;&:has([inert]){cursor:progress}&:not([part=tree] [part=tree]){overflow-x:clip}}[part=item]{align-items:center;display:grid;gap:0 .6ch;grid-template-areas:"toggle label" "tree tree";grid-template-columns:max(1em,16px) 1fr;&[aria-expanded=false]>[part=tree]{display:none}}[part=toggle]{background:none;border:none;color:inherit;font:inherit;grid-area:toggle;height:max(1em,16px);padding:0;position:relative;width:max(1em,16px);z-index:1;&:not(:disabled){cursor:pointer}&:before{background:currentColor;content:"";inset:-4px;mask:var(--cbx-tree-toggle-closed-mask) 50% 50% / contain no-repeat content-box;padding:4px;position:absolute}[aria-expanded=true]>&:before{mask-image:var(--cbx-tree-toggle-open-mask)}[inert]>&:before{mask-image:var(--cbx-tree-toggle-pending-mask)}&:where(:hover,:focus-visible):before,&:has(+[part=label]:hover):before,[part=item]:has(>[part=label] :focus-visible)>&:before{color:var(--cbx-tree-label-hover-fg)}}[part=label]{align-items:inherit;display:flex;gap:inherit;grid-area:label;isolation:isolate;padding-block:.2em;position:relative;&:hover,&:has(:focus-visible),[part=toggle]:where(:hover,:focus-visible)+&{color:var(--cbx-tree-label-hover-fg);&:before{background:var(--cbx-tree-label-hover-bg)}}&:before{content:"";inset:0;inset-inline-start:-100vw;position:absolute;z-index:-1}}:where([part=item]) [part=tree]{grid-area:tree;padding-inline-start:var(--cbx-tree-nesting-indent)}');class n extends HTMLElement{static get formAssociated(){return!0}#e;#t;#i=new Map;#r=new Set;subtreeProvider=null;get formData(){const e=new FormData,{name:t}=this;return this.#r.forEach(i=>{const r=this.#a(i)?.value;void 0!==r&&e.append(t,r)}),e}get form(){return this.#t.form}get name(){return this.getAttribute("name")}set name(e){this.setAttribute("name",e)}get disabled(){return this.hasAttribute("disabled")}set disabled(e){e?this.setAttribute("disabled",""):this.removeAttribute("disabled")}get type(){return this.localName}constructor(){super(),this.#e=this.attachShadow({mode:"open"}),this.#e.adoptedStyleSheets=[s],this.#t=this.attachInternals(),this.setData(this.#s()),this.hasAttribute("tabindex")||(this.tabIndex=0),this.#e.addEventListener("change",e=>this.#n(e)),this.#e.addEventListener("click",e=>this.#l(e))}formDisabledCallback(e){this.#o(e)}formResetCallback(){this.setData(this.#s())}formStateRestoreCallback(e,t){if("restore"===t)try{this.setData(JSON.parse(e))}catch(e){console.warn("Failed to restore the tree state",e)}}#n({target:e}){if(!e.part.contains("checkbox"))return;const t=r(e.id),i=e.checked?"add":"delete";this.#r[i](t);const a=this.#a(t);this.#d(a),this.#h(a),this.#c(),this.dispatchEvent(new CustomEvent("cbxtreechange",{bubbles:!0,detail:this.formData}))}#l({target:e}){if(!e.part.contains("toggle"))return;const t=e.closest('[part="item"]'),i="true"!==t.ariaExpanded;t.ariaExpanded=i?"true":"false";const a=r(t.id);i&&this.#g(a);const s=this.#a(a);s.collapsed=!i,this.#c(),this.dispatchEvent(new CustomEvent("cbxtreetoggle",{bubbles:!0,detail:{title:s.title,value:s.value,newState:i?"expanded":"collapsed"}}))}#p(){this.#e.setHTMLUnsafe(e(this.#i)),[...this.#e.querySelectorAll('[part="checkbox"]')].forEach(e=>{const t=this.#a(r(e.id))?.state;e.checked="checked"===t,e.indeterminate="indeterminate"===t})}#u(e,t){return new Map(e.map((e,i)=>{const r=t?`${t}:${i}`:String(i);e.checked&&this.#r.add(r);const a={id:r,title:e.title,value:e.value,icon:e.icon,collapsed:e.children?.length?!!e.collapsed:null===e.children||void 0,children:e.children?this.#u(e.children,r):e.children};return Object.defineProperty(a,"state",{get:()=>this.#r.has(a.id)?"checked":a.children?.size?this.#b(a):"unchecked"}),[r,a]}))}async#g(t){if("function"!=typeof this.subtreeProvider)return;const i=this.#a(t);if(null!==i?.children)return;const r=this.#e.getElementById(`item_${t}`);r.inert=!0;try{const e=await this.subtreeProvider(i.value);a(e),i.children=this.#u(e,i.id)}finally{r.inert=!1}i.children.size&&(r.insertAdjacentHTML("beforeend",e(i.children,!1)),this.disabled&&this.#o(!0,r),this.#d(i),this.#c())}#a(e){const t=e.split(":");return t.slice(1).reduce((e,t)=>e?.children?.get(`${e?.id}:${t}`),this.#i.get(t[0]))}#b(e){const t=new Set([...e.children.values()].map(({state:e})=>e));return t.has("indeterminate")?"indeterminate":t.has("checked")?t.has("unchecked")?"indeterminate":"checked":"unchecked"}#m(e,t){if(!t?.size)return;const i=e?"add":"delete";t.forEach((t,r)=>{this.#r[i](r);const a=this.#e.getElementById(`cbx_${r}`);a.checked=e,a.indeterminate=!1,this.#m(e,t.children)})}#o(e,t=this.#e){[...t.querySelectorAll("button, input")].forEach(t=>t.disabled=e)}#d(e){e.children&&this.#m(this.#r.has(e.id),e.children)}#h(e){if(this.#i.has(e.id))return;const t=this.#a(e.id.slice(0,e.id.lastIndexOf(":"))),i=this.#b(t);this.#r["checked"===i?"add":"delete"](t.id);const r=this.#e.getElementById(`cbx_${t.id}`);r.checked="checked"===i,r.indeterminate="indeterminate"===i,this.#h(t)}#c(){this.#t.setFormValue(this.formData,JSON.stringify(this))}#s(){const e=this.textContent.trim()||"[]";try{const t=JSON.parse(e);return a(t),t}catch{return console.error(new DOMException(" contents must be a valid JSON array representation","DataError")),[]}}#v(e=this.#i){return[...e.values()].map(e=>({title:e.title,value:e.value,icon:e.icon,checked:this.#r.has(e.id),collapsed:!0===e.collapsed||void 0,children:e.children?this.#v(e.children):e.children}))}setData(e){a(e),this.#r.clear(),this.#i=this.#u(e),this.#p(),this.#c()}toJSON(){return this.#v()}toggleChecked(e){void 0===e&&(e=!!this.#e.querySelector('[part="checkbox"]:not(:checked)')),this.#m(e,this.#i),this.#c()}toggle(e){let t=[...this.#e.querySelectorAll('[part="item"]:has([part="tree"])')];void 0===e&&(e=t.some(({ariaExpanded:e})=>"false"===e));const i=String(e);t.forEach(t=>{if(t.ariaExpanded===i)return;t.ariaExpanded=i;this.#a(r(t.id)).collapsed=!e}),this.#c()}get validity(){return this.#t.validity}get validationMessage(){return this.#t.validationMessage}get willValidate(){return this.#t.willValidate}checkValidity(){return this.#t.checkValidity()}reportValidity(){return this.#t.reportValidity()}setValidity(...e){return this.#t.setValidity(...e)}}customElements.define("cbx-tree",n);export{n as default}; \ No newline at end of file +class e{static assertRawTreeValid(e){if(!Array.isArray(e))throw new TypeError("Tree data must be an array of tree items")}#e=new Map;get tree(){return this.#e}selection=new Set;constructor(e){this.#e=this.#t(e)}#t(e,t){return new Map(e.map((e,i)=>{const s=t?`${t}:${i}`:String(i);e.checked&&this.selection.add(s);const r={id:s,title:e.title,value:e.value,icon:e.icon,collapsed:e.children?.length?!!e.collapsed:null===e.children||void 0,children:e.children?this.#t(e.children,s):e.children};return Object.defineProperty(r,"state",{get:()=>this.selection.has(r.id)?"checked":r.children?.size?this.calcItemState(r):"unchecked"}),[s,r]}))}getItem(e){const[t,...i]=e.split(":");return i.reduce((e,t)=>e?.children?.get(`${e?.id}:${t}`),this.#e.get(t))}getParentItem(e){return this.getItem(e.slice(0,e.lastIndexOf(":")))}calcItemState(e){const t=new Set([...e.children.values()].map(({state:e})=>e));return t.has("indeterminate")?"indeterminate":t.has("checked")?t.has("unchecked")?"indeterminate":"checked":"unchecked"}setSubtree(e,t){e.children=this.#t(t,e.id)}toRaw(e=this.#e){return[...e.values()].map(e=>({title:e.title,value:e.value,icon:e.icon,checked:this.selection.has(e.id),collapsed:!0===e.collapsed||void 0,children:e.children?this.toRaw(e.children):e.children}))}}const t=(e,t=!0)=>`\n
      \n ${[...e.values()].reduce((e,t)=>e+i(t),"")}\n
    `,i=({id:e,title:i,icon:r,collapsed:a,children:n})=>`\n
  • \n ${void 0!==n?'':""}\n \n ${n?.size>0?t(n,!1):""}\n
  • `,s=e=>e?e.startsWith("")?'e.replaceAll(t,`&#${t.charCodeAt(0)};`),e))(e)}" alt="" part="icon">`:"",r=e=>e?.slice(e.indexOf("_")+1),a=new CSSStyleSheet;a.replaceSync(':host{--cbx-tree-toggle-closed-mask: url(\'data:image/svg+xml,\');--cbx-tree-toggle-open-mask: url(\'data:image/svg+xml,\');--cbx-tree-toggle-pending-mask: url(\'data:image/svg+xml,\');--cbx-tree-label-focus-bg: SelectedItem;--cbx-tree-label-focus-fg: SelectedItemText;--cbx-tree-nesting-indent: 1em}:host(:dir(rtl)){--cbx-tree-toggle-closed-mask: url(\'data:image/svg+xml,\')}:host(:not([hidden])){display:block}[part=tree]{list-style:none;margin:0;padding:0;&:has([inert]){cursor:progress}&:not([part=tree] [part=tree]){overflow-x:clip}}[part=item]{align-items:center;display:grid;gap:0 .6ch;grid-template-areas:"toggle label" "tree tree";grid-template-columns:max(1em,16px) 1fr;&[aria-expanded=false]>[part=tree]{display:none}}[part=toggle]{background:none;border:none;color:inherit;font:inherit;grid-area:toggle;height:max(1em,16px);padding:0;position:relative;width:max(1em,16px);z-index:1;&:not(:disabled){cursor:pointer}&:before{background:currentColor;content:"";inset:-4px;mask:var(--cbx-tree-toggle-closed-mask) 50% 50% / contain no-repeat content-box;padding:4px;position:absolute}[aria-expanded=true]>&:before{mask-image:var(--cbx-tree-toggle-open-mask)}[inert]>&:before{mask-image:var(--cbx-tree-toggle-pending-mask)}&:has(+[part=label]:focus):before{color:var(--cbx-tree-label-focus-fg)}}[part=label]{align-items:inherit;display:flex;gap:inherit;grid-area:label;isolation:isolate;outline:none;padding-block:.2em;position:relative;&:focus{color:var(--cbx-tree-label-focus-fg);&:before{background:var(--cbx-tree-label-focus-bg)}}&:before{content:"";inset:0;inset-inline-start:-100vw;position:absolute;z-index:-1}}:where([part=item]) [part=tree]{grid-area:tree;padding-inline-start:var(--cbx-tree-nesting-indent)}');class n extends HTMLElement{static get formAssociated(){return!0}static get observedAttributes(){return["nohover"]}#e;#t;#i;#s=null;get#r(){return this.#e.querySelector('[tabindex="0"]')}set#r(e){const t=this.#r;e!==t&&(t?.removeAttribute("tabindex"),e?.setAttribute("tabindex","0"))}get#a(){return[...this.#e.querySelectorAll('[part="label"]:not([aria-expanded="false"] [part="tree"] *)')]}subtreeProvider=null;get formData(){const e=new FormData,{name:t}=this;return this.#i.selection.forEach(i=>{const s=this.#i.getItem(i)?.value;void 0!==s&&e.append(t,s)}),e}get form(){return this.#t.form}get name(){return this.getAttribute("name")}set name(e){this.setAttribute("name",e)}get disabled(){return this.hasAttribute("disabled")}set disabled(e){e?this.setAttribute("disabled",""):this.removeAttribute("disabled")}get noHover(){return this.hasAttribute("nohover")}set noHover(e){e?this.setAttribute("nohover",""):this.removeAttribute("nohover")}get type(){return this.localName}constructor(){super(),this.#e=this.attachShadow({mode:"open"}),this.#e.adoptedStyleSheets=[a],this.#t=this.attachInternals(),this.setData(this.#n()),this.hasAttribute("tabindex")||(this.tabIndex=0),this.#e.addEventListener("change",e=>this.#o(e)),this.#e.addEventListener("pointerdown",e=>this.#l(e)),this.addEventListener("focus",()=>this.#h()),this.#e.addEventListener("keydown",e=>this.#c(e)),this.#d()}attributeChangedCallback(e){"nohover"===e&&this.#d()}formDisabledCallback(e){this.#g(e)}formResetCallback(){this.setData(this.#n())}formStateRestoreCallback(e,t){if("restore"===t)try{this.setData(JSON.parse(e))}catch(e){console.warn("Failed to restore the tree state",e)}}#o({target:e}){if(e.part.contains("checkbox")){this.#u(e);const t=e.closest('[part="label"]');return void this.#p(t,!0)}}#l(e){e.isPrimary&&e.target.part.contains("toggle")&&(this.#b(e.target.closest('[part="item"]')),e.preventDefault())}#h(){this.#r?.focus()}#c(e){if(!e.defaultPrevented&&!this.disabled){switch(e.key){case"ArrowRight":{const e=this.#r?.closest('[part="item"]');"true"===e?.ariaExpanded?this.#m():"false"===e?.ariaExpanded&&this.#b(e);break}case"ArrowLeft":{const e=this.#r?.closest('[part="item"]');"true"===e?.ariaExpanded?this.#b(e):this.#v();break}case"ArrowDown":this.#m();break;case"ArrowUp":this.#f();break;case"PageDown":this.#x();break;case"PageUp":this.#w();break;case"Home":this.#k();break;case"End":this.#y();break;case"Enter":{const e=this.#r?.closest('[part="item"]');"undefined"!==e.ariaExpanded&&this.#b(e);break}case" ":{const e=this.#r?.querySelector('[part="checkbox"]');e&&(e.checked=!e.checked,e.indeterminate=!1,this.#u(e));break}default:return}e.preventDefault()}}#S({target:e}){const t=e.part.contains("toggle")?e.closest('[part="item"]').querySelector('[part="label"]'):e.closest('[part="label"]');this.#p(t,!0)}#d(){this.#s?.abort(),this.noHover?this.#s=null:(this.#s=new AbortController,this.#e.addEventListener("pointerover",e=>this.#S(e),{signal:this.#s.signal}))}#E(){this.#e.setHTMLUnsafe(t(this.#i.tree)),[...this.#e.querySelectorAll('[part="checkbox"]')].forEach(e=>{const t=this.#i.getItem(r(e.id))?.state;e.checked="checked"===t,e.indeterminate="indeterminate"===t}),this.#r=this.#e.querySelector('[part="label"]')}async#A(i){if("function"!=typeof this.subtreeProvider)return;const s=this.#i.getItem(i);if(null!==s?.children)return;const r=this.#e.getElementById(`item_${i}`);r.inert=!0;try{const t=await this.subtreeProvider(s.value);e.assertRawTreeValid(t),this.#i.setSubtree(s,t)}finally{r.inert=!1}s.children.size&&(r.insertAdjacentHTML("beforeend",t(s.children,!1)),this.disabled&&this.#g(!0,r),this.#I(s),this.#$())}#M(e,t){if(!t?.size)return;const i=e?"add":"delete";t.forEach((t,s)=>{this.#i.selection[i](s);const r=this.#e.getElementById(`cbx_${s}`);r.checked=e,r.indeterminate=!1,this.#M(e,t.children)})}#g(e,t=this.#e){[...t.querySelectorAll("button, input")].forEach(t=>t.disabled=e)}#I(e){e.children&&this.#M(this.#i.selection.has(e.id),e.children)}#C(e){if(this.#i.tree.has(e.id))return;const t=this.#i.getParentItem(e.id),i=this.#i.calcItemState(t);this.#i.selection["checked"===i?"add":"delete"](t.id);const s=this.#e.getElementById(`cbx_${t.id}`);s.checked="checked"===i,s.indeterminate="indeterminate"===i,this.#C(t)}#$(){this.#t.setFormValue(this.formData,JSON.stringify(this))}#u(e){const t=r(e.id),i=e.checked?"add":"delete";this.#i.selection[i](t);const s=this.#i.getItem(t);this.#I(s),this.#C(s),this.#$(),this.dispatchEvent(new CustomEvent("cbxtreechange",{bubbles:!0,detail:this.formData}))}#b(e){const t="true"!==e.ariaExpanded;e.ariaExpanded=t?"true":"false";const i=r(e.id);t&&this.#A(i);const s=this.#i.getItem(i);s.collapsed=!t,this.#p(e.querySelector('[part="label"]'),!0),this.#$(),this.dispatchEvent(new CustomEvent("cbxtreetoggle",{bubbles:!0,detail:{title:s.title,value:s.value,newState:t?"expanded":"collapsed"}}))}#p(e,t=!1){e&&(this.#r=e,e.focus({preventScroll:t}))}#k(){const e=this.#e.querySelector('[part="label"]');this.#p(e)}#y(){const e=this.#a.at(-1);this.#p(e)}#m(){const e=this.#r;if(!e)return void this.#k();const t=this.#a,i=t[t.indexOf(e)+1];this.#p(i)}#f(){const e=this.#r;if(!e)return void this.#k();const t=this.#a,i=t[t.indexOf(e)-1];this.#p(i)}#x(){const{clientHeight:e,scrollHeight:t}=this;if(t-e<10)return void this.#y();const i=this.#a,s=t/i.length,r=Math.floor(e/s);let a=i.indexOf(this.#r);-1===a&&(a=0);const n=Math.min(a+r-1,i.length-1);this.#p(i[n])}#w(){const{clientHeight:e,scrollHeight:t}=this;if(t-e<10)return void this.#k();const i=this.#a,s=t/i.length,r=Math.round(e/s);let a=i.indexOf(this.#r);-1===a&&(a=0);const n=Math.max(a-r+1,0);this.#p(i[n])}#v(){const e=this.#r;if(!e)return void this.#k();const t=e.closest('[part="tree"]').closest('[part="item"]')?.querySelector('[part="label"]');this.#p(t)}#n(){const t=this.textContent.trim()||"[]";try{const i=JSON.parse(t);return e.assertRawTreeValid(i),i}catch{return console.error(new DOMException(" contents must be a valid JSON array representation","DataError")),[]}}setData(t){e.assertRawTreeValid(t),this.#i=new e(t),this.#E(),this.#$()}toJSON(){return this.#i.toRaw()}toggleChecked(e){void 0===e&&(e=!!this.#e.querySelector('[part="checkbox"]:not(:checked)')),this.#M(e,this.#i.tree),this.#$()}toggle(e){let t=[...this.#e.querySelectorAll('[part="item"]:has([part="tree"])')];void 0===e&&(e=t.some(({ariaExpanded:e})=>"false"===e));const i=String(e);t.forEach(t=>{if(t.ariaExpanded===i)return;t.ariaExpanded=i;this.#i.getItem(r(t.id)).collapsed=!e}),this.#$()}get validity(){return this.#t.validity}get validationMessage(){return this.#t.validationMessage}get willValidate(){return this.#t.willValidate}checkValidity(){return this.#t.checkValidity()}reportValidity(){return this.#t.reportValidity()}setValidity(...e){return this.#t.setValidity(...e)}}customElements.define("cbx-tree",n);export{n as default}; \ No newline at end of file diff --git a/docs/cbx-tree.mjs b/docs/cbx-tree.mjs index dc53a89..af1f8a7 100644 --- a/docs/cbx-tree.mjs +++ b/docs/cbx-tree.mjs @@ -1,6 +1,6 @@ /*! -cbx-tree v1.1.1 +cbx-tree v2.0.0 https://amphiluke.github.io/cbx-tree/ (c) 2025 Amphiluke */ -const e=(e,i=!0)=>`\n
      \n ${[...e.values()].reduce((e,i)=>e+t(i),"")}\n
    `,t=({id:t,title:r,icon:a,collapsed:s,children:n})=>`\n
  • \n ${void 0!==n?'':""}\n \n ${n?.size>0?e(n,!1):""}\n
  • `,i=e=>e?e.startsWith("")?'e.replaceAll(t,`&#${t.charCodeAt(0)};`),e))(e)}" alt="" part="icon">`:"",r=e=>e?.slice(e.indexOf("_")+1),a=e=>{if(!Array.isArray(e))throw new TypeError("Tree data must be an array of tree items")},s=new CSSStyleSheet;s.replaceSync(':host{--cbx-tree-toggle-closed-mask: url(\'data:image/svg+xml,\');--cbx-tree-toggle-open-mask: url(\'data:image/svg+xml,\');--cbx-tree-toggle-pending-mask: url(\'data:image/svg+xml,\');--cbx-tree-label-hover-bg: SelectedItem;--cbx-tree-label-hover-fg: SelectedItemText;--cbx-tree-nesting-indent: 1em}:host(:dir(rtl)){--cbx-tree-toggle-closed-mask: url(\'data:image/svg+xml,\')}:host(:not([hidden])){display:block}[part=tree]{list-style:none;margin:0;padding:0;&:has([inert]){cursor:progress}&:not([part=tree] [part=tree]){overflow-x:clip}}[part=item]{align-items:center;display:grid;gap:0 .6ch;grid-template-areas:"toggle label" "tree tree";grid-template-columns:max(1em,16px) 1fr;&[aria-expanded=false]>[part=tree]{display:none}}[part=toggle]{background:none;border:none;color:inherit;font:inherit;grid-area:toggle;height:max(1em,16px);padding:0;position:relative;width:max(1em,16px);z-index:1;&:not(:disabled){cursor:pointer}&:before{background:currentColor;content:"";inset:-4px;mask:var(--cbx-tree-toggle-closed-mask) 50% 50% / contain no-repeat content-box;padding:4px;position:absolute}[aria-expanded=true]>&:before{mask-image:var(--cbx-tree-toggle-open-mask)}[inert]>&:before{mask-image:var(--cbx-tree-toggle-pending-mask)}&:where(:hover,:focus-visible):before,&:has(+[part=label]:hover):before,[part=item]:has(>[part=label] :focus-visible)>&:before{color:var(--cbx-tree-label-hover-fg)}}[part=label]{align-items:inherit;display:flex;gap:inherit;grid-area:label;isolation:isolate;padding-block:.2em;position:relative;&:hover,&:has(:focus-visible),[part=toggle]:where(:hover,:focus-visible)+&{color:var(--cbx-tree-label-hover-fg);&:before{background:var(--cbx-tree-label-hover-bg)}}&:before{content:"";inset:0;inset-inline-start:-100vw;position:absolute;z-index:-1}}:where([part=item]) [part=tree]{grid-area:tree;padding-inline-start:var(--cbx-tree-nesting-indent)}');class n extends HTMLElement{static get formAssociated(){return!0}#e;#t;#i=new Map;#r=new Set;subtreeProvider=null;get formData(){const e=new FormData,{name:t}=this;return this.#r.forEach(i=>{const r=this.#a(i)?.value;void 0!==r&&e.append(t,r)}),e}get form(){return this.#t.form}get name(){return this.getAttribute("name")}set name(e){this.setAttribute("name",e)}get disabled(){return this.hasAttribute("disabled")}set disabled(e){e?this.setAttribute("disabled",""):this.removeAttribute("disabled")}get type(){return this.localName}constructor(){super(),this.#e=this.attachShadow({mode:"open"}),this.#e.adoptedStyleSheets=[s],this.#t=this.attachInternals(),this.setData(this.#s()),this.hasAttribute("tabindex")||(this.tabIndex=0),this.#e.addEventListener("change",e=>this.#n(e)),this.#e.addEventListener("click",e=>this.#l(e))}formDisabledCallback(e){this.#o(e)}formResetCallback(){this.setData(this.#s())}formStateRestoreCallback(e,t){if("restore"===t)try{this.setData(JSON.parse(e))}catch(e){console.warn("Failed to restore the tree state",e)}}#n({target:e}){if(!e.part.contains("checkbox"))return;const t=r(e.id),i=e.checked?"add":"delete";this.#r[i](t);const a=this.#a(t);this.#d(a),this.#h(a),this.#c(),this.dispatchEvent(new CustomEvent("cbxtreechange",{bubbles:!0,detail:this.formData}))}#l({target:e}){if(!e.part.contains("toggle"))return;const t=e.closest('[part="item"]'),i="true"!==t.ariaExpanded;t.ariaExpanded=i?"true":"false";const a=r(t.id);i&&this.#g(a);const s=this.#a(a);s.collapsed=!i,this.#c(),this.dispatchEvent(new CustomEvent("cbxtreetoggle",{bubbles:!0,detail:{title:s.title,value:s.value,newState:i?"expanded":"collapsed"}}))}#p(){this.#e.setHTMLUnsafe(e(this.#i)),[...this.#e.querySelectorAll('[part="checkbox"]')].forEach(e=>{const t=this.#a(r(e.id))?.state;e.checked="checked"===t,e.indeterminate="indeterminate"===t})}#u(e,t){return new Map(e.map((e,i)=>{const r=t?`${t}:${i}`:String(i);e.checked&&this.#r.add(r);const a={id:r,title:e.title,value:e.value,icon:e.icon,collapsed:e.children?.length?!!e.collapsed:null===e.children||void 0,children:e.children?this.#u(e.children,r):e.children};return Object.defineProperty(a,"state",{get:()=>this.#r.has(a.id)?"checked":a.children?.size?this.#b(a):"unchecked"}),[r,a]}))}async#g(t){if("function"!=typeof this.subtreeProvider)return;const i=this.#a(t);if(null!==i?.children)return;const r=this.#e.getElementById(`item_${t}`);r.inert=!0;try{const e=await this.subtreeProvider(i.value);a(e),i.children=this.#u(e,i.id)}finally{r.inert=!1}i.children.size&&(r.insertAdjacentHTML("beforeend",e(i.children,!1)),this.disabled&&this.#o(!0,r),this.#d(i),this.#c())}#a(e){const t=e.split(":");return t.slice(1).reduce((e,t)=>e?.children?.get(`${e?.id}:${t}`),this.#i.get(t[0]))}#b(e){const t=new Set([...e.children.values()].map(({state:e})=>e));return t.has("indeterminate")?"indeterminate":t.has("checked")?t.has("unchecked")?"indeterminate":"checked":"unchecked"}#m(e,t){if(!t?.size)return;const i=e?"add":"delete";t.forEach((t,r)=>{this.#r[i](r);const a=this.#e.getElementById(`cbx_${r}`);a.checked=e,a.indeterminate=!1,this.#m(e,t.children)})}#o(e,t=this.#e){[...t.querySelectorAll("button, input")].forEach(t=>t.disabled=e)}#d(e){e.children&&this.#m(this.#r.has(e.id),e.children)}#h(e){if(this.#i.has(e.id))return;const t=this.#a(e.id.slice(0,e.id.lastIndexOf(":"))),i=this.#b(t);this.#r["checked"===i?"add":"delete"](t.id);const r=this.#e.getElementById(`cbx_${t.id}`);r.checked="checked"===i,r.indeterminate="indeterminate"===i,this.#h(t)}#c(){this.#t.setFormValue(this.formData,JSON.stringify(this))}#s(){const e=this.textContent.trim()||"[]";try{const t=JSON.parse(e);return a(t),t}catch{return console.error(new DOMException(" contents must be a valid JSON array representation","DataError")),[]}}#v(e=this.#i){return[...e.values()].map(e=>({title:e.title,value:e.value,icon:e.icon,checked:this.#r.has(e.id),collapsed:!0===e.collapsed||void 0,children:e.children?this.#v(e.children):e.children}))}setData(e){a(e),this.#r.clear(),this.#i=this.#u(e),this.#p(),this.#c()}toJSON(){return this.#v()}toggleChecked(e){void 0===e&&(e=!!this.#e.querySelector('[part="checkbox"]:not(:checked)')),this.#m(e,this.#i),this.#c()}toggle(e){let t=[...this.#e.querySelectorAll('[part="item"]:has([part="tree"])')];void 0===e&&(e=t.some(({ariaExpanded:e})=>"false"===e));const i=String(e);t.forEach(t=>{if(t.ariaExpanded===i)return;t.ariaExpanded=i;this.#a(r(t.id)).collapsed=!e}),this.#c()}get validity(){return this.#t.validity}get validationMessage(){return this.#t.validationMessage}get willValidate(){return this.#t.willValidate}checkValidity(){return this.#t.checkValidity()}reportValidity(){return this.#t.reportValidity()}setValidity(...e){return this.#t.setValidity(...e)}}customElements.define("cbx-tree",n);export{n as default}; \ No newline at end of file +class e{static assertRawTreeValid(e){if(!Array.isArray(e))throw new TypeError("Tree data must be an array of tree items")}#e=new Map;get tree(){return this.#e}selection=new Set;constructor(e){this.#e=this.#t(e)}#t(e,t){return new Map(e.map((e,i)=>{const s=t?`${t}:${i}`:String(i);e.checked&&this.selection.add(s);const r={id:s,title:e.title,value:e.value,icon:e.icon,collapsed:e.children?.length?!!e.collapsed:null===e.children||void 0,children:e.children?this.#t(e.children,s):e.children};return Object.defineProperty(r,"state",{get:()=>this.selection.has(r.id)?"checked":r.children?.size?this.calcItemState(r):"unchecked"}),[s,r]}))}getItem(e){const[t,...i]=e.split(":");return i.reduce((e,t)=>e?.children?.get(`${e?.id}:${t}`),this.#e.get(t))}getParentItem(e){return this.getItem(e.slice(0,e.lastIndexOf(":")))}calcItemState(e){const t=new Set([...e.children.values()].map(({state:e})=>e));return t.has("indeterminate")?"indeterminate":t.has("checked")?t.has("unchecked")?"indeterminate":"checked":"unchecked"}setSubtree(e,t){e.children=this.#t(t,e.id)}toRaw(e=this.#e){return[...e.values()].map(e=>({title:e.title,value:e.value,icon:e.icon,checked:this.selection.has(e.id),collapsed:!0===e.collapsed||void 0,children:e.children?this.toRaw(e.children):e.children}))}}const t=(e,t=!0)=>`\n
      \n ${[...e.values()].reduce((e,t)=>e+i(t),"")}\n
    `,i=({id:e,title:i,icon:r,collapsed:a,children:n})=>`\n
  • \n ${void 0!==n?'':""}\n \n ${n?.size>0?t(n,!1):""}\n
  • `,s=e=>e?e.startsWith("")?'e.replaceAll(t,`&#${t.charCodeAt(0)};`),e))(e)}" alt="" part="icon">`:"",r=e=>e?.slice(e.indexOf("_")+1),a=new CSSStyleSheet;a.replaceSync(':host{--cbx-tree-toggle-closed-mask: url(\'data:image/svg+xml,\');--cbx-tree-toggle-open-mask: url(\'data:image/svg+xml,\');--cbx-tree-toggle-pending-mask: url(\'data:image/svg+xml,\');--cbx-tree-label-focus-bg: SelectedItem;--cbx-tree-label-focus-fg: SelectedItemText;--cbx-tree-nesting-indent: 1em}:host(:dir(rtl)){--cbx-tree-toggle-closed-mask: url(\'data:image/svg+xml,\')}:host(:not([hidden])){display:block}[part=tree]{list-style:none;margin:0;padding:0;&:has([inert]){cursor:progress}&:not([part=tree] [part=tree]){overflow-x:clip}}[part=item]{align-items:center;display:grid;gap:0 .6ch;grid-template-areas:"toggle label" "tree tree";grid-template-columns:max(1em,16px) 1fr;&[aria-expanded=false]>[part=tree]{display:none}}[part=toggle]{background:none;border:none;color:inherit;font:inherit;grid-area:toggle;height:max(1em,16px);padding:0;position:relative;width:max(1em,16px);z-index:1;&:not(:disabled){cursor:pointer}&:before{background:currentColor;content:"";inset:-4px;mask:var(--cbx-tree-toggle-closed-mask) 50% 50% / contain no-repeat content-box;padding:4px;position:absolute}[aria-expanded=true]>&:before{mask-image:var(--cbx-tree-toggle-open-mask)}[inert]>&:before{mask-image:var(--cbx-tree-toggle-pending-mask)}&:has(+[part=label]:focus):before{color:var(--cbx-tree-label-focus-fg)}}[part=label]{align-items:inherit;display:flex;gap:inherit;grid-area:label;isolation:isolate;outline:none;padding-block:.2em;position:relative;&:focus{color:var(--cbx-tree-label-focus-fg);&:before{background:var(--cbx-tree-label-focus-bg)}}&:before{content:"";inset:0;inset-inline-start:-100vw;position:absolute;z-index:-1}}:where([part=item]) [part=tree]{grid-area:tree;padding-inline-start:var(--cbx-tree-nesting-indent)}');class n extends HTMLElement{static get formAssociated(){return!0}static get observedAttributes(){return["nohover"]}#e;#t;#i;#s=null;get#r(){return this.#e.querySelector('[tabindex="0"]')}set#r(e){const t=this.#r;e!==t&&(t?.removeAttribute("tabindex"),e?.setAttribute("tabindex","0"))}get#a(){return[...this.#e.querySelectorAll('[part="label"]:not([aria-expanded="false"] [part="tree"] *)')]}subtreeProvider=null;get formData(){const e=new FormData,{name:t}=this;return this.#i.selection.forEach(i=>{const s=this.#i.getItem(i)?.value;void 0!==s&&e.append(t,s)}),e}get form(){return this.#t.form}get name(){return this.getAttribute("name")}set name(e){this.setAttribute("name",e)}get disabled(){return this.hasAttribute("disabled")}set disabled(e){e?this.setAttribute("disabled",""):this.removeAttribute("disabled")}get noHover(){return this.hasAttribute("nohover")}set noHover(e){e?this.setAttribute("nohover",""):this.removeAttribute("nohover")}get type(){return this.localName}constructor(){super(),this.#e=this.attachShadow({mode:"open"}),this.#e.adoptedStyleSheets=[a],this.#t=this.attachInternals(),this.setData(this.#n()),this.hasAttribute("tabindex")||(this.tabIndex=0),this.#e.addEventListener("change",e=>this.#o(e)),this.#e.addEventListener("pointerdown",e=>this.#l(e)),this.addEventListener("focus",()=>this.#h()),this.#e.addEventListener("keydown",e=>this.#c(e)),this.#d()}attributeChangedCallback(e){"nohover"===e&&this.#d()}formDisabledCallback(e){this.#g(e)}formResetCallback(){this.setData(this.#n())}formStateRestoreCallback(e,t){if("restore"===t)try{this.setData(JSON.parse(e))}catch(e){console.warn("Failed to restore the tree state",e)}}#o({target:e}){if(e.part.contains("checkbox")){this.#u(e);const t=e.closest('[part="label"]');return void this.#p(t,!0)}}#l(e){e.isPrimary&&e.target.part.contains("toggle")&&(this.#b(e.target.closest('[part="item"]')),e.preventDefault())}#h(){this.#r?.focus()}#c(e){if(!e.defaultPrevented&&!this.disabled){switch(e.key){case"ArrowRight":{const e=this.#r?.closest('[part="item"]');"true"===e?.ariaExpanded?this.#m():"false"===e?.ariaExpanded&&this.#b(e);break}case"ArrowLeft":{const e=this.#r?.closest('[part="item"]');"true"===e?.ariaExpanded?this.#b(e):this.#v();break}case"ArrowDown":this.#m();break;case"ArrowUp":this.#f();break;case"PageDown":this.#x();break;case"PageUp":this.#w();break;case"Home":this.#k();break;case"End":this.#y();break;case"Enter":{const e=this.#r?.closest('[part="item"]');"undefined"!==e.ariaExpanded&&this.#b(e);break}case" ":{const e=this.#r?.querySelector('[part="checkbox"]');e&&(e.checked=!e.checked,e.indeterminate=!1,this.#u(e));break}default:return}e.preventDefault()}}#S({target:e}){const t=e.part.contains("toggle")?e.closest('[part="item"]').querySelector('[part="label"]'):e.closest('[part="label"]');this.#p(t,!0)}#d(){this.#s?.abort(),this.noHover?this.#s=null:(this.#s=new AbortController,this.#e.addEventListener("pointerover",e=>this.#S(e),{signal:this.#s.signal}))}#E(){this.#e.setHTMLUnsafe(t(this.#i.tree)),[...this.#e.querySelectorAll('[part="checkbox"]')].forEach(e=>{const t=this.#i.getItem(r(e.id))?.state;e.checked="checked"===t,e.indeterminate="indeterminate"===t}),this.#r=this.#e.querySelector('[part="label"]')}async#A(i){if("function"!=typeof this.subtreeProvider)return;const s=this.#i.getItem(i);if(null!==s?.children)return;const r=this.#e.getElementById(`item_${i}`);r.inert=!0;try{const t=await this.subtreeProvider(s.value);e.assertRawTreeValid(t),this.#i.setSubtree(s,t)}finally{r.inert=!1}s.children.size&&(r.insertAdjacentHTML("beforeend",t(s.children,!1)),this.disabled&&this.#g(!0,r),this.#I(s),this.#$())}#M(e,t){if(!t?.size)return;const i=e?"add":"delete";t.forEach((t,s)=>{this.#i.selection[i](s);const r=this.#e.getElementById(`cbx_${s}`);r.checked=e,r.indeterminate=!1,this.#M(e,t.children)})}#g(e,t=this.#e){[...t.querySelectorAll("button, input")].forEach(t=>t.disabled=e)}#I(e){e.children&&this.#M(this.#i.selection.has(e.id),e.children)}#C(e){if(this.#i.tree.has(e.id))return;const t=this.#i.getParentItem(e.id),i=this.#i.calcItemState(t);this.#i.selection["checked"===i?"add":"delete"](t.id);const s=this.#e.getElementById(`cbx_${t.id}`);s.checked="checked"===i,s.indeterminate="indeterminate"===i,this.#C(t)}#$(){this.#t.setFormValue(this.formData,JSON.stringify(this))}#u(e){const t=r(e.id),i=e.checked?"add":"delete";this.#i.selection[i](t);const s=this.#i.getItem(t);this.#I(s),this.#C(s),this.#$(),this.dispatchEvent(new CustomEvent("cbxtreechange",{bubbles:!0,detail:this.formData}))}#b(e){const t="true"!==e.ariaExpanded;e.ariaExpanded=t?"true":"false";const i=r(e.id);t&&this.#A(i);const s=this.#i.getItem(i);s.collapsed=!t,this.#p(e.querySelector('[part="label"]'),!0),this.#$(),this.dispatchEvent(new CustomEvent("cbxtreetoggle",{bubbles:!0,detail:{title:s.title,value:s.value,newState:t?"expanded":"collapsed"}}))}#p(e,t=!1){e&&(this.#r=e,e.focus({preventScroll:t}))}#k(){const e=this.#e.querySelector('[part="label"]');this.#p(e)}#y(){const e=this.#a.at(-1);this.#p(e)}#m(){const e=this.#r;if(!e)return void this.#k();const t=this.#a,i=t[t.indexOf(e)+1];this.#p(i)}#f(){const e=this.#r;if(!e)return void this.#k();const t=this.#a,i=t[t.indexOf(e)-1];this.#p(i)}#x(){const{clientHeight:e,scrollHeight:t}=this;if(t-e<10)return void this.#y();const i=this.#a,s=t/i.length,r=Math.floor(e/s);let a=i.indexOf(this.#r);-1===a&&(a=0);const n=Math.min(a+r-1,i.length-1);this.#p(i[n])}#w(){const{clientHeight:e,scrollHeight:t}=this;if(t-e<10)return void this.#k();const i=this.#a,s=t/i.length,r=Math.round(e/s);let a=i.indexOf(this.#r);-1===a&&(a=0);const n=Math.max(a-r+1,0);this.#p(i[n])}#v(){const e=this.#r;if(!e)return void this.#k();const t=e.closest('[part="tree"]').closest('[part="item"]')?.querySelector('[part="label"]');this.#p(t)}#n(){const t=this.textContent.trim()||"[]";try{const i=JSON.parse(t);return e.assertRawTreeValid(i),i}catch{return console.error(new DOMException(" contents must be a valid JSON array representation","DataError")),[]}}setData(t){e.assertRawTreeValid(t),this.#i=new e(t),this.#E(),this.#$()}toJSON(){return this.#i.toRaw()}toggleChecked(e){void 0===e&&(e=!!this.#e.querySelector('[part="checkbox"]:not(:checked)')),this.#M(e,this.#i.tree),this.#$()}toggle(e){let t=[...this.#e.querySelectorAll('[part="item"]:has([part="tree"])')];void 0===e&&(e=t.some(({ariaExpanded:e})=>"false"===e));const i=String(e);t.forEach(t=>{if(t.ariaExpanded===i)return;t.ariaExpanded=i;this.#i.getItem(r(t.id)).collapsed=!e}),this.#$()}get validity(){return this.#t.validity}get validationMessage(){return this.#t.validationMessage}get willValidate(){return this.#t.willValidate}checkValidity(){return this.#t.checkValidity()}reportValidity(){return this.#t.reportValidity()}setValidity(...e){return this.#t.setValidity(...e)}}customElements.define("cbx-tree",n);export{n as default}; \ No newline at end of file diff --git a/docs/index.css b/docs/index.css index f243995..3346a4e 100644 --- a/docs/index.css +++ b/docs/index.css @@ -86,8 +86,8 @@ cbx-tree[data-theme='custom'] { --cbx-tree-toggle-closed-mask: url('data:image/svg+xml,'); --cbx-tree-toggle-open-mask: url('data:image/svg+xml,'); --cbx-tree-nesting-indent: 1.3em; - --cbx-tree-label-hover-bg: rgb(from SelectedItem r g b / 0.2); - --cbx-tree-label-hover-fg: currentColor; + --cbx-tree-label-focus-bg: rgb(from SelectedItem r g b / 0.2); + --cbx-tree-label-focus-fg: currentColor; &::part(item) { --line-color: rgb(from currentColor r g b / 0.3); diff --git a/index-prod.html b/index-prod.html deleted file mode 100644 index 337a3c2..0000000 --- a/index-prod.html +++ /dev/null @@ -1,268 +0,0 @@ - - - - - - CbxTree - - - -
    - - - - - -
    - -
    -
    - - -
    - - - - diff --git a/package-lock.json b/package-lock.json index 938310c..898c35d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,26 @@ { "name": "cbx-tree", - "version": "1.1.1", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cbx-tree", - "version": "1.1.1", + "version": "2.0.0", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.30.1", + "@eslint/js": "^9.31.0", "@stylistic/eslint-plugin": "^5.1.0", - "eslint": "^9.30.1", + "eslint": "^9.31.0", "globals": "^16.3.0", "terser": "^5.43.1", - "vite": "^7.0.0" + "vite": "^7.0.4" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", "cpu": [ "ppc64" ], @@ -35,9 +35,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", "cpu": [ "arm" ], @@ -52,9 +52,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", "cpu": [ "arm64" ], @@ -69,9 +69,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", "cpu": [ "x64" ], @@ -86,9 +86,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", "cpu": [ "arm64" ], @@ -103,9 +103,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", "cpu": [ "x64" ], @@ -120,9 +120,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", "cpu": [ "arm64" ], @@ -137,9 +137,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", "cpu": [ "x64" ], @@ -154,9 +154,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", "cpu": [ "arm" ], @@ -171,9 +171,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", "cpu": [ "arm64" ], @@ -188,9 +188,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", "cpu": [ "ia32" ], @@ -205,9 +205,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", "cpu": [ "loong64" ], @@ -222,9 +222,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", "cpu": [ "mips64el" ], @@ -239,9 +239,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", "cpu": [ "ppc64" ], @@ -256,9 +256,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", "cpu": [ "riscv64" ], @@ -273,9 +273,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", "cpu": [ "s390x" ], @@ -290,9 +290,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", "cpu": [ "x64" ], @@ -307,9 +307,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", "cpu": [ "arm64" ], @@ -324,9 +324,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", "cpu": [ "x64" ], @@ -341,9 +341,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", "cpu": [ "arm64" ], @@ -358,9 +358,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", "cpu": [ "x64" ], @@ -374,10 +374,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", "cpu": [ "x64" ], @@ -392,9 +409,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", "cpu": [ "arm64" ], @@ -409,9 +426,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", "cpu": [ "ia32" ], @@ -426,9 +443,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", "cpu": [ "x64" ], @@ -510,9 +527,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -560,9 +577,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", - "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { @@ -596,19 +613,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -726,9 +730,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", - "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.0.tgz", + "integrity": "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==", "cpu": [ "arm" ], @@ -740,9 +744,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", - "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.0.tgz", + "integrity": "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==", "cpu": [ "arm64" ], @@ -754,9 +758,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", - "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.0.tgz", + "integrity": "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==", "cpu": [ "arm64" ], @@ -768,9 +772,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", - "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.0.tgz", + "integrity": "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==", "cpu": [ "x64" ], @@ -782,9 +786,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", - "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.0.tgz", + "integrity": "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==", "cpu": [ "arm64" ], @@ -796,9 +800,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", - "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.0.tgz", + "integrity": "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==", "cpu": [ "x64" ], @@ -810,9 +814,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", - "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.0.tgz", + "integrity": "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==", "cpu": [ "arm" ], @@ -824,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", - "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.0.tgz", + "integrity": "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==", "cpu": [ "arm" ], @@ -838,9 +842,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", - "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.0.tgz", + "integrity": "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==", "cpu": [ "arm64" ], @@ -852,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", - "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.0.tgz", + "integrity": "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==", "cpu": [ "arm64" ], @@ -866,9 +870,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", - "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.0.tgz", + "integrity": "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==", "cpu": [ "loong64" ], @@ -880,9 +884,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", - "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.0.tgz", + "integrity": "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==", "cpu": [ "ppc64" ], @@ -894,9 +898,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", - "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.0.tgz", + "integrity": "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==", "cpu": [ "riscv64" ], @@ -908,9 +912,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", - "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.0.tgz", + "integrity": "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==", "cpu": [ "riscv64" ], @@ -922,9 +926,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", - "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.0.tgz", + "integrity": "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==", "cpu": [ "s390x" ], @@ -936,9 +940,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", - "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.0.tgz", + "integrity": "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==", "cpu": [ "x64" ], @@ -950,9 +954,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", - "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.0.tgz", + "integrity": "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==", "cpu": [ "x64" ], @@ -964,9 +968,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", - "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.0.tgz", + "integrity": "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==", "cpu": [ "arm64" ], @@ -978,9 +982,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", - "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.0.tgz", + "integrity": "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==", "cpu": [ "ia32" ], @@ -992,9 +996,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", - "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.0.tgz", + "integrity": "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==", "cpu": [ "x64" ], @@ -1041,9 +1045,9 @@ "license": "MIT" }, "node_modules/@typescript-eslint/types": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", - "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz", + "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", "dev": true, "license": "MIT", "engines": { @@ -1244,9 +1248,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1257,31 +1261,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" } }, "node_modules/escape-string-regexp": { @@ -1298,9 +1303,9 @@ } }, "node_modules/eslint": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", - "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1308,9 +1313,9 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.1", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -1947,9 +1952,9 @@ } }, "node_modules/rollup": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", - "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.0.tgz", + "integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==", "dev": true, "license": "MIT", "dependencies": { @@ -1963,26 +1968,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.44.1", - "@rollup/rollup-android-arm64": "4.44.1", - "@rollup/rollup-darwin-arm64": "4.44.1", - "@rollup/rollup-darwin-x64": "4.44.1", - "@rollup/rollup-freebsd-arm64": "4.44.1", - "@rollup/rollup-freebsd-x64": "4.44.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", - "@rollup/rollup-linux-arm-musleabihf": "4.44.1", - "@rollup/rollup-linux-arm64-gnu": "4.44.1", - "@rollup/rollup-linux-arm64-musl": "4.44.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", - "@rollup/rollup-linux-riscv64-gnu": "4.44.1", - "@rollup/rollup-linux-riscv64-musl": "4.44.1", - "@rollup/rollup-linux-s390x-gnu": "4.44.1", - "@rollup/rollup-linux-x64-gnu": "4.44.1", - "@rollup/rollup-linux-x64-musl": "4.44.1", - "@rollup/rollup-win32-arm64-msvc": "4.44.1", - "@rollup/rollup-win32-ia32-msvc": "4.44.1", - "@rollup/rollup-win32-x64-msvc": "4.44.1", + "@rollup/rollup-android-arm-eabi": "4.45.0", + "@rollup/rollup-android-arm64": "4.45.0", + "@rollup/rollup-darwin-arm64": "4.45.0", + "@rollup/rollup-darwin-x64": "4.45.0", + "@rollup/rollup-freebsd-arm64": "4.45.0", + "@rollup/rollup-freebsd-x64": "4.45.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", + "@rollup/rollup-linux-arm-musleabihf": "4.45.0", + "@rollup/rollup-linux-arm64-gnu": "4.45.0", + "@rollup/rollup-linux-arm64-musl": "4.45.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", + "@rollup/rollup-linux-riscv64-gnu": "4.45.0", + "@rollup/rollup-linux-riscv64-musl": "4.45.0", + "@rollup/rollup-linux-s390x-gnu": "4.45.0", + "@rollup/rollup-linux-x64-gnu": "4.45.0", + "@rollup/rollup-linux-x64-musl": "4.45.0", + "@rollup/rollup-win32-arm64-msvc": "4.45.0", + "@rollup/rollup-win32-ia32-msvc": "4.45.0", + "@rollup/rollup-win32-x64-msvc": "4.45.0", "fsevents": "~2.3.2" } }, @@ -2126,9 +2131,9 @@ } }, "node_modules/vite": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.0.tgz", - "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz", + "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 178ad8c..c57049f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cbx-tree", - "version": "1.1.1", + "version": "2.0.0", "description": "Web Component for building tree-like UI with checkable items", "type": "module", "main": "./dist/cbx-tree.mjs", @@ -37,11 +37,11 @@ }, "homepage": "https://amphiluke.github.io/cbx-tree/", "devDependencies": { - "@eslint/js": "^9.30.1", + "@eslint/js": "^9.31.0", "@stylistic/eslint-plugin": "^5.1.0", - "eslint": "^9.30.1", + "eslint": "^9.31.0", "globals": "^16.3.0", "terser": "^5.43.1", - "vite": "^7.0.0" + "vite": "^7.0.4" } } diff --git a/src/cbx-tree.css b/src/cbx-tree.css index 2141a47..953874a 100644 --- a/src/cbx-tree.css +++ b/src/cbx-tree.css @@ -3,8 +3,8 @@ --cbx-tree-toggle-open-mask: url('data:image/svg+xml,'); --cbx-tree-toggle-pending-mask: url('data:image/svg+xml,'); - --cbx-tree-label-hover-bg: SelectedItem; - --cbx-tree-label-hover-fg: SelectedItemText; + --cbx-tree-label-focus-bg: SelectedItem; + --cbx-tree-label-focus-fg: SelectedItemText; --cbx-tree-nesting-indent: 1em; } @@ -78,10 +78,8 @@ mask-image: var(--cbx-tree-toggle-pending-mask); } - &:where(:hover, :focus-visible)::before, - &:has(+ [part='label']:hover)::before, - [part='item']:has(> [part='label'] :focus-visible) > &::before { - color: var(--cbx-tree-label-hover-fg); + &:has(+ [part='label']:focus)::before { + color: var(--cbx-tree-label-focus-fg); } } @@ -91,16 +89,15 @@ gap: inherit; grid-area: label; isolation: isolate; + outline: none; padding-block: 0.2em; position: relative; - &:hover, - &:has(:focus-visible), - [part='toggle']:where(:hover, :focus-visible) + & { - color: var(--cbx-tree-label-hover-fg); + &:focus { + color: var(--cbx-tree-label-focus-fg); &::before { - background: var(--cbx-tree-label-hover-bg); + background: var(--cbx-tree-label-focus-bg); } } diff --git a/src/cbx-tree.mjs b/src/cbx-tree.mjs index 7392caf..52b163f 100644 --- a/src/cbx-tree.mjs +++ b/src/cbx-tree.mjs @@ -1,55 +1,52 @@ +import {Tree} from './tree.mjs'; import {treeTemplate} from './templating.mjs'; -import {unprefixId, assertRawTreeValid} from './helpers.mjs'; +import {unprefixId} from './helpers.mjs'; import css from './cbx-tree.css?inline'; +/** @import {CbxRawTreeItem, CbxTreeItem, CbxTreeMap} from './tree.mjs' */ + const stylesheet = new CSSStyleSheet(); stylesheet.replaceSync(css); -/** - * Raw user-defined data for a single item of the tree - * @typedef {object} CbxRawTreeItem - * @property {string} title - Item title - * @property {string} value - Item checkbox’s value, unique within the entire tree - * @property {string} [icon] - Item icon’s URL - * @property {boolean} [checked] - Item selection state - * @property {boolean} [collapsed] - Whether a children subtree is collapsed - * @property {CbxRawTreeItem[] | null} [children] - A list of child items, or `null` if subtree isn’t fetched yet - */ - -/** - * Internal representation for a single item of the tree - * @typedef {object} CbxTreeItem - * @property {string} id - Item identifier, unique within the entire tree - * @property {string} title - Item title - * @property {string} value - Item checkbox’s value, unique within the entire tree - * @property {string} [icon] - Item icon’s URL - * @property {'checked' | 'unchecked' | 'indeterminate'} state - Computed state of the item’s selection - * @property {boolean} [collapsed] - Whether a children subtree is collapsed - * @property {CbxTreeMap | null} [children] - A map of child items, or `null` if subtree isn’t fetched yet - */ - -/** - * Map ids to corresponding tree items - * @typedef {Map} CbxTreeMap - */ - export default class CbxTree extends HTMLElement { static get formAssociated() { return true; } + static get observedAttributes() { + return ['nohover']; + } + /** @type {ShadowRoot} */ #shadowRoot; /** @type {ElementInternals} */ #internals; - /** @type {CbxTreeMap} */ - #tree = new Map(); + /** @type {Tree} */ + #tree; + + /** @type {AbortController | null} */ + #hoverEventCtrl = null; - /** @type {Set} */ - #selection = new Set(); + /** @type {HTMLLabelElement | null} */ + get #focusedLabel() { + return this.#shadowRoot.querySelector('[tabindex="0"]'); + } + set #focusedLabel(newLabel) { + const prevLabel = this.#focusedLabel; + if (newLabel === prevLabel) { + return; + } + prevLabel?.removeAttribute('tabindex'); + newLabel?.setAttribute('tabindex', '0'); + } + + /** @type {HTMLLabelElement[]} */ + get #visibleLabels() { + return [...this.#shadowRoot.querySelectorAll('[part="label"]:not([aria-expanded="false"] [part="tree"] *)')]; + } /** @type {((parentValue: string) => Promise) | null} */ subtreeProvider = null; @@ -58,8 +55,8 @@ export default class CbxTree extends HTMLElement { get formData() { const data = new FormData(); const {name} = this; - this.#selection.forEach((id) => { - const value = this.#getItem(id)?.value; + this.#tree.selection.forEach((id) => { + const value = this.#tree.getItem(id)?.value; if (value !== undefined) { data.append(name, value); } @@ -89,6 +86,17 @@ export default class CbxTree extends HTMLElement { } } + get noHover() { + return this.hasAttribute('nohover'); + } + set noHover(value) { + if (value) { + this.setAttribute('nohover', ''); + } else { + this.removeAttribute('nohover'); + } + } + get type() { return this.localName; } @@ -109,12 +117,21 @@ export default class CbxTree extends HTMLElement { } this.#shadowRoot.addEventListener('change', (e) => this.#onChange(e)); - this.#shadowRoot.addEventListener('click', (e) => this.#onItemToggle(e)); + this.#shadowRoot.addEventListener('pointerdown', (e) => this.#onItemToggle(e)); + this.addEventListener('focus', () => this.#onFocus()); + this.#shadowRoot.addEventListener('keydown', (e) => this.#onKeyDown(e)); + this.#toggleHoverListener(); } // === Lifecycle callbacks === + attributeChangedCallback(name) { + if (name === 'nohover') { + this.#toggleHoverListener(); + } + } + formDisabledCallback(disabled) { this.#setControlsDisabled(disabled); } @@ -138,95 +155,124 @@ export default class CbxTree extends HTMLElement { // === Event listeners === #onChange({target}) { - if (!target.part.contains('checkbox')) { + if (target.part.contains('checkbox')) { + this.#toggleItemChecked(target); + const label = target.closest('[part="label"]'); + this.#focusLabel(label, true); return; } - const id = unprefixId(target.id); - const method = target.checked ? 'add' : 'delete'; - this.#selection[method](id); - const item = this.#getItem(id); - // Order of synchronisation matters (descendants first, then ancestors) - this.#syncDescendants(item); - this.#syncAncestors(item); - this.#refreshFormValue(); - this.dispatchEvent(new CustomEvent('cbxtreechange', {bubbles: true, detail: this.formData})); } - #onItemToggle({target}) { - if (!target.part.contains('toggle')) { + #onItemToggle(event) { + if (event.isPrimary && event.target.part.contains('toggle')) { + this.#toggleItem(event.target.closest('[part="item"]')); + event.preventDefault(); // prevent toggle button from grabbing focus + } + } + + #onFocus() { + this.#focusedLabel?.focus(); + } + + #onKeyDown(e) { + if (e.defaultPrevented || this.disabled) { return; } - const itemElement = target.closest('[part="item"]'); - const isExpanding = itemElement.ariaExpanded !== 'true'; - itemElement.ariaExpanded = isExpanding ? 'true' : 'false'; - const id = unprefixId(itemElement.id); - if (isExpanding) { - this.#requestSubtree(id); + switch (e.key) { + case 'ArrowRight': { + const item = this.#focusedLabel?.closest('[part="item"]'); + if (item?.ariaExpanded === 'true') { + this.#focusNext(); + } else if (item?.ariaExpanded === 'false') { + this.#toggleItem(item); + } + break; + } + case 'ArrowLeft': { + const item = this.#focusedLabel?.closest('[part="item"]'); + if (item?.ariaExpanded === 'true') { + this.#toggleItem(item); + } else { + this.#focusParent(); + } + break; + } + case 'ArrowDown': + this.#focusNext(); + break; + case 'ArrowUp': + this.#focusPrev(); + break; + case 'PageDown': + this.#focusNextPage(); + break; + case 'PageUp': + this.#focusPrevPage(); + break; + case 'Home': + this.#focusFirst(); + break; + case 'End': + this.#focusLast(); + break; + case 'Enter': { + const item = this.#focusedLabel?.closest('[part="item"]'); + if (item.ariaExpanded !== 'undefined') { + this.#toggleItem(item); + } + break; + } + case ' ': { + const checkbox = this.#focusedLabel?.querySelector('[part="checkbox"]'); + if (checkbox) { + checkbox.checked = !checkbox.checked; + checkbox.indeterminate = false; + this.#toggleItemChecked(checkbox); + } + break; + } + default: + return; } - const item = this.#getItem(id); - item.collapsed = !isExpanding; - this.#refreshFormValue(); - this.dispatchEvent(new CustomEvent('cbxtreetoggle', {bubbles: true, detail: { - title: item.title, - value: item.value, - newState: isExpanding ? 'expanded' : 'collapsed', - }})); + e.preventDefault(); + } + + #onPointerOver({target}) { + const label = target.part.contains('toggle') ? + target.closest('[part="item"]').querySelector('[part="label"]') : + target.closest('[part="label"]'); + this.#focusLabel(label, true); } // === Internals === + #toggleHoverListener() { + this.#hoverEventCtrl?.abort(); + if (this.noHover) { + this.#hoverEventCtrl = null; + return; + } + this.#hoverEventCtrl = new AbortController(); + this.#shadowRoot.addEventListener('pointerover', (e) => this.#onPointerOver(e), {signal: this.#hoverEventCtrl.signal}); + } + #render() { - this.#shadowRoot.setHTMLUnsafe(treeTemplate(this.#tree)); + this.#shadowRoot.setHTMLUnsafe(treeTemplate(this.#tree.tree)); const checkboxes = this.#shadowRoot.querySelectorAll('[part="checkbox"]'); [...checkboxes].forEach((checkbox) => { - const state = this.#getItem(unprefixId(checkbox.id))?.state; + const state = this.#tree.getItem(unprefixId(checkbox.id))?.state; checkbox.checked = state === 'checked'; checkbox.indeterminate = state === 'indeterminate'; }); - } - - /** - * Convert raw tree data to internal tree representation - * @param {CbxRawTreeItem[]} rawTree - Raw tree data - * @param {string} parentId - Identifier of a parent item (the case of building a subtree) - * @returns {CbxTreeMap} - */ - #buildTree(rawTree, parentId) { - return new Map(rawTree.map((rawItem, index) => { - const id = parentId ? `${parentId}:${index}` : String(index); - if (rawItem.checked) { - this.#selection.add(id); - } - /** @type {CbxTreeItem} */ - const item = { - id, - title: rawItem.title, - value: rawItem.value, - icon: rawItem.icon, - collapsed: rawItem.children?.length ? !!rawItem.collapsed : (rawItem.children === null ? true : undefined), - children: rawItem.children ? this.#buildTree(rawItem.children, id) : rawItem.children, - }; - Object.defineProperty(item, 'state', { - get: () => { - if (this.#selection.has(item.id)) { - return 'checked'; - } - if (!item.children?.size) { - return 'unchecked'; - } - return this.#calcItemState(item); - }, - }); - return [id, item]; - })); + this.#focusedLabel = this.#shadowRoot.querySelector('[part="label"]'); } async #requestSubtree(parentId) { if (typeof this.subtreeProvider !== 'function') { return; } - const parentItem = this.#getItem(parentId); + const parentItem = this.#tree.getItem(parentId); if (parentItem?.children !== null) { return; } @@ -234,8 +280,8 @@ export default class CbxTree extends HTMLElement { itemElement.inert = true; try { const subtree = await this.subtreeProvider(parentItem.value); - assertRawTreeValid(subtree); - parentItem.children = this.#buildTree(subtree, parentItem.id); + Tree.assertRawTreeValid(subtree); + this.#tree.setSubtree(parentItem, subtree); } finally { itemElement.inert = false; } @@ -250,35 +296,6 @@ export default class CbxTree extends HTMLElement { this.#refreshFormValue(); } - /** - * Get item object reference by item id - * @param {string} id - Item identifier - * @returns {CbxTreeItem | undefined} - */ - #getItem(id) { - const parts = id.split(':'); - return parts.slice(1).reduce((item, part) => item?.children?.get(`${item?.id}:${part}`), this.#tree.get(parts[0])); - } - - /** - * Determine item state based on the states of its children - * @param {CbxTreeItem} item - * @returns {'checked' | 'unchecked' | 'indeterminate'} - */ - #calcItemState(item) { - const childrenStates = new Set([...item.children.values()].map(({state}) => state)); - if (childrenStates.has('indeterminate')) { - return 'indeterminate'; - } - if (!childrenStates.has('checked')) { - return 'unchecked'; - } - if (!childrenStates.has('unchecked')) { - return 'checked'; - } - return 'indeterminate'; - } - /** * Check/uncheck all items of a the tree or a subtree * @param {boolean} isChecked @@ -290,7 +307,7 @@ export default class CbxTree extends HTMLElement { } const method = isChecked ? 'add' : 'delete'; tree.forEach((item, id) => { - this.#selection[method](id); + this.#tree.selection[method](id); const checkbox = this.#shadowRoot.getElementById(`cbx_${id}`); checkbox.checked = isChecked; checkbox.indeterminate = false; @@ -314,7 +331,7 @@ export default class CbxTree extends HTMLElement { */ #syncDescendants(item) { if (item.children) { - this.#setAllChecked(this.#selection.has(item.id), item.children); + this.#setAllChecked(this.#tree.selection.has(item.id), item.children); } } @@ -323,12 +340,12 @@ export default class CbxTree extends HTMLElement { * @param {CbxTreeItem} item */ #syncAncestors(item) { - if (this.#tree.has(item.id)) { // top-level item + if (this.#tree.tree.has(item.id)) { // top-level item return; } - const parentItem = this.#getItem(item.id.slice(0, item.id.lastIndexOf(':'))); - const state = this.#calcItemState(parentItem); - this.#selection[state === 'checked' ? 'add' : 'delete'](parentItem.id); + const parentItem = this.#tree.getParentItem(item.id); + const state = this.#tree.calcItemState(parentItem); + this.#tree.selection[state === 'checked' ? 'add' : 'delete'](parentItem.id); const checkbox = this.#shadowRoot.getElementById(`cbx_${parentItem.id}`); checkbox.checked = state === 'checked'; checkbox.indeterminate = state === 'indeterminate'; @@ -339,12 +356,154 @@ export default class CbxTree extends HTMLElement { this.#internals.setFormValue(this.formData, JSON.stringify(this)); } + /** + * Toggle the item’s checked state based on the checkbox current state + * @param {HTMLInputElement} checkbox + */ + #toggleItemChecked(checkbox) { + const id = unprefixId(checkbox.id); + const method = checkbox.checked ? 'add' : 'delete'; + this.#tree.selection[method](id); + const item = this.#tree.getItem(id); + // Order of synchronisation matters (descendants first, then ancestors) + this.#syncDescendants(item); + this.#syncAncestors(item); + this.#refreshFormValue(); + this.dispatchEvent(new CustomEvent('cbxtreechange', {bubbles: true, detail: this.formData})); + } + + /** + * Expand the item if it is collapsed or collapse if it is expanded + * @param {HTMLLIElement} itemElement + */ + #toggleItem(itemElement) { + const isExpanding = itemElement.ariaExpanded !== 'true'; + itemElement.ariaExpanded = isExpanding ? 'true' : 'false'; + const id = unprefixId(itemElement.id); + if (isExpanding) { + this.#requestSubtree(id); + } + const item = this.#tree.getItem(id); + item.collapsed = !isExpanding; + this.#focusLabel(itemElement.querySelector('[part="label"]'), true); + this.#refreshFormValue(); + this.dispatchEvent(new CustomEvent('cbxtreetoggle', {bubbles: true, detail: { + title: item.title, + value: item.value, + newState: isExpanding ? 'expanded' : 'collapsed', + }})); + } + + #focusLabel(label, preventScroll = false) { + if (label) { + this.#focusedLabel = label; + label.focus({preventScroll}); + } + } + + /** + * Apply focus to the first item in the tree + */ + #focusFirst() { + const firstLabel = this.#shadowRoot.querySelector('[part="label"]'); + this.#focusLabel(firstLabel); + } + + /** + * Apply focus to the last visible item in the tree + */ + #focusLast() { + const lastLabel = this.#visibleLabels.at(-1); + this.#focusLabel(lastLabel); + } + + /** + * Move focus to the next visible item in the tree + */ + #focusNext() { + const currentLabel = this.#focusedLabel; + if (!currentLabel) { + this.#focusFirst(); + return; + } + const visibleLabels = this.#visibleLabels; + const nextLabel = visibleLabels[visibleLabels.indexOf(currentLabel) + 1]; + this.#focusLabel(nextLabel); + } + + /** + * Move focus to the previous visible item in the tree + */ + #focusPrev() { + const currentLabel = this.#focusedLabel; + if (!currentLabel) { + this.#focusFirst(); + return; + } + const visibleLabels = this.#visibleLabels; + const prevLabel = visibleLabels[visibleLabels.indexOf(currentLabel) - 1]; + this.#focusLabel(prevLabel); + } + + /** + * Move focus one page down + */ + #focusNextPage() { + const {clientHeight, scrollHeight} = this; + if (scrollHeight - clientHeight < 10) { + this.#focusLast(); + return; + } + const visibleLabels = this.#visibleLabels; + const labelHeight = scrollHeight / visibleLabels.length; + const pageSize = Math.floor(clientHeight / labelHeight); + let currentIndex = visibleLabels.indexOf(this.#focusedLabel); + if (currentIndex === -1) { + currentIndex = 0; + } + const nextIndex = Math.min(currentIndex + pageSize - 1, visibleLabels.length - 1); + this.#focusLabel(visibleLabels[nextIndex]); + } + + /** + * Move focus one page up + */ + #focusPrevPage() { + const {clientHeight, scrollHeight} = this; + if (scrollHeight - clientHeight < 10) { + this.#focusFirst(); + return; + } + const visibleLabels = this.#visibleLabels; + const labelHeight = scrollHeight / visibleLabels.length; + const pageSize = Math.round(clientHeight / labelHeight); + let currentIndex = visibleLabels.indexOf(this.#focusedLabel); + if (currentIndex === -1) { + currentIndex = 0; + } + const nextIndex = Math.max(currentIndex - pageSize + 1, 0); + this.#focusLabel(visibleLabels[nextIndex]); + } + + /** + * Move focus one level up, to the parent item + */ + #focusParent() { + const currentLabel = this.#focusedLabel; + if (!currentLabel) { + this.#focusFirst(); + return; + } + const parentLabel = currentLabel.closest('[part="tree"]').closest('[part="item"]')?.querySelector('[part="label"]'); + this.#focusLabel(parentLabel); + } + /** @returns {CbxRawTreeItem[]} */ #getDefaultRawTree() { const contentJSON = this.textContent.trim() || '[]'; try { const tree = JSON.parse(contentJSON); - assertRawTreeValid(tree); + Tree.assertRawTreeValid(tree); return tree; } catch { console.error(new DOMException(' contents must be a valid JSON array representation', 'DataError')); @@ -352,22 +511,6 @@ export default class CbxTree extends HTMLElement { } } - /** - * Convert internal representation of a tree back to its raw format - * @param {CbxTreeMap} tree - * @returns {CbxRawTreeItem[]} - */ - #toRaw(tree = this.#tree) { - return [...tree.values()].map((item) => ({ - title: item.title, - value: item.value, - icon: item.icon, - checked: this.#selection.has(item.id), - collapsed: item.collapsed === true ? true : undefined, - children: item.children ? this.#toRaw(item.children) : item.children, - })); - } - // === Public interface === @@ -376,15 +519,14 @@ export default class CbxTree extends HTMLElement { * @param {CbxRawTreeItem[]} treeData */ setData(treeData) { - assertRawTreeValid(treeData); - this.#selection.clear(); - this.#tree = this.#buildTree(treeData); + Tree.assertRawTreeValid(treeData); + this.#tree = new Tree(treeData); this.#render(); this.#refreshFormValue(); } toJSON() { - return this.#toRaw(); + return this.#tree.toRaw(); } /** @@ -395,7 +537,7 @@ export default class CbxTree extends HTMLElement { if (checked === undefined) { checked = !!this.#shadowRoot.querySelector('[part="checkbox"]:not(:checked)'); } - this.#setAllChecked(checked, this.#tree); + this.#setAllChecked(checked, this.#tree.tree); this.#refreshFormValue(); } @@ -414,7 +556,7 @@ export default class CbxTree extends HTMLElement { return; } itemElement.ariaExpanded = ariaExpanded; - const item = this.#getItem(unprefixId(itemElement.id)); + const item = this.#tree.getItem(unprefixId(itemElement.id)); item.collapsed = !isExpanding; }); this.#refreshFormValue(); diff --git a/src/helpers.mjs b/src/helpers.mjs index ddc3634..369fafd 100644 --- a/src/helpers.mjs +++ b/src/helpers.mjs @@ -4,13 +4,3 @@ * @returns string */ export const unprefixId = (id) => id?.slice(id.indexOf('_') + 1); - -/** - * A loose assertion for the argument to be a valid raw tree - * @param {*} rawTree - */ -export const assertRawTreeValid = (rawTree) => { - if (!Array.isArray(rawTree)) { // cheap and cheerful (kind of) - throw new TypeError('Tree data must be an array of tree items'); - } -}; diff --git a/src/templating.mjs b/src/templating.mjs index 341f673..2c0aadb 100644 --- a/src/templating.mjs +++ b/src/templating.mjs @@ -1,4 +1,4 @@ -/** @import {CbxTreeItem, CbxTreeMap} from './cbx-tree.mjs' */ +/** @import {CbxTreeItem, CbxTreeMap} from './tree.mjs' */ const sanitize = (unsafeStr) => ['&', '"'].reduce((str, char) => str.replaceAll(char, `&#${char.charCodeAt(0)};`), unsafeStr); @@ -20,9 +20,9 @@ export const treeTemplate = (tree, isRoot = true) => ` */ export const itemTemplate = ({id, title, icon, collapsed, children}) => `
  • - ${(children !== undefined) ? '' : ''} + ${(children !== undefined) ? '' : ''} diff --git a/src/tree.mjs b/src/tree.mjs new file mode 100644 index 0000000..1c45d5f --- /dev/null +++ b/src/tree.mjs @@ -0,0 +1,148 @@ +/** + * Raw user-defined data for a single item of the tree + * @typedef {object} CbxRawTreeItem + * @property {string} title - Item title + * @property {string} value - Item checkbox’s value, unique within the entire tree + * @property {string} [icon] - Item icon’s URL + * @property {boolean} [checked] - Item selection state + * @property {boolean} [collapsed] - Whether a children subtree is collapsed + * @property {CbxRawTreeItem[] | null} [children] - A list of child items, or `null` if subtree isn’t fetched yet + */ + +/** + * Internal representation for a single item of the tree + * @typedef {object} CbxTreeItem + * @property {string} id - Item identifier, unique within the entire tree + * @property {string} title - Item title + * @property {string} value - Item checkbox’s value, unique within the entire tree + * @property {string} [icon] - Item icon’s URL + * @property {'checked' | 'unchecked' | 'indeterminate'} state - Computed state of the item’s selection + * @property {boolean} [collapsed] - Whether a children subtree is collapsed + * @property {CbxTreeMap | null} [children] - A map of child items, or `null` if subtree isn’t fetched yet + */ + +/** + * Map ids to corresponding tree items + * @typedef {Map} CbxTreeMap + */ + +export class Tree { + static assertRawTreeValid(rawTree) { + if (!Array.isArray(rawTree)) { // cheap and cheerful (kind of) + throw new TypeError('Tree data must be an array of tree items'); + } + } + + /** @type {CbxTreeMap} */ + #tree = new Map(); + + get tree() { + return this.#tree; + } + + /** @type {Set} */ + selection = new Set(); + + constructor(rawTree) { + this.#tree = this.#buildTree(rawTree); + } + + /** + * Convert raw tree data to internal tree representation + * @param {CbxRawTreeItem[]} rawTree - Raw tree data + * @param {string} [parentId] - Identifier of a parent item (the case of building a subtree) + * @returns {CbxTreeMap} + */ + #buildTree(rawTree, parentId) { + return new Map(rawTree.map((rawItem, index) => { + const id = parentId ? `${parentId}:${index}` : String(index); + if (rawItem.checked) { + this.selection.add(id); + } + /** @type {CbxTreeItem} */ + const item = { + id, + title: rawItem.title, + value: rawItem.value, + icon: rawItem.icon, + collapsed: rawItem.children?.length ? !!rawItem.collapsed : (rawItem.children === null ? true : undefined), + children: rawItem.children ? this.#buildTree(rawItem.children, id) : rawItem.children, + }; + Object.defineProperty(item, 'state', { + get: () => { + if (this.selection.has(item.id)) { + return 'checked'; + } + if (!item.children?.size) { + return 'unchecked'; + } + return this.calcItemState(item); + }, + }); + return [id, item]; + })); + } + + /** + * Get item object reference by item id + * @param {string} id - Item identifier + * @returns {CbxTreeItem | undefined} + */ + getItem(id) { + const [topPart, ...parts] = id.split(':'); + return parts.reduce((item, part) => item?.children?.get(`${item?.id}:${part}`), this.#tree.get(topPart)); + } + + /** + * Get parent item object reference by the id of its child item + * @param {string} id - Child item identifier + * @returns {CbxTreeItem | undefined} + */ + getParentItem(id) { + return this.getItem(id.slice(0, id.lastIndexOf(':'))); + } + + /** + * Determine item state based on the states of its children + * @param {CbxTreeItem} item + * @returns {'checked' | 'unchecked' | 'indeterminate'} + */ + calcItemState(item) { + const childrenStates = new Set([...item.children.values()].map(({state}) => state)); + if (childrenStates.has('indeterminate')) { + return 'indeterminate'; + } + if (!childrenStates.has('checked')) { + return 'unchecked'; + } + if (!childrenStates.has('unchecked')) { + return 'checked'; + } + return 'indeterminate'; + } + + /** + * Overwrite the subtree of an item + * @param {CbxTreeItem} parentItem - Parent item for the subtree + * @param {CbxRawTreeItem[]} rawSubtree - Raw subtree data + */ + setSubtree(parentItem, rawSubtree) { + parentItem.children = this.#buildTree(rawSubtree, parentItem.id); + } + + /** + * Convert internal representation of a tree back to its raw format + * @param {CbxTreeMap} tree + * @returns {CbxRawTreeItem[]} + */ + toRaw(tree = this.#tree) { + return [...tree.values()].map((item) => ({ + title: item.title, + value: item.value, + icon: item.icon, + checked: this.selection.has(item.id), + collapsed: item.collapsed === true ? true : undefined, + children: item.children ? this.toRaw(item.children) : item.children, + })); + } +} diff --git a/test/index.css b/test/index.css index 64374e8..33f7f24 100644 --- a/test/index.css +++ b/test/index.css @@ -32,8 +32,8 @@ html { } cbx-tree { - --cbx-tree-label-hover-bg: rgb(from SaddleBrown r g b / 0.15); - --cbx-tree-label-hover-fg: SaddleBrown; + --cbx-tree-label-focus-bg: rgb(from SaddleBrown r g b / 0.15); + --cbx-tree-label-focus-fg: SaddleBrown; scrollbar-color: SaddleBrown AntiqueWhite; } }