Draggable
When drag, show a placeholder to help user.
diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.nojekyll @@ -0,0 +1 @@ + diff --git a/404.html b/404.html new file mode 100644 index 0000000..01e5146 --- /dev/null +++ b/404.html @@ -0,0 +1,23 @@ + + +
+ + +404
But if you don't change your direction, and if you keep looking, you may end up where you are heading.
=0)c=r.activeElement;else{var f=i.tabbableGroups[0],p=f&&f.firstTabbableNode;c=p||h("fallbackFocus")}if(!c)throw new Error("Your focus-trap needs to have at least one focusable element");return c},v=function(){if(i.containerGroups=i.containers.map(function(c){var f=br(c,a.tabbableOptions),p=wr(c,a.tabbableOptions),C=f.length>0?f[0]:void 0,I=f.length>0?f[f.length-1]:void 0,M=p.find(function(m){return le(m)}),P=p.slice().reverse().find(function(m){return le(m)}),z=!!f.find(function(m){return se(m)>0});return{container:c,tabbableNodes:f,focusableNodes:p,posTabIndexesFound:z,firstTabbableNode:C,lastTabbableNode:I,firstDomTabbableNode:M,lastDomTabbableNode:P,nextTabbableNode:function(x){var $=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,K=f.indexOf(x);return K<0?$?p.slice(p.indexOf(x)+1).find(function(q){return le(q)}):p.slice(0,p.indexOf(x)).reverse().find(function(q){return le(q)}):f[K+($?1:-1)]}}}),i.tabbableGroups=i.containerGroups.filter(function(c){return c.tabbableNodes.length>0}),i.tabbableGroups.length<=0&&!h("fallbackFocus"))throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times");if(i.containerGroups.find(function(c){return c.posTabIndexesFound})&&i.containerGroups.length>1)throw new Error("At least one node with a positive tabindex was found in one of your focus-trap's multiple containers. Positive tabindexes are only supported in single-container focus-traps.")},y=function w(c){var f=c.activeElement;if(f)return f.shadowRoot&&f.shadowRoot.activeElement!==null?w(f.shadowRoot):f},b=function w(c){if(c!==!1&&c!==y(document)){if(!c||!c.focus){w(d());return}c.focus({preventScroll:!!a.preventScroll}),i.mostRecentlyFocusedNode=c,Ar(c)&&c.select()}},E=function(c){var f=h("setReturnFocus",c);return f||(f===!1?!1:c)},g=function(c){var f=c.target,p=c.event,C=c.isBackward,I=C===void 0?!1:C;f=f||Ae(p),v();var M=null;if(i.tabbableGroups.length>0){var P=l(f,p),z=P>=0?i.containerGroups[P]:void 0;if(P<0)I?M=i.tabbableGroups[i.tabbableGroups.length-1].lastTabbableNode:M=i.tabbableGroups[0].firstTabbableNode;else if(I){var m=ht(i.tabbableGroups,function(V){var U=V.firstTabbableNode;return f===U});if(m<0&&(z.container===f||Re(f,a.tabbableOptions)&&!le(f,a.tabbableOptions)&&!z.nextTabbableNode(f,!1))&&(m=P),m>=0){var x=m===0?i.tabbableGroups.length-1:m-1,$=i.tabbableGroups[x];M=se(f)>=0?$.lastTabbableNode:$.lastDomTabbableNode}else ge(p)||(M=z.nextTabbableNode(f,!1))}else{var K=ht(i.tabbableGroups,function(V){var U=V.lastTabbableNode;return f===U});if(K<0&&(z.container===f||Re(f,a.tabbableOptions)&&!le(f,a.tabbableOptions)&&!z.nextTabbableNode(f))&&(K=P),K>=0){var q=K===i.tabbableGroups.length-1?0:K+1,H=i.tabbableGroups[q];M=se(f)>=0?H.firstTabbableNode:H.firstDomTabbableNode}else ge(p)||(M=z.nextTabbableNode(f))}}else M=h("fallbackFocus");return M},S=function(c){var f=Ae(c);if(!(l(f,c)>=0)){if(ye(a.clickOutsideDeactivates,c)){s.deactivate({returnFocus:a.returnFocusOnDeactivate});return}ye(a.allowOutsideClick,c)||c.preventDefault()}},T=function(c){var f=Ae(c),p=l(f,c)>=0;if(p||f instanceof Document)p&&(i.mostRecentlyFocusedNode=f);else{c.stopImmediatePropagation();var C,I=!0;if(i.mostRecentlyFocusedNode)if(se(i.mostRecentlyFocusedNode)>0){var M=l(i.mostRecentlyFocusedNode),P=i.containerGroups[M].tabbableNodes;if(P.length>0){var z=P.findIndex(function(m){return m===i.mostRecentlyFocusedNode});z>=0&&(a.isKeyForward(i.recentNavEvent)?z+1
H)for(;C<=B;)Le(u[C],b,S,!0),C++;else{const W=C,z=C,Q=new Map;for(C=z;C<=H;C++){const _e=d[C]=R?ze(d[C]):Re(d[C]);_e.key!=null&&Q.set(_e.key,C)}let te,ae=0;const Ae=H-z+1;let gt=!1,Zr=0;const Ot=new Array(Ae);for(C=0;C {const{el:S,type:L,transition:x,children:R,shapeFlag:C}=u;if(C&6){tt(u.component.subTree,d,m,v);return}if(C&128){u.suspense.move(d,m,v);return}if(C&64){L.move(u,d,m,pt);return}if(L===me){r(S,d,m);for(let B=0;B The exported variables, methods, and Typescript types. Typescript types: The main function of this library. React hook. The arguments are as follows: options: Options, type is object. The following are some properties in options: The remaining callback functions in options: The return of The method of traversing tree data through The method to traverse tree data through the callback method. Executing Like Like Open all parent nodes of a single or multiple nodes to make the node visible. Reference. Update the Sort the flat data according to the order of the nodes in the tree. Return the new sorted array. Your data should use it to ensure order after initialized. The method of traversing flat data through Compared to walkTreeDataGenerator, it lacks The method of traversing flat data through the callback method. Executing Open all parent nodes of a single or multiple nodes to make the node visible. Make sure your data is in the correct order before using it. Reference. Update the Calculate the index of a node in the tree through its parent node id and its index in the sibling nodes. Add a node to the flat data. It will change the original data array. Therefore, it is recommended to pass in a copy of the original data, or use it together with Remove node by id from the flat data. It will change the original data array. Therefore, it is recommended to pass in a copy of the original data, or use it together with A method to iterate over another special kind of data. This data is like Source Source Source Source Source Source Source Source Source Source Source Source Source Source Source This library supports two types of data structures: The This library does not export components, but exports a hook See the You can add the Node HTML: There are two The outer div is called Lines 4 to 7 are drag-and-drop placeholder. Line 9 is node. Use the following options to control: Use the following options to control: Due to the immutable nature of React, it is difficult to update flat data and tree data. For flat data, this library provides two methods to add nodes or delete nodes. If you want to perform more complex operations, or update tree data, it is recommended that you use Note, here we use Note, here we use Related options: Use option It is based on HTML5 Drag and Drop API. So it works in any device that supports Drag and Drop API. For others, you can try Drag and Drop API polyfill. Notice In mobile, user need touch and hold to trigger drag. 此库导出的变量, 方法, Typescript 类型. 以下为 Typescript 的类型: 本库的主要功能. React hook. 参数如下: options: 选项, 类型是对象. 以下是 options 中的部分属性: 以下是 options 中的剩余回调方法: 通过 通过回调方法遍历树形数据的方法. 回调方法中执行 类似 类似 打开单个或多个节点的所有父节点, 这样才能确保该节点可见. 参考. 更新单个节点或多个节点的 把扁平数据按照节点在树里的顺序排序. 返回排序后的新数组. 你的数据在初始化时应该使用它以保证顺序. 通过 相比于 通过回调方法遍历扁平数据的方法. 回调方法中执行 打开单个或多个节点的所有父节点, 这样才能确保该节点可见. 用前需确保你的数据的顺序是正确的. 参考. 更新单个节点或多个节点的 通过某节点的父节点 id 和它在兄弟节点中的索引, 计算出它在整棵树中的索引. 向扁平数据添加一个节点. 它会改变原数据数组. 所以推荐传入原始数据的拷贝, 或者与 从扁平数据删除一个节点. 返回被删除的数据. 它会改变原数据数组. 所以推荐传入原始数据的拷贝, 或者与 遍历另一种特殊数据的方法. 这种数据类似 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 此库支持两种结构的数据: 数据中的 此库没有导出组件,而是导出一个 hook 给节点任意子元素添加 节点 HTML 如下: 上面有两个 div. 使用 外层节点被称为 第 4 到第 7 行是拖拽占位节点. 第 9 行是节点元素. 此例子顶部 4 个按钮分别是: 展开全部, 折叠全部, 展开'Python'节点的所有父节点, 仅展开'Python'节点的所有父节点. 使用以下选项控制: 使用以下选项控制: 由于 React 的不可变特性, 扁平数据和树形数据更新都很困难. 针对扁平数据, 此库提供了两个方法, 用以增加节点或删除节点. 如果你要进行更复杂的操作, 或者更新树形数据, 推荐你使用 注意, 这里使用了 注意, 这里使用了 相关选项: 使用选项 此库基于 HTML5 Drag and Drop API, 所以在支持 Drag and Drop API 的移动设备上能工作. 如果不支持, 可以尝试添加兼容 Drag and Drop API 的库. 注意 触摸时, 用户需要触摸并等一会儿才能触发拖拽。 The exported variables, methods, and Typescript types. Typescript types: The main function of this library. React hook. The arguments are as follows: options: Options, type is object. The following are some properties in options: The remaining callback functions in options: The return of The method of traversing tree data through The method to traverse tree data through the callback method. Executing Like Like Open all parent nodes of a single or multiple nodes to make the node visible. Reference. Update the Sort the flat data according to the order of the nodes in the tree. Return the new sorted array. Your data should use it to ensure order after initialized. The method of traversing flat data through Compared to walkTreeDataGenerator, it lacks The method of traversing flat data through the callback method. Executing Open all parent nodes of a single or multiple nodes to make the node visible. Make sure your data is in the correct order before using it. Reference. Update the Calculate the index of a node in the tree through its parent node id and its index in the sibling nodes. Add a node to the flat data. It will change the original data array. Therefore, it is recommended to pass in a copy of the original data, or use it together with Remove node by id from the flat data. It will change the original data array. Therefore, it is recommended to pass in a copy of the original data, or use it together with A method to iterate over another special kind of data. This data is like Source Source Source Source Source Source Source Source Source Source Source Source Source Source Source This library supports two types of data structures: The This library does not export components, but exports a hook See the You can add the Node HTML: There are two The outer div is called Lines 4 to 7 are drag-and-drop placeholder. Line 9 is node. Use the following options to control: Use the following options to control: Due to the immutable nature of React, it is difficult to update flat data and tree data. For flat data, this library provides two methods to add nodes or delete nodes. If you want to perform more complex operations, or update tree data, it is recommended that you use Note, here we use Note, here we use Related options: Use option It is based on HTML5 Drag and Drop API. So it works in any device that supports Drag and Drop API. For others, you can try Drag and Drop API polyfill. Notice In mobile, user need touch and hold to trigger drag. 此库导出的变量, 方法, Typescript 类型. 以下为 Typescript 的类型: 本库的主要功能. React hook. 参数如下: options: 选项, 类型是对象. 以下是 options 中的部分属性: 以下是 options 中的剩余回调方法: 通过 通过回调方法遍历树形数据的方法. 回调方法中执行 类似 类似 打开单个或多个节点的所有父节点, 这样才能确保该节点可见. 参考. 更新单个节点或多个节点的 把扁平数据按照节点在树里的顺序排序. 返回排序后的新数组. 你的数据在初始化时应该使用它以保证顺序. 通过 相比于 通过回调方法遍历扁平数据的方法. 回调方法中执行 打开单个或多个节点的所有父节点, 这样才能确保该节点可见. 用前需确保你的数据的顺序是正确的. 参考. 更新单个节点或多个节点的 通过某节点的父节点 id 和它在兄弟节点中的索引, 计算出它在整棵树中的索引. 向扁平数据添加一个节点. 它会改变原数据数组. 所以推荐传入原始数据的拷贝, 或者与 从扁平数据删除一个节点. 返回被删除的数据. 它会改变原数据数组. 所以推荐传入原始数据的拷贝, 或者与 遍历另一种特殊数据的方法. 这种数据类似 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 源代码 此库支持两种结构的数据: 数据中的 此库没有导出组件,而是导出一个 hook 给节点任意子元素添加 节点 HTML 如下: 上面有两个 div. 使用 外层节点被称为 第 4 到第 7 行是拖拽占位节点. 第 9 行是节点元素. 此例子顶部 4 个按钮分别是: 展开全部, 折叠全部, 展开'Python'节点的所有父节点, 仅展开'Python'节点的所有父节点. 使用以下选项控制: 使用以下选项控制: 由于 React 的不可变特性, 扁平数据和树形数据更新都很困难. 针对扁平数据, 此库提供了两个方法, 用以增加节点或删除节点. 如果你要进行更复杂的操作, 或者更新树形数据, 推荐你使用 注意, 这里使用了 注意, 这里使用了 相关选项: 使用选项 此库基于 HTML5 Drag and Drop API, 所以在支持 Drag and Drop API 的移动设备上能工作. 如果不支持, 可以尝试添加兼容 Drag and Drop API 的库. 注意 触摸时, 用户需要触摸并等一会儿才能触发拖拽。Features
',2),o=[r];function s(n,d,c,h,p,u){return a(),t("div",null,o)}const f=e(i,[["render",s]]);export{_ as __pageData,f as default};
diff --git a/assets/index.md.Dnn-p8GL.lean.js b/assets/index.md.Dnn-p8GL.lean.js
new file mode 100644
index 0000000..4ad82ac
--- /dev/null
+++ b/assets/index.md.Dnn-p8GL.lean.js
@@ -0,0 +1 @@
+import{_ as e,c as t,o as a,a5 as l}from"./chunks/framework.BthLuVtL.js";const _=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"He Tree React","tagline":"React draggable sortable tree component","actions":[{"theme":"brand","text":"Get started","link":"./v1/guide"},{"theme":"alt","text":"Online Examples","link":"./v1/examples"}]},"features":[{"title":"Draggable","details":"When drag, show a placeholder to help user."},{"title":"High-performance","details":"Supports virtual list to handle large data calmly."},{"title":"Easy to customize","details":"Simple structure, few built-in style, so the style and UI can be easily customized."}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}'),i={name:"index.md"},r=l("",2),o=[r];function s(n,d,c,h,p,u){return a(),t("div",null,o)}const f=e(i,[["render",s]]);export{_ as __pageData,f as default};
diff --git a/assets/inter-italic-cyrillic-ext.5XJwZIOp.woff2 b/assets/inter-italic-cyrillic-ext.5XJwZIOp.woff2
new file mode 100644
index 0000000..2a68729
Binary files /dev/null and b/assets/inter-italic-cyrillic-ext.5XJwZIOp.woff2 differ
diff --git a/assets/inter-italic-cyrillic.D6csxwjC.woff2 b/assets/inter-italic-cyrillic.D6csxwjC.woff2
new file mode 100644
index 0000000..f640351
Binary files /dev/null and b/assets/inter-italic-cyrillic.D6csxwjC.woff2 differ
diff --git a/assets/inter-italic-greek-ext.CHOfFY1k.woff2 b/assets/inter-italic-greek-ext.CHOfFY1k.woff2
new file mode 100644
index 0000000..0021896
Binary files /dev/null and b/assets/inter-italic-greek-ext.CHOfFY1k.woff2 differ
diff --git a/assets/inter-italic-greek.9J96vYpw.woff2 b/assets/inter-italic-greek.9J96vYpw.woff2
new file mode 100644
index 0000000..71c265f
Binary files /dev/null and b/assets/inter-italic-greek.9J96vYpw.woff2 differ
diff --git a/assets/inter-italic-latin-ext.BGcWXLrn.woff2 b/assets/inter-italic-latin-ext.BGcWXLrn.woff2
new file mode 100644
index 0000000..9c1b944
Binary files /dev/null and b/assets/inter-italic-latin-ext.BGcWXLrn.woff2 differ
diff --git a/assets/inter-italic-latin.DbsTr1gm.woff2 b/assets/inter-italic-latin.DbsTr1gm.woff2
new file mode 100644
index 0000000..01fcf20
Binary files /dev/null and b/assets/inter-italic-latin.DbsTr1gm.woff2 differ
diff --git a/assets/inter-italic-vietnamese.DHNAd7Wr.woff2 b/assets/inter-italic-vietnamese.DHNAd7Wr.woff2
new file mode 100644
index 0000000..e4f788e
Binary files /dev/null and b/assets/inter-italic-vietnamese.DHNAd7Wr.woff2 differ
diff --git a/assets/inter-roman-cyrillic-ext.DxP3Awbn.woff2 b/assets/inter-roman-cyrillic-ext.DxP3Awbn.woff2
new file mode 100644
index 0000000..28593cc
Binary files /dev/null and b/assets/inter-roman-cyrillic-ext.DxP3Awbn.woff2 differ
diff --git a/assets/inter-roman-cyrillic.CMhn1ESj.woff2 b/assets/inter-roman-cyrillic.CMhn1ESj.woff2
new file mode 100644
index 0000000..a20adc1
Binary files /dev/null and b/assets/inter-roman-cyrillic.CMhn1ESj.woff2 differ
diff --git a/assets/inter-roman-greek-ext.D0mI3NpI.woff2 b/assets/inter-roman-greek-ext.D0mI3NpI.woff2
new file mode 100644
index 0000000..e3b0be7
Binary files /dev/null and b/assets/inter-roman-greek-ext.D0mI3NpI.woff2 differ
diff --git a/assets/inter-roman-greek.JvnBZ4YD.woff2 b/assets/inter-roman-greek.JvnBZ4YD.woff2
new file mode 100644
index 0000000..f790e04
Binary files /dev/null and b/assets/inter-roman-greek.JvnBZ4YD.woff2 differ
diff --git a/assets/inter-roman-latin-ext.ZlYT4o7i.woff2 b/assets/inter-roman-latin-ext.ZlYT4o7i.woff2
new file mode 100644
index 0000000..715bd90
Binary files /dev/null and b/assets/inter-roman-latin-ext.ZlYT4o7i.woff2 differ
diff --git a/assets/inter-roman-latin.Bu8hRsVA.woff2 b/assets/inter-roman-latin.Bu8hRsVA.woff2
new file mode 100644
index 0000000..a540b7a
Binary files /dev/null and b/assets/inter-roman-latin.Bu8hRsVA.woff2 differ
diff --git a/assets/inter-roman-vietnamese.ClpjcLMQ.woff2 b/assets/inter-roman-vietnamese.ClpjcLMQ.woff2
new file mode 100644
index 0000000..5a9f9cb
Binary files /dev/null and b/assets/inter-roman-vietnamese.ClpjcLMQ.woff2 differ
diff --git a/assets/style.CN68qRN9.css b/assets/style.CN68qRN9.css
new file mode 100644
index 0000000..9ba4ffd
--- /dev/null
+++ b/assets/style.CN68qRN9.css
@@ -0,0 +1 @@
+@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-cyrillic.CMhn1ESj.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-cyrillic-ext.DxP3Awbn.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-greek.JvnBZ4YD.woff2) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-greek-ext.D0mI3NpI.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-latin.Bu8hRsVA.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-latin-ext.ZlYT4o7i.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-vietnamese.ClpjcLMQ.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-cyrillic.D6csxwjC.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-cyrillic-ext.5XJwZIOp.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-greek.9J96vYpw.woff2) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-greek-ext.CHOfFY1k.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-latin.DbsTr1gm.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-latin-ext.BGcWXLrn.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-vietnamese.DHNAd7Wr.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Chinese Quotes;src:local("PingFang SC Regular"),local("PingFang SC"),local("SimHei"),local("Source Han Sans SC");unicode-range:U+2018,U+2019,U+201C,U+201D}:root{--vp-c-white: #ffffff;--vp-c-black: #000000;--vp-c-neutral: var(--vp-c-black);--vp-c-neutral-inverse: var(--vp-c-white)}.dark{--vp-c-neutral: var(--vp-c-white);--vp-c-neutral-inverse: var(--vp-c-black)}:root{--vp-c-gray-1: #dddde3;--vp-c-gray-2: #e4e4e9;--vp-c-gray-3: #ebebef;--vp-c-gray-soft: rgba(142, 150, 170, .14);--vp-c-indigo-1: #3451b2;--vp-c-indigo-2: #3a5ccc;--vp-c-indigo-3: #5672cd;--vp-c-indigo-soft: rgba(100, 108, 255, .14);--vp-c-purple-1: #6f42c1;--vp-c-purple-2: #7e4cc9;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .14);--vp-c-green-1: #18794e;--vp-c-green-2: #299764;--vp-c-green-3: #30a46c;--vp-c-green-soft: rgba(16, 185, 129, .14);--vp-c-yellow-1: #915930;--vp-c-yellow-2: #946300;--vp-c-yellow-3: #9f6a00;--vp-c-yellow-soft: rgba(234, 179, 8, .14);--vp-c-red-1: #b8272c;--vp-c-red-2: #d5393e;--vp-c-red-3: #e0575b;--vp-c-red-soft: rgba(244, 63, 94, .14);--vp-c-sponsor: #db2777}.dark{--vp-c-gray-1: #515c67;--vp-c-gray-2: #414853;--vp-c-gray-3: #32363f;--vp-c-gray-soft: rgba(101, 117, 133, .16);--vp-c-indigo-1: #a8b1ff;--vp-c-indigo-2: #5c73e7;--vp-c-indigo-3: #3e63dd;--vp-c-indigo-soft: rgba(100, 108, 255, .16);--vp-c-purple-1: #c8abfa;--vp-c-purple-2: #a879e6;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .16);--vp-c-green-1: #3dd68c;--vp-c-green-2: #30a46c;--vp-c-green-3: #298459;--vp-c-green-soft: rgba(16, 185, 129, .16);--vp-c-yellow-1: #f9b44e;--vp-c-yellow-2: #da8b17;--vp-c-yellow-3: #a46a0a;--vp-c-yellow-soft: rgba(234, 179, 8, .16);--vp-c-red-1: #f66f81;--vp-c-red-2: #f14158;--vp-c-red-3: #b62a3c;--vp-c-red-soft: rgba(244, 63, 94, .16)}:root{--vp-c-bg: #ffffff;--vp-c-bg-alt: #f6f6f7;--vp-c-bg-elv: #ffffff;--vp-c-bg-soft: #f6f6f7}.dark{--vp-c-bg: #1b1b1f;--vp-c-bg-alt: #161618;--vp-c-bg-elv: #202127;--vp-c-bg-soft: #202127}:root{--vp-c-border: #c2c2c4;--vp-c-divider: #e2e2e3;--vp-c-gutter: #e2e2e3}.dark{--vp-c-border: #3c3f44;--vp-c-divider: #2e2e32;--vp-c-gutter: #000000}:root{--vp-c-text-1: rgba(60, 60, 67);--vp-c-text-2: rgba(60, 60, 67, .78);--vp-c-text-3: rgba(60, 60, 67, .56)}.dark{--vp-c-text-1: rgba(255, 255, 245, .86);--vp-c-text-2: rgba(235, 235, 245, .6);--vp-c-text-3: rgba(235, 235, 245, .38)}:root{--vp-c-default-1: var(--vp-c-gray-1);--vp-c-default-2: var(--vp-c-gray-2);--vp-c-default-3: var(--vp-c-gray-3);--vp-c-default-soft: var(--vp-c-gray-soft);--vp-c-brand-1: var(--vp-c-indigo-1);--vp-c-brand-2: var(--vp-c-indigo-2);--vp-c-brand-3: var(--vp-c-indigo-3);--vp-c-brand-soft: var(--vp-c-indigo-soft);--vp-c-brand: var(--vp-c-brand-1);--vp-c-tip-1: var(--vp-c-brand-1);--vp-c-tip-2: var(--vp-c-brand-2);--vp-c-tip-3: var(--vp-c-brand-3);--vp-c-tip-soft: var(--vp-c-brand-soft);--vp-c-note-1: var(--vp-c-brand-1);--vp-c-note-2: var(--vp-c-brand-2);--vp-c-note-3: var(--vp-c-brand-3);--vp-c-note-soft: var(--vp-c-brand-soft);--vp-c-success-1: var(--vp-c-green-1);--vp-c-success-2: var(--vp-c-green-2);--vp-c-success-3: var(--vp-c-green-3);--vp-c-success-soft: var(--vp-c-green-soft);--vp-c-important-1: var(--vp-c-purple-1);--vp-c-important-2: var(--vp-c-purple-2);--vp-c-important-3: var(--vp-c-purple-3);--vp-c-important-soft: var(--vp-c-purple-soft);--vp-c-warning-1: var(--vp-c-yellow-1);--vp-c-warning-2: var(--vp-c-yellow-2);--vp-c-warning-3: var(--vp-c-yellow-3);--vp-c-warning-soft: var(--vp-c-yellow-soft);--vp-c-danger-1: var(--vp-c-red-1);--vp-c-danger-2: var(--vp-c-red-2);--vp-c-danger-3: var(--vp-c-red-3);--vp-c-danger-soft: var(--vp-c-red-soft);--vp-c-caution-1: var(--vp-c-red-1);--vp-c-caution-2: var(--vp-c-red-2);--vp-c-caution-3: var(--vp-c-red-3);--vp-c-caution-soft: var(--vp-c-red-soft)}:root{--vp-font-family-base: "Chinese Quotes", "Inter var", "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--vp-font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}:root{--vp-shadow-1: 0 1px 2px rgba(0, 0, 0, .04), 0 1px 2px rgba(0, 0, 0, .06);--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, .1), 0 2px 6px rgba(0, 0, 0, .08);--vp-shadow-4: 0 14px 44px rgba(0, 0, 0, .12), 0 3px 9px rgba(0, 0, 0, .12);--vp-shadow-5: 0 18px 56px rgba(0, 0, 0, .16), 0 4px 12px rgba(0, 0, 0, .16)}:root{--vp-z-index-footer: 10;--vp-z-index-local-nav: 20;--vp-z-index-nav: 30;--vp-z-index-layout-top: 40;--vp-z-index-backdrop: 50;--vp-z-index-sidebar: 60}@media (min-width: 960px){:root{--vp-z-index-sidebar: 25}}:root{--vp-layout-max-width: 1440px}:root{--vp-header-anchor-symbol: "#"}:root{--vp-code-line-height: 1.7;--vp-code-font-size: .875em;--vp-code-color: var(--vp-c-brand-1);--vp-code-link-color: var(--vp-c-brand-1);--vp-code-link-hover-color: var(--vp-c-brand-2);--vp-code-bg: var(--vp-c-default-soft);--vp-code-block-color: var(--vp-c-text-2);--vp-code-block-bg: var(--vp-c-bg-alt);--vp-code-block-divider-color: var(--vp-c-gutter);--vp-code-lang-color: var(--vp-c-text-3);--vp-code-line-highlight-color: var(--vp-c-default-soft);--vp-code-line-number-color: var(--vp-c-text-3);--vp-code-line-diff-add-color: var(--vp-c-success-soft);--vp-code-line-diff-add-symbol-color: var(--vp-c-success-1);--vp-code-line-diff-remove-color: var(--vp-c-danger-soft);--vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1);--vp-code-line-warning-color: var(--vp-c-warning-soft);--vp-code-line-error-color: var(--vp-c-danger-soft);--vp-code-copy-code-border-color: var(--vp-c-divider);--vp-code-copy-code-bg: var(--vp-c-bg-soft);--vp-code-copy-code-hover-border-color: var(--vp-c-divider);--vp-code-copy-code-hover-bg: var(--vp-c-bg);--vp-code-copy-code-active-text: var(--vp-c-text-2);--vp-code-copy-copied-text-content: "Copied";--vp-code-tab-divider: var(--vp-code-block-divider-color);--vp-code-tab-text-color: var(--vp-c-text-2);--vp-code-tab-bg: var(--vp-code-block-bg);--vp-code-tab-hover-text-color: var(--vp-c-text-1);--vp-code-tab-active-text-color: var(--vp-c-text-1);--vp-code-tab-active-bar-color: var(--vp-c-brand-1)}:root{--vp-button-brand-border: transparent;--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand-3);--vp-button-brand-hover-border: transparent;--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-2);--vp-button-brand-active-border: transparent;--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-c-brand-1);--vp-button-alt-border: transparent;--vp-button-alt-text: var(--vp-c-text-1);--vp-button-alt-bg: var(--vp-c-default-3);--vp-button-alt-hover-border: transparent;--vp-button-alt-hover-text: var(--vp-c-text-1);--vp-button-alt-hover-bg: var(--vp-c-default-2);--vp-button-alt-active-border: transparent;--vp-button-alt-active-text: var(--vp-c-text-1);--vp-button-alt-active-bg: var(--vp-c-default-1);--vp-button-sponsor-border: var(--vp-c-text-2);--vp-button-sponsor-text: var(--vp-c-text-2);--vp-button-sponsor-bg: transparent;--vp-button-sponsor-hover-border: var(--vp-c-sponsor);--vp-button-sponsor-hover-text: var(--vp-c-sponsor);--vp-button-sponsor-hover-bg: transparent;--vp-button-sponsor-active-border: var(--vp-c-sponsor);--vp-button-sponsor-active-text: var(--vp-c-sponsor);--vp-button-sponsor-active-bg: transparent}:root{--vp-custom-block-font-size: 14px;--vp-custom-block-code-font-size: 13px;--vp-custom-block-info-border: transparent;--vp-custom-block-info-text: var(--vp-c-text-1);--vp-custom-block-info-bg: var(--vp-c-default-soft);--vp-custom-block-info-code-bg: var(--vp-c-default-soft);--vp-custom-block-note-border: transparent;--vp-custom-block-note-text: var(--vp-c-text-1);--vp-custom-block-note-bg: var(--vp-c-default-soft);--vp-custom-block-note-code-bg: var(--vp-c-default-soft);--vp-custom-block-tip-border: transparent;--vp-custom-block-tip-text: var(--vp-c-text-1);--vp-custom-block-tip-bg: var(--vp-c-tip-soft);--vp-custom-block-tip-code-bg: var(--vp-c-tip-soft);--vp-custom-block-important-border: transparent;--vp-custom-block-important-text: var(--vp-c-text-1);--vp-custom-block-important-bg: var(--vp-c-important-soft);--vp-custom-block-important-code-bg: var(--vp-c-important-soft);--vp-custom-block-warning-border: transparent;--vp-custom-block-warning-text: var(--vp-c-text-1);--vp-custom-block-warning-bg: var(--vp-c-warning-soft);--vp-custom-block-warning-code-bg: var(--vp-c-warning-soft);--vp-custom-block-danger-border: transparent;--vp-custom-block-danger-text: var(--vp-c-text-1);--vp-custom-block-danger-bg: var(--vp-c-danger-soft);--vp-custom-block-danger-code-bg: var(--vp-c-danger-soft);--vp-custom-block-caution-border: transparent;--vp-custom-block-caution-text: var(--vp-c-text-1);--vp-custom-block-caution-bg: var(--vp-c-caution-soft);--vp-custom-block-caution-code-bg: var(--vp-c-caution-soft);--vp-custom-block-details-border: var(--vp-custom-block-info-border);--vp-custom-block-details-text: var(--vp-custom-block-info-text);--vp-custom-block-details-bg: var(--vp-custom-block-info-bg);--vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg)}:root{--vp-input-border-color: var(--vp-c-border);--vp-input-bg-color: var(--vp-c-bg-alt);--vp-input-switch-bg-color: var(--vp-c-default-soft)}:root{--vp-nav-height: 64px;--vp-nav-bg-color: var(--vp-c-bg);--vp-nav-screen-bg-color: var(--vp-c-bg);--vp-nav-logo-height: 24px}.hide-nav{--vp-nav-height: 0px}.hide-nav .VPSidebar{--vp-nav-height: 22px}:root{--vp-local-nav-bg-color: var(--vp-c-bg)}:root{--vp-sidebar-width: 272px;--vp-sidebar-bg-color: var(--vp-c-bg-alt)}:root{--vp-backdrop-bg-color: rgba(0, 0, 0, .6)}:root{--vp-home-hero-name-color: var(--vp-c-brand-1);--vp-home-hero-name-background: transparent;--vp-home-hero-image-background-image: none;--vp-home-hero-image-filter: none}:root{--vp-badge-info-border: transparent;--vp-badge-info-text: var(--vp-c-text-2);--vp-badge-info-bg: var(--vp-c-default-soft);--vp-badge-tip-border: transparent;--vp-badge-tip-text: var(--vp-c-tip-1);--vp-badge-tip-bg: var(--vp-c-tip-soft);--vp-badge-warning-border: transparent;--vp-badge-warning-text: var(--vp-c-warning-1);--vp-badge-warning-bg: var(--vp-c-warning-soft);--vp-badge-danger-border: transparent;--vp-badge-danger-text: var(--vp-c-danger-1);--vp-badge-danger-bg: var(--vp-c-danger-soft)}:root{--vp-carbon-ads-text-color: var(--vp-c-text-1);--vp-carbon-ads-poweredby-color: var(--vp-c-text-2);--vp-carbon-ads-bg-color: var(--vp-c-bg-soft);--vp-carbon-ads-hover-text-color: var(--vp-c-brand-1);--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1)}:root{--vp-local-search-bg: var(--vp-c-bg);--vp-local-search-result-bg: var(--vp-c-bg);--vp-local-search-result-border: var(--vp-c-divider);--vp-local-search-result-selected-bg: var(--vp-c-bg);--vp-local-search-result-selected-border: var(--vp-c-brand-1);--vp-local-search-highlight-bg: var(--vp-c-brand-1);--vp-local-search-highlight-text: var(--vp-c-neutral-inverse)}@media (prefers-reduced-motion: reduce){*,:before,:after{animation-delay:-1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important;background-attachment:initial!important;scroll-behavior:auto!important;transition-duration:0s!important;transition-delay:0s!important}}*,:before,:after{box-sizing:border-box}html{line-height:1.4;font-size:16px;-webkit-text-size-adjust:100%}html.dark{color-scheme:dark}body{margin:0;width:100%;min-width:320px;min-height:100vh;line-height:24px;font-family:var(--vp-font-family-base);font-size:16px;font-weight:400;color:var(--vp-c-text-1);background-color:var(--vp-c-bg);font-synthesis:style;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;line-height:24px;font-size:16px;font-weight:400}p{margin:0}strong,b{font-weight:600}a,area,button,[role=button],input,label,select,summary,textarea{touch-action:manipulation}a{color:inherit;text-decoration:inherit}ol,ul{list-style:none;margin:0;padding:0}blockquote{margin:0}pre,code,kbd,samp{font-family:var(--vp-font-family-mono)}img,svg,video,canvas,audio,iframe,embed,object{display:block}figure{margin:0}img,video{max-width:100%;height:auto}button,input,optgroup,select,textarea{border:0;padding:0;line-height:inherit;color:inherit}button{padding:0;font-family:inherit;background-color:transparent;background-image:none}button:enabled,[role=button]:enabled{cursor:pointer}button:focus,button:focus-visible{outline:1px dotted;outline:4px auto -webkit-focus-ring-color}button:focus:not(:focus-visible){outline:none!important}input:focus,textarea:focus,select:focus{outline:none}table{border-collapse:collapse}input{background-color:transparent}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:var(--vp-c-text-3)}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:var(--vp-c-text-3)}input::placeholder,textarea::placeholder{color:var(--vp-c-text-3)}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}textarea{resize:vertical}select{-webkit-appearance:none}fieldset{margin:0;padding:0}h1,h2,h3,h4,h5,h6,li,p{overflow-wrap:break-word}vite-error-overlay{z-index:9999}mjx-container{display:inline-block;margin:auto 2px -2px}mjx-container>svg{display:inline-block;margin:auto}[class^=vpi-],[class*=" vpi-"],.vp-icon{width:1em;height:1em}[class^=vpi-].bg,[class*=" vpi-"].bg,.vp-icon.bg{background-size:100% 100%;background-color:transparent}[class^=vpi-]:not(.bg),[class*=" vpi-"]:not(.bg),.vp-icon:not(.bg){-webkit-mask:var(--icon) no-repeat;mask:var(--icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit}.vpi-align-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M21 6H3M15 12H3M17 18H3'/%3E%3C/svg%3E")}.vpi-arrow-right,.vpi-arrow-down,.vpi-arrow-left,.vpi-arrow-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5l7 7-7 7'/%3E%3C/svg%3E")}.vpi-chevron-right,.vpi-chevron-down,.vpi-chevron-left,.vpi-chevron-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E")}.vpi-chevron-down,.vpi-arrow-down{transform:rotate(90deg)}.vpi-chevron-left,.vpi-arrow-left{transform:rotate(180deg)}.vpi-chevron-up,.vpi-arrow-up{transform:rotate(-90deg)}.vpi-square-pen{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/%3E%3Cpath d='M18.375 2.625a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4Z'/%3E%3C/svg%3E")}.vpi-plus{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5v14'/%3E%3C/svg%3E")}.vpi-sun{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E")}.vpi-moon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'/%3E%3C/svg%3E")}.vpi-more-horizontal{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/svg%3E")}.vpi-languages{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m5 8 6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6'/%3E%3C/svg%3E")}.vpi-heart{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E")}.vpi-search{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E")}.vpi-layout-list{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7M14 9h7M14 15h7M14 20h7'/%3E%3C/svg%3E")}.vpi-delete{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M20 5H9l-7 7 7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2ZM18 9l-6 6M12 9l6 6'/%3E%3C/svg%3E")}.vpi-corner-down-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 10-5 5 5 5'/%3E%3Cpath d='M20 4v7a4 4 0 0 1-4 4H4'/%3E%3C/svg%3E")}:root{--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E")}.vpi-social-discord{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418Z'/%3E%3C/svg%3E")}.vpi-social-facebook{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z'/%3E%3C/svg%3E")}.vpi-social-github{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")}.vpi-social-instagram{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.03.084c-1.277.06-2.149.264-2.91.563a5.874 5.874 0 0 0-2.124 1.388 5.878 5.878 0 0 0-1.38 2.127C.321 4.926.12 5.8.064 7.076.008 8.354-.005 8.764.001 12.023c.007 3.259.021 3.667.083 4.947.061 1.277.264 2.149.563 2.911.308.789.72 1.457 1.388 2.123a5.872 5.872 0 0 0 2.129 1.38c.763.295 1.636.496 2.913.552 1.278.056 1.689.069 4.947.063 3.257-.007 3.668-.021 4.947-.082 1.28-.06 2.147-.265 2.91-.563a5.881 5.881 0 0 0 2.123-1.388 5.881 5.881 0 0 0 1.38-2.129c.295-.763.496-1.636.551-2.912.056-1.28.07-1.69.063-4.948-.006-3.258-.02-3.667-.081-4.947-.06-1.28-.264-2.148-.564-2.911a5.892 5.892 0 0 0-1.387-2.123 5.857 5.857 0 0 0-2.128-1.38C19.074.322 18.202.12 16.924.066 15.647.009 15.236-.006 11.977 0 8.718.008 8.31.021 7.03.084m.14 21.693c-1.17-.05-1.805-.245-2.228-.408a3.736 3.736 0 0 1-1.382-.895 3.695 3.695 0 0 1-.9-1.378c-.165-.423-.363-1.058-.417-2.228-.06-1.264-.072-1.644-.08-4.848-.006-3.204.006-3.583.061-4.848.05-1.169.246-1.805.408-2.228.216-.561.477-.96.895-1.382a3.705 3.705 0 0 1 1.379-.9c.423-.165 1.057-.361 2.227-.417 1.265-.06 1.644-.072 4.848-.08 3.203-.006 3.583.006 4.85.062 1.168.05 1.804.244 2.227.408.56.216.96.475 1.382.895.421.42.681.817.9 1.378.165.422.362 1.056.417 2.227.06 1.265.074 1.645.08 4.848.005 3.203-.006 3.583-.061 4.848-.051 1.17-.245 1.805-.408 2.23-.216.56-.477.96-.896 1.38a3.705 3.705 0 0 1-1.378.9c-.422.165-1.058.362-2.226.418-1.266.06-1.645.072-4.85.079-3.204.007-3.582-.006-4.848-.06m9.783-16.192a1.44 1.44 0 1 0 1.437-1.442 1.44 1.44 0 0 0-1.437 1.442M5.839 12.012a6.161 6.161 0 1 0 12.323-.024 6.162 6.162 0 0 0-12.323.024M8 12.008A4 4 0 1 1 12.008 16 4 4 0 0 1 8 12.008'/%3E%3C/svg%3E")}.vpi-social-linkedin{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z'/%3E%3C/svg%3E")}.vpi-social-mastodon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z'/%3E%3C/svg%3E")}.vpi-social-npm{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z'/%3E%3C/svg%3E")}.vpi-social-slack{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zm10.122 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.268 0a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zm-2.523 10.122a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zm0-1.268a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z'/%3E%3C/svg%3E")}.vpi-social-twitter,.vpi-social-x{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z'/%3E%3C/svg%3E")}.vpi-social-youtube{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z'/%3E%3C/svg%3E")}.visually-hidden{position:absolute;width:1px;height:1px;white-space:nowrap;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden}.custom-block{border:1px solid transparent;border-radius:8px;padding:16px 16px 8px;line-height:24px;font-size:var(--vp-custom-block-font-size);color:var(--vp-c-text-2)}.custom-block.info{border-color:var(--vp-custom-block-info-border);color:var(--vp-custom-block-info-text);background-color:var(--vp-custom-block-info-bg)}.custom-block.info a,.custom-block.info code{color:var(--vp-c-brand-1)}.custom-block.info a:hover,.custom-block.info a:hover>code{color:var(--vp-c-brand-2)}.custom-block.info code{background-color:var(--vp-custom-block-info-code-bg)}.custom-block.note{border-color:var(--vp-custom-block-note-border);color:var(--vp-custom-block-note-text);background-color:var(--vp-custom-block-note-bg)}.custom-block.note a,.custom-block.note code{color:var(--vp-c-brand-1)}.custom-block.note a:hover,.custom-block.note a:hover>code{color:var(--vp-c-brand-2)}.custom-block.note code{background-color:var(--vp-custom-block-note-code-bg)}.custom-block.tip{border-color:var(--vp-custom-block-tip-border);color:var(--vp-custom-block-tip-text);background-color:var(--vp-custom-block-tip-bg)}.custom-block.tip a,.custom-block.tip code{color:var(--vp-c-tip-1)}.custom-block.tip a:hover,.custom-block.tip a:hover>code{color:var(--vp-c-tip-2)}.custom-block.tip code{background-color:var(--vp-custom-block-tip-code-bg)}.custom-block.important{border-color:var(--vp-custom-block-important-border);color:var(--vp-custom-block-important-text);background-color:var(--vp-custom-block-important-bg)}.custom-block.important a,.custom-block.important code{color:var(--vp-c-important-1)}.custom-block.important a:hover,.custom-block.important a:hover>code{color:var(--vp-c-important-2)}.custom-block.important code{background-color:var(--vp-custom-block-important-code-bg)}.custom-block.warning{border-color:var(--vp-custom-block-warning-border);color:var(--vp-custom-block-warning-text);background-color:var(--vp-custom-block-warning-bg)}.custom-block.warning a,.custom-block.warning code{color:var(--vp-c-warning-1)}.custom-block.warning a:hover,.custom-block.warning a:hover>code{color:var(--vp-c-warning-2)}.custom-block.warning code{background-color:var(--vp-custom-block-warning-code-bg)}.custom-block.danger{border-color:var(--vp-custom-block-danger-border);color:var(--vp-custom-block-danger-text);background-color:var(--vp-custom-block-danger-bg)}.custom-block.danger a,.custom-block.danger code{color:var(--vp-c-danger-1)}.custom-block.danger a:hover,.custom-block.danger a:hover>code{color:var(--vp-c-danger-2)}.custom-block.danger code{background-color:var(--vp-custom-block-danger-code-bg)}.custom-block.caution{border-color:var(--vp-custom-block-caution-border);color:var(--vp-custom-block-caution-text);background-color:var(--vp-custom-block-caution-bg)}.custom-block.caution a,.custom-block.caution code{color:var(--vp-c-caution-1)}.custom-block.caution a:hover,.custom-block.caution a:hover>code{color:var(--vp-c-caution-2)}.custom-block.caution code{background-color:var(--vp-custom-block-caution-code-bg)}.custom-block.details{border-color:var(--vp-custom-block-details-border);color:var(--vp-custom-block-details-text);background-color:var(--vp-custom-block-details-bg)}.custom-block.details a{color:var(--vp-c-brand-1)}.custom-block.details a:hover,.custom-block.details a:hover>code{color:var(--vp-c-brand-2)}.custom-block.details code{background-color:var(--vp-custom-block-details-code-bg)}.custom-block-title{font-weight:600}.custom-block p+p{margin:8px 0}.custom-block.details summary{margin:0 0 8px;font-weight:700;cursor:pointer;-webkit-user-select:none;user-select:none}.custom-block.details summary+p{margin:8px 0}.custom-block a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px;transition:opacity .25s}.custom-block a:hover{opacity:.75}.custom-block code{font-size:var(--vp-custom-block-code-font-size)}.custom-block.custom-block th,.custom-block.custom-block blockquote>p{font-size:var(--vp-custom-block-font-size);color:inherit}.dark .vp-code span{color:var(--shiki-dark, inherit)}html:not(.dark) .vp-code span{color:var(--shiki-light, inherit)}.vp-code-group{margin-top:16px}.vp-code-group .tabs{position:relative;display:flex;margin-right:-24px;margin-left:-24px;padding:0 12px;background-color:var(--vp-code-tab-bg);overflow-x:auto;overflow-y:hidden;box-shadow:inset 0 -1px var(--vp-code-tab-divider)}@media (min-width: 640px){.vp-code-group .tabs{margin-right:0;margin-left:0;border-radius:8px 8px 0 0}}.vp-code-group .tabs input{position:fixed;opacity:0;pointer-events:none}.vp-code-group .tabs label{position:relative;display:inline-block;border-bottom:1px solid transparent;padding:0 12px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-code-tab-text-color);white-space:nowrap;cursor:pointer;transition:color .25s}.vp-code-group .tabs label:after{position:absolute;right:8px;bottom:-1px;left:8px;z-index:1;height:2px;border-radius:2px;content:"";background-color:transparent;transition:background-color .25s}.vp-code-group label:hover{color:var(--vp-code-tab-hover-text-color)}.vp-code-group input:checked+label{color:var(--vp-code-tab-active-text-color)}.vp-code-group input:checked+label:after{background-color:var(--vp-code-tab-active-bar-color)}.vp-code-group div[class*=language-],.vp-block{display:none;margin-top:0!important;border-top-left-radius:0!important;border-top-right-radius:0!important}.vp-code-group div[class*=language-].active,.vp-block.active{display:block}.vp-block{padding:20px 24px}.vp-doc h1,.vp-doc h2,.vp-doc h3,.vp-doc h4,.vp-doc h5,.vp-doc h6{position:relative;font-weight:600;outline:none}.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:28px}.vp-doc h2{margin:48px 0 16px;border-top:1px solid var(--vp-c-divider);padding-top:24px;letter-spacing:-.02em;line-height:32px;font-size:24px}.vp-doc h3{margin:32px 0 0;letter-spacing:-.01em;line-height:28px;font-size:20px}.vp-doc .header-anchor{position:absolute;top:0;left:0;margin-left:-.87em;font-weight:500;-webkit-user-select:none;user-select:none;opacity:0;text-decoration:none;transition:color .25s,opacity .25s}.vp-doc .header-anchor:before{content:var(--vp-header-anchor-symbol)}.vp-doc h1:hover .header-anchor,.vp-doc h1 .header-anchor:focus,.vp-doc h2:hover .header-anchor,.vp-doc h2 .header-anchor:focus,.vp-doc h3:hover .header-anchor,.vp-doc h3 .header-anchor:focus,.vp-doc h4:hover .header-anchor,.vp-doc h4 .header-anchor:focus,.vp-doc h5:hover .header-anchor,.vp-doc h5 .header-anchor:focus,.vp-doc h6:hover .header-anchor,.vp-doc h6 .header-anchor:focus{opacity:1}@media (min-width: 768px){.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:32px}}.vp-doc h2 .header-anchor{top:24px}.vp-doc p,.vp-doc summary{margin:16px 0}.vp-doc p{line-height:28px}.vp-doc blockquote{margin:16px 0;border-left:2px solid var(--vp-c-divider);padding-left:16px;transition:border-color .5s}.vp-doc blockquote>p{margin:0;font-size:16px;color:var(--vp-c-text-2);transition:color .5s}.vp-doc a{font-weight:500;color:var(--vp-c-brand-1);text-decoration:underline;text-underline-offset:2px;transition:color .25s,opacity .25s}.vp-doc a:hover{color:var(--vp-c-brand-2)}.vp-doc strong{font-weight:600}.vp-doc ul,.vp-doc ol{padding-left:1.25rem;margin:16px 0}.vp-doc ul{list-style:disc}.vp-doc ol{list-style:decimal}.vp-doc li+li{margin-top:8px}.vp-doc li>ol,.vp-doc li>ul{margin:8px 0 0}.vp-doc table{display:block;border-collapse:collapse;margin:20px 0;overflow-x:auto}.vp-doc tr{background-color:var(--vp-c-bg);border-top:1px solid var(--vp-c-divider);transition:background-color .5s}.vp-doc tr:nth-child(2n){background-color:var(--vp-c-bg-soft)}.vp-doc th,.vp-doc td{border:1px solid var(--vp-c-divider);padding:8px 16px}.vp-doc th{text-align:left;font-size:14px;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-doc td{font-size:14px}.vp-doc hr{margin:16px 0;border:none;border-top:1px solid var(--vp-c-divider)}.vp-doc .custom-block{margin:16px 0}.vp-doc .custom-block p{margin:8px 0;line-height:24px}.vp-doc .custom-block p:first-child{margin:0}.vp-doc .custom-block div[class*=language-]{margin:8px 0;border-radius:8px}.vp-doc .custom-block div[class*=language-] code{font-weight:400;background-color:transparent}.vp-doc .custom-block .vp-code-group .tabs{margin:0;border-radius:8px 8px 0 0}.vp-doc :not(pre,h1,h2,h3,h4,h5,h6)>code{font-size:var(--vp-code-font-size);color:var(--vp-code-color)}.vp-doc :not(pre)>code{border-radius:4px;padding:3px 6px;background-color:var(--vp-code-bg);transition:color .25s,background-color .5s}.vp-doc a>code{color:var(--vp-code-link-color)}.vp-doc a:hover>code{color:var(--vp-code-link-hover-color)}.vp-doc h1>code,.vp-doc h2>code,.vp-doc h3>code{font-size:.9em}.vp-doc div[class*=language-],.vp-block{position:relative;margin:16px -24px;background-color:var(--vp-code-block-bg);overflow-x:auto;transition:background-color .5s}@media (min-width: 640px){.vp-doc div[class*=language-],.vp-block{border-radius:8px;margin:16px 0}}@media (max-width: 639px){.vp-doc li div[class*=language-]{border-radius:8px 0 0 8px}}.vp-doc div[class*=language-]+div[class*=language-],.vp-doc div[class$=-api]+div[class*=language-],.vp-doc div[class*=language-]+div[class$=-api]>div[class*=language-]{margin-top:-8px}.vp-doc [class*=language-] pre,.vp-doc [class*=language-] code{direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.vp-doc [class*=language-] pre{position:relative;z-index:1;margin:0;padding:20px 0;background:transparent;overflow-x:auto}.vp-doc [class*=language-] code{display:block;padding:0 24px;width:fit-content;min-width:100%;line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-block-color);transition:color .5s}.vp-doc [class*=language-] code .highlighted{background-color:var(--vp-code-line-highlight-color);transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .highlighted.error{background-color:var(--vp-code-line-error-color)}.vp-doc [class*=language-] code .highlighted.warning{background-color:var(--vp-code-line-warning-color)}.vp-doc [class*=language-] code .diff{transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .diff:before{position:absolute;left:10px}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){filter:blur(.095rem);opacity:.4;transition:filter .35s,opacity .35s}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){opacity:.7;transition:filter .35s,opacity .35s}.vp-doc [class*=language-]:hover .has-focused-lines .line:not(.has-focus){filter:blur(0);opacity:1}.vp-doc [class*=language-] code .diff.remove{background-color:var(--vp-code-line-diff-remove-color);opacity:.7}.vp-doc [class*=language-] code .diff.remove:before{content:"-";color:var(--vp-code-line-diff-remove-symbol-color)}.vp-doc [class*=language-] code .diff.add{background-color:var(--vp-code-line-diff-add-color)}.vp-doc [class*=language-] code .diff.add:before{content:"+";color:var(--vp-code-line-diff-add-symbol-color)}.vp-doc div[class*=language-].line-numbers-mode{padding-left:32px}.vp-doc .line-numbers-wrapper{position:absolute;top:0;bottom:0;left:0;z-index:3;border-right:1px solid var(--vp-code-block-divider-color);padding-top:20px;width:32px;text-align:center;font-family:var(--vp-font-family-mono);line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-line-number-color);transition:border-color .5s,color .5s}.vp-doc [class*=language-]>button.copy{direction:ltr;position:absolute;top:12px;right:12px;z-index:3;border:1px solid var(--vp-code-copy-code-border-color);border-radius:4px;width:40px;height:40px;background-color:var(--vp-code-copy-code-bg);opacity:0;cursor:pointer;background-image:var(--vp-icon-copy);background-position:50%;background-size:20px;background-repeat:no-repeat;transition:border-color .25s,background-color .25s,opacity .25s}.vp-doc [class*=language-]:hover>button.copy,.vp-doc [class*=language-]>button.copy:focus{opacity:1}.vp-doc [class*=language-]>button.copy:hover,.vp-doc [class*=language-]>button.copy.copied{border-color:var(--vp-code-copy-code-hover-border-color);background-color:var(--vp-code-copy-code-hover-bg)}.vp-doc [class*=language-]>button.copy.copied,.vp-doc [class*=language-]>button.copy:hover.copied{border-radius:0 4px 4px 0;background-color:var(--vp-code-copy-code-hover-bg);background-image:var(--vp-icon-copied)}.vp-doc [class*=language-]>button.copy.copied:before,.vp-doc [class*=language-]>button.copy:hover.copied:before{position:relative;top:-1px;transform:translate(calc(-100% - 1px));display:flex;justify-content:center;align-items:center;border:1px solid var(--vp-code-copy-code-hover-border-color);border-right:0;border-radius:4px 0 0 4px;padding:0 10px;width:fit-content;height:40px;text-align:center;font-size:12px;font-weight:500;color:var(--vp-code-copy-code-active-text);background-color:var(--vp-code-copy-code-hover-bg);white-space:nowrap;content:var(--vp-code-copy-copied-text-content)}.vp-doc [class*=language-]>span.lang{position:absolute;top:2px;right:8px;z-index:2;font-size:12px;font-weight:500;color:var(--vp-code-lang-color);transition:color .4s,opacity .4s}.vp-doc [class*=language-]:hover>button.copy+span.lang,.vp-doc [class*=language-]>button.copy:focus+span.lang{opacity:0}.vp-doc .VPTeamMembers{margin-top:24px}.vp-doc .VPTeamMembers.small.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}.vp-doc .VPTeamMembers.small.count-2 .container,.vp-doc .VPTeamMembers.small.count-3 .container{max-width:100%!important}.vp-doc .VPTeamMembers.medium.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}:is(.vp-external-link-icon,.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(.no-icon):after{display:inline-block;margin-top:-1px;margin-left:4px;width:11px;height:11px;background:currentColor;color:var(--vp-c-text-3);flex-shrink:0;--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");-webkit-mask-image:var(--icon);mask-image:var(--icon)}.vp-external-link-icon:after{content:""}.external-link-icon-enabled :is(.vp-doc a[href*="://"],.vp-doc a[target=_blank]):after{content:"";color:currentColor}.vp-sponsor{border-radius:16px;overflow:hidden}.vp-sponsor.aside{border-radius:12px}.vp-sponsor-section+.vp-sponsor-section{margin-top:4px}.vp-sponsor-tier{margin:0 0 4px!important;text-align:center;letter-spacing:1px!important;line-height:24px;width:100%;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-sponsor.normal .vp-sponsor-tier{padding:13px 0 11px;font-size:14px}.vp-sponsor.aside .vp-sponsor-tier{padding:9px 0 7px;font-size:12px}.vp-sponsor-grid+.vp-sponsor-tier{margin-top:4px}.vp-sponsor-grid{display:flex;flex-wrap:wrap;gap:4px}.vp-sponsor-grid.xmini .vp-sponsor-grid-link{height:64px}.vp-sponsor-grid.xmini .vp-sponsor-grid-image{max-width:64px;max-height:22px}.vp-sponsor-grid.mini .vp-sponsor-grid-link{height:72px}.vp-sponsor-grid.mini .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.small .vp-sponsor-grid-link{height:96px}.vp-sponsor-grid.small .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.medium .vp-sponsor-grid-link{height:112px}.vp-sponsor-grid.medium .vp-sponsor-grid-image{max-width:120px;max-height:36px}.vp-sponsor-grid.big .vp-sponsor-grid-link{height:184px}.vp-sponsor-grid.big .vp-sponsor-grid-image{max-width:192px;max-height:56px}.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item{width:calc((100% - 4px)/2)}.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item{width:calc((100% - 4px * 2) / 3)}.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item{width:calc((100% - 12px)/4)}.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item{width:calc((100% - 16px)/5)}.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item{width:calc((100% - 4px * 5) / 6)}.vp-sponsor-grid-item{flex-shrink:0;width:100%;background-color:var(--vp-c-bg-soft);transition:background-color .25s}.vp-sponsor-grid-item:hover{background-color:var(--vp-c-default-soft)}.vp-sponsor-grid-item:hover .vp-sponsor-grid-image{filter:grayscale(0) invert(0)}.vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.dark .vp-sponsor-grid-item:hover{background-color:var(--vp-c-white)}.dark .vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.vp-sponsor-grid-link{display:flex}.vp-sponsor-grid-box{display:flex;justify-content:center;align-items:center;width:100%}.vp-sponsor-grid-image{max-width:100%;filter:grayscale(1);transition:filter .25s}.dark .vp-sponsor-grid-image{filter:grayscale(1) invert(1)}.VPBadge{display:inline-block;margin-left:2px;border:1px solid transparent;border-radius:12px;padding:0 10px;line-height:22px;font-size:12px;font-weight:500;transform:translateY(-2px)}.VPBadge.small{padding:0 6px;line-height:18px;font-size:10px;transform:translateY(-8px)}.VPDocFooter .VPBadge{display:none}.vp-doc h1>.VPBadge{margin-top:4px;vertical-align:top}.vp-doc h2>.VPBadge{margin-top:3px;padding:0 8px;vertical-align:top}.vp-doc h3>.VPBadge{vertical-align:middle}.vp-doc h4>.VPBadge,.vp-doc h5>.VPBadge,.vp-doc h6>.VPBadge{vertical-align:middle;line-height:18px}.VPBadge.info{border-color:var(--vp-badge-info-border);color:var(--vp-badge-info-text);background-color:var(--vp-badge-info-bg)}.VPBadge.tip{border-color:var(--vp-badge-tip-border);color:var(--vp-badge-tip-text);background-color:var(--vp-badge-tip-bg)}.VPBadge.warning{border-color:var(--vp-badge-warning-border);color:var(--vp-badge-warning-text);background-color:var(--vp-badge-warning-bg)}.VPBadge.danger{border-color:var(--vp-badge-danger-border);color:var(--vp-badge-danger-text);background-color:var(--vp-badge-danger-bg)}.VPBackdrop[data-v-c79a1216]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--vp-z-index-backdrop);background:var(--vp-backdrop-bg-color);transition:opacity .5s}.VPBackdrop.fade-enter-from[data-v-c79a1216],.VPBackdrop.fade-leave-to[data-v-c79a1216]{opacity:0}.VPBackdrop.fade-leave-active[data-v-c79a1216]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-c79a1216]{display:none}}.NotFound[data-v-f87ff6e4]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-f87ff6e4]{padding:96px 32px 168px}}.code[data-v-f87ff6e4]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-f87ff6e4]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-f87ff6e4]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-f87ff6e4]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-f87ff6e4]{padding-top:20px}.link[data-v-f87ff6e4]{display:inline-block;border:1px solid var(--vp-c-brand-1);border-radius:16px;padding:3px 16px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:border-color .25s,color .25s}.link[data-v-f87ff6e4]:hover{border-color:var(--vp-c-brand-2);color:var(--vp-c-brand-2)}.root[data-v-b933a997]{position:relative;z-index:1}.nested[data-v-b933a997]{padding-right:16px;padding-left:16px}.outline-link[data-v-b933a997]{display:block;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .5s}.outline-link[data-v-b933a997]:hover,.outline-link.active[data-v-b933a997]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-b933a997]{padding-left:13px}.VPDocAsideOutline[data-v-935f8a84]{display:none}.VPDocAsideOutline.has-outline[data-v-935f8a84]{display:block}.content[data-v-935f8a84]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-935f8a84]{position:absolute;top:32px;left:-1px;z-index:0;opacity:0;width:2px;border-radius:2px;height:18px;background-color:var(--vp-c-brand-1);transition:top .25s cubic-bezier(0,1,.5,1),background-color .5s,opacity .25s}.outline-title[data-v-935f8a84]{line-height:32px;font-size:14px;font-weight:600}.VPDocAside[data-v-3f215769]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-3f215769]{flex-grow:1}.VPDocAside[data-v-3f215769] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-3f215769] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-3f215769] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-7e05ebdb]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-7e05ebdb]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-09de1c0f]{margin-top:64px}.edit-info[data-v-09de1c0f]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-09de1c0f]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-09de1c0f]{display:flex;align-items:center;border:0;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.edit-link-button[data-v-09de1c0f]:hover{color:var(--vp-c-brand-2)}.edit-link-icon[data-v-09de1c0f]{margin-right:8px}.prev-next[data-v-09de1c0f]{border-top:1px solid var(--vp-c-divider);padding-top:24px;display:grid;grid-row-gap:8px}@media (min-width: 640px){.prev-next[data-v-09de1c0f]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-09de1c0f]{display:block;border:1px solid var(--vp-c-divider);border-radius:8px;padding:11px 16px 13px;width:100%;height:100%;transition:border-color .25s}.pager-link[data-v-09de1c0f]:hover{border-color:var(--vp-c-brand-1)}.pager-link.next[data-v-09de1c0f]{margin-left:auto;text-align:right}.desc[data-v-09de1c0f]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-09de1c0f]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.VPDoc[data-v-39a288b8]{padding:32px 24px 96px;width:100%}@media (min-width: 768px){.VPDoc[data-v-39a288b8]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-39a288b8]{padding:48px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-39a288b8]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-39a288b8]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-39a288b8]{display:flex;justify-content:center}.VPDoc .aside[data-v-39a288b8]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-39a288b8]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-39a288b8]{max-width:1104px}}.container[data-v-39a288b8]{margin:0 auto;width:100%}.aside[data-v-39a288b8]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-39a288b8]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-39a288b8]{position:fixed;top:0;padding-top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);width:224px;height:100vh;overflow-x:hidden;overflow-y:auto;scrollbar-width:none}.aside-container[data-v-39a288b8]::-webkit-scrollbar{display:none}.aside-curtain[data-v-39a288b8]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-39a288b8]{display:flex;flex-direction:column;min-height:calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));padding-bottom:32px}.content[data-v-39a288b8]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-39a288b8]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-39a288b8]{order:1;margin:0;min-width:640px}}.content-container[data-v-39a288b8]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-39a288b8]{max-width:688px}.VPButton[data-v-cad61b99]{display:inline-block;border:1px solid transparent;text-align:center;font-weight:600;white-space:nowrap;transition:color .25s,border-color .25s,background-color .25s}.VPButton[data-v-cad61b99]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-cad61b99]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-cad61b99]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-cad61b99]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-cad61b99]:hover{border-color:var(--vp-button-brand-hover-border);color:var(--vp-button-brand-hover-text);background-color:var(--vp-button-brand-hover-bg)}.VPButton.brand[data-v-cad61b99]:active{border-color:var(--vp-button-brand-active-border);color:var(--vp-button-brand-active-text);background-color:var(--vp-button-brand-active-bg)}.VPButton.alt[data-v-cad61b99]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-cad61b99]:hover{border-color:var(--vp-button-alt-hover-border);color:var(--vp-button-alt-hover-text);background-color:var(--vp-button-alt-hover-bg)}.VPButton.alt[data-v-cad61b99]:active{border-color:var(--vp-button-alt-active-border);color:var(--vp-button-alt-active-text);background-color:var(--vp-button-alt-active-bg)}.VPButton.sponsor[data-v-cad61b99]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-cad61b99]:hover{border-color:var(--vp-button-sponsor-hover-border);color:var(--vp-button-sponsor-hover-text);background-color:var(--vp-button-sponsor-hover-bg)}.VPButton.sponsor[data-v-cad61b99]:active{border-color:var(--vp-button-sponsor-active-border);color:var(--vp-button-sponsor-active-text);background-color:var(--vp-button-sponsor-active-bg)}html:not(.dark) .VPImage.dark[data-v-8426fc1a]{display:none}.dark .VPImage.light[data-v-8426fc1a]{display:none}.VPHero[data-v-303bb580]{margin-top:calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px}@media (min-width: 640px){.VPHero[data-v-303bb580]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-303bb580]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-303bb580]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-303bb580]{flex-direction:row}}.main[data-v-303bb580]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-303bb580]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-303bb580]{text-align:left}}@media (min-width: 960px){.main[data-v-303bb580]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-303bb580]{max-width:592px}}.name[data-v-303bb580],.text[data-v-303bb580]{max-width:392px;letter-spacing:-.4px;line-height:40px;font-size:32px;font-weight:700;white-space:pre-wrap}.VPHero.has-image .name[data-v-303bb580],.VPHero.has-image .text[data-v-303bb580]{margin:0 auto}.name[data-v-303bb580]{color:var(--vp-home-hero-name-color)}.clip[data-v-303bb580]{background:var(--vp-home-hero-name-background);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:var(--vp-home-hero-name-color)}@media (min-width: 640px){.name[data-v-303bb580],.text[data-v-303bb580]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-303bb580],.text[data-v-303bb580]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-303bb580],.VPHero.has-image .text[data-v-303bb580]{margin:0}}.tagline[data-v-303bb580]{padding-top:8px;max-width:392px;line-height:28px;font-size:18px;font-weight:500;white-space:pre-wrap;color:var(--vp-c-text-2)}.VPHero.has-image .tagline[data-v-303bb580]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-303bb580]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-303bb580]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-303bb580]{margin:0}}.actions[data-v-303bb580]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-303bb580]{justify-content:center}@media (min-width: 640px){.actions[data-v-303bb580]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-303bb580]{justify-content:flex-start}}.action[data-v-303bb580]{flex-shrink:0;padding:6px}.image[data-v-303bb580]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-303bb580]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-303bb580]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-303bb580]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-303bb580]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-303bb580]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-303bb580]{position:absolute;top:50%;left:50%;border-radius:50%;width:192px;height:192px;background-image:var(--vp-home-hero-image-background-image);filter:var(--vp-home-hero-image-filter);transform:translate(-50%,-50%)}@media (min-width: 640px){.image-bg[data-v-303bb580]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-303bb580]{width:320px;height:320px}}[data-v-303bb580] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-303bb580] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-303bb580] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-a3976bdc]{display:block;border:1px solid var(--vp-c-bg-soft);border-radius:12px;height:100%;background-color:var(--vp-c-bg-soft);transition:border-color .25s,background-color .25s}.VPFeature.link[data-v-a3976bdc]:hover{border-color:var(--vp-c-brand-1)}.box[data-v-a3976bdc]{display:flex;flex-direction:column;padding:24px;height:100%}.box[data-v-a3976bdc]>.VPImage{margin-bottom:20px}.icon[data-v-a3976bdc]{display:flex;justify-content:center;align-items:center;margin-bottom:20px;border-radius:6px;background-color:var(--vp-c-default-soft);width:48px;height:48px;font-size:24px;transition:background-color .25s}.title[data-v-a3976bdc]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-a3976bdc]{flex-grow:1;padding-top:8px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.link-text[data-v-a3976bdc]{padding-top:8px}.link-text-value[data-v-a3976bdc]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.link-text-icon[data-v-a3976bdc]{margin-left:6px}.VPFeatures[data-v-a6181336]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-a6181336]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-a6181336]{padding:0 64px}}.container[data-v-a6181336]{margin:0 auto;max-width:1152px}.items[data-v-a6181336]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-a6181336]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-a6181336],.item.grid-4[data-v-a6181336],.item.grid-6[data-v-a6181336]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-a6181336],.item.grid-4[data-v-a6181336]{width:50%}.item.grid-3[data-v-a6181336],.item.grid-6[data-v-a6181336]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-a6181336]{width:25%}}.container[data-v-82d4af08]{margin:auto;width:100%;max-width:1280px;padding:0 24px}@media (min-width: 640px){.container[data-v-82d4af08]{padding:0 48px}}@media (min-width: 960px){.container[data-v-82d4af08]{width:100%;padding:0 64px}}.vp-doc[data-v-82d4af08] .VPHomeSponsors,.vp-doc[data-v-82d4af08] .VPTeamPage{margin-left:var(--vp-offset, calc(50% - 50vw) );margin-right:var(--vp-offset, calc(50% - 50vw) )}.vp-doc[data-v-82d4af08] .VPHomeSponsors h2{border-top:none;letter-spacing:normal}.vp-doc[data-v-82d4af08] .VPHomeSponsors a,.vp-doc[data-v-82d4af08] .VPTeamPage a{text-decoration:none}.VPHome[data-v-686f80a6]{margin-bottom:96px}@media (min-width: 768px){.VPHome[data-v-686f80a6]{margin-bottom:128px}}.VPContent[data-v-1428d186]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-1428d186]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-1428d186]{margin:0}@media (min-width: 960px){.VPContent[data-v-1428d186]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-1428d186]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-1428d186]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.VPFooter[data-v-e315a0ad]{position:relative;z-index:var(--vp-z-index-footer);border-top:1px solid var(--vp-c-gutter);padding:32px 24px;background-color:var(--vp-c-bg)}.VPFooter.has-sidebar[data-v-e315a0ad]{display:none}.VPFooter[data-v-e315a0ad] a{text-decoration-line:underline;text-underline-offset:2px;transition:color .25s}.VPFooter[data-v-e315a0ad] a:hover{color:var(--vp-c-text-1)}@media (min-width: 768px){.VPFooter[data-v-e315a0ad]{padding:32px}}.container[data-v-e315a0ad]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-e315a0ad],.copyright[data-v-e315a0ad]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-d2ecc192]{padding:12px 20px 11px}@media (min-width: 960px){.VPLocalNavOutlineDropdown[data-v-d2ecc192]{padding:12px 36px 11px}}.VPLocalNavOutlineDropdown button[data-v-d2ecc192]{display:block;font-size:12px;font-weight:500;line-height:24px;color:var(--vp-c-text-2);transition:color .5s;position:relative}.VPLocalNavOutlineDropdown button[data-v-d2ecc192]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-d2ecc192]{color:var(--vp-c-text-1)}.icon[data-v-d2ecc192]{display:inline-block;vertical-align:middle;margin-left:2px;font-size:14px;transform:rotate(0);transition:transform .25s}@media (min-width: 960px){.VPLocalNavOutlineDropdown button[data-v-d2ecc192]{font-size:14px}.icon[data-v-d2ecc192]{font-size:16px}}.open>.icon[data-v-d2ecc192]{transform:rotate(90deg)}.items[data-v-d2ecc192]{position:absolute;top:40px;right:16px;left:16px;display:grid;gap:1px;border:1px solid var(--vp-c-border);border-radius:8px;background-color:var(--vp-c-gutter);max-height:calc(var(--vp-vh, 100vh) - 86px);overflow:hidden auto;box-shadow:var(--vp-shadow-3)}@media (min-width: 960px){.items[data-v-d2ecc192]{right:auto;left:calc(var(--vp-sidebar-width) + 32px);width:320px}}.header[data-v-d2ecc192]{background-color:var(--vp-c-bg-soft)}.top-link[data-v-d2ecc192]{display:block;padding:0 16px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.outline[data-v-d2ecc192]{padding:8px 0;background-color:var(--vp-c-bg-soft)}.flyout-enter-active[data-v-d2ecc192]{transition:all .2s ease-out}.flyout-leave-active[data-v-d2ecc192]{transition:all .15s ease-in}.flyout-enter-from[data-v-d2ecc192],.flyout-leave-to[data-v-d2ecc192]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-a6f0e41e]{position:sticky;top:0;left:0;z-index:var(--vp-z-index-local-nav);border-bottom:1px solid var(--vp-c-gutter);padding-top:var(--vp-layout-top-height, 0px);width:100%;background-color:var(--vp-local-nav-bg-color)}.VPLocalNav.fixed[data-v-a6f0e41e]{position:fixed}@media (min-width: 960px){.VPLocalNav[data-v-a6f0e41e]{top:var(--vp-nav-height)}.VPLocalNav.has-sidebar[data-v-a6f0e41e]{padding-left:var(--vp-sidebar-width)}.VPLocalNav.empty[data-v-a6f0e41e]{display:none}}@media (min-width: 1280px){.VPLocalNav[data-v-a6f0e41e]{display:none}}@media (min-width: 1440px){.VPLocalNav.has-sidebar[data-v-a6f0e41e]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.container[data-v-a6f0e41e]{display:flex;justify-content:space-between;align-items:center}.menu[data-v-a6f0e41e]{display:flex;align-items:center;padding:12px 24px 11px;line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.menu[data-v-a6f0e41e]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-a6f0e41e]{padding:0 32px}}@media (min-width: 960px){.menu[data-v-a6f0e41e]{display:none}}.menu-icon[data-v-a6f0e41e]{margin-right:8px;font-size:14px}.VPOutlineDropdown[data-v-a6f0e41e]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-a6f0e41e]{padding:12px 32px 11px}}.VPSwitch[data-v-1d5665e3]{position:relative;border-radius:11px;display:block;width:40px;height:22px;flex-shrink:0;border:1px solid var(--vp-input-border-color);background-color:var(--vp-input-switch-bg-color);transition:border-color .25s!important}.VPSwitch[data-v-1d5665e3]:hover{border-color:var(--vp-c-brand-1)}.check[data-v-1d5665e3]{position:absolute;top:1px;left:1px;width:18px;height:18px;border-radius:50%;background-color:var(--vp-c-neutral-inverse);box-shadow:var(--vp-shadow-1);transition:transform .25s!important}.icon[data-v-1d5665e3]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-1d5665e3] [class^=vpi-]{position:absolute;top:3px;left:3px;width:12px;height:12px;color:var(--vp-c-text-2)}.dark .icon[data-v-1d5665e3] [class^=vpi-]{color:var(--vp-c-text-1);transition:opacity .25s!important}.sun[data-v-d1f28634]{opacity:1}.moon[data-v-d1f28634],.dark .sun[data-v-d1f28634]{opacity:0}.dark .moon[data-v-d1f28634]{opacity:1}.dark .VPSwitchAppearance[data-v-d1f28634] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-e6aabb21]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-e6aabb21]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-43f1e123]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-43f1e123]{display:block;border-radius:6px;padding:0 12px;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);white-space:nowrap;transition:background-color .25s,color .25s}.link[data-v-43f1e123]:hover{color:var(--vp-c-brand-1);background-color:var(--vp-c-default-soft)}.link.active[data-v-43f1e123]{color:var(--vp-c-brand-1)}.VPMenuGroup[data-v-69e747b5]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-69e747b5]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-69e747b5]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-69e747b5]{padding:0 12px;line-height:32px;font-size:14px;font-weight:600;color:var(--vp-c-text-2);white-space:nowrap;transition:color .25s}.VPMenu[data-v-e7ea1737]{border-radius:12px;padding:12px;min-width:128px;border:1px solid var(--vp-c-divider);background-color:var(--vp-c-bg-elv);box-shadow:var(--vp-shadow-3);transition:background-color .5s;max-height:calc(100vh - var(--vp-nav-height));overflow-y:auto}.VPMenu[data-v-e7ea1737] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-e7ea1737] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-e7ea1737] .group:last-child{padding-bottom:0}.VPMenu[data-v-e7ea1737] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-e7ea1737] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-e7ea1737] .label{flex-grow:1;line-height:28px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.VPMenu[data-v-e7ea1737] .action{padding-left:24px}.VPFlyout[data-v-b6c34ac9]{position:relative}.VPFlyout[data-v-b6c34ac9]:hover{color:var(--vp-c-brand-1);transition:color .25s}.VPFlyout:hover .text[data-v-b6c34ac9]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-b6c34ac9]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-b6c34ac9]{color:var(--vp-c-brand-1)}.VPFlyout.active:hover .text[data-v-b6c34ac9]{color:var(--vp-c-brand-2)}.VPFlyout:hover .menu[data-v-b6c34ac9],.button[aria-expanded=true]+.menu[data-v-b6c34ac9]{opacity:1;visibility:visible;transform:translateY(0)}.button[aria-expanded=false]+.menu[data-v-b6c34ac9]{opacity:0;visibility:hidden;transform:translateY(0)}.button[data-v-b6c34ac9]{display:flex;align-items:center;padding:0 12px;height:var(--vp-nav-height);color:var(--vp-c-text-1);transition:color .5s}.text[data-v-b6c34ac9]{display:flex;align-items:center;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.option-icon[data-v-b6c34ac9]{margin-right:0;font-size:16px}.text-icon[data-v-b6c34ac9]{margin-left:4px;font-size:14px}.icon[data-v-b6c34ac9]{font-size:20px;transition:fill .25s}.menu[data-v-b6c34ac9]{position:absolute;top:calc(var(--vp-nav-height) / 2 + 20px);right:0;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s,transform .25s}.VPSocialLink[data-v-eee4e7cb]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-eee4e7cb]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-eee4e7cb]>svg,.VPSocialLink[data-v-eee4e7cb]>[class^=vpi-social-]{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-7bc22406]{display:flex;justify-content:center}.VPNavBarExtra[data-v-d0bd9dde]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-d0bd9dde]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-d0bd9dde]{display:none}}.trans-title[data-v-d0bd9dde]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-d0bd9dde],.item.social-links[data-v-d0bd9dde]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-d0bd9dde]{min-width:176px}.appearance-action[data-v-d0bd9dde]{margin-right:-2px}.social-links-list[data-v-d0bd9dde]{margin:-4px -8px}.VPNavBarHamburger[data-v-e5dd9c1c]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-e5dd9c1c]{display:none}}.container[data-v-e5dd9c1c]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-e5dd9c1c]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-e5dd9c1c]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-e5dd9c1c]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-e5dd9c1c]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-e5dd9c1c]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-e5dd9c1c]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-e5dd9c1c],.VPNavBarHamburger.active:hover .middle[data-v-e5dd9c1c],.VPNavBarHamburger.active:hover .bottom[data-v-e5dd9c1c]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-e5dd9c1c],.middle[data-v-e5dd9c1c],.bottom[data-v-e5dd9c1c]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-e5dd9c1c]{top:0;left:0;transform:translate(0)}.middle[data-v-e5dd9c1c]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-e5dd9c1c]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-42ef59de]{display:flex;align-items:center;padding:0 12px;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.VPNavBarMenuLink.active[data-v-42ef59de],.VPNavBarMenuLink[data-v-42ef59de]:hover{color:var(--vp-c-brand-1)}.VPNavBarMenu[data-v-7f418b0f]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-7f418b0f]{display:flex}}/*! @docsearch/css 3.6.0 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 1px 0 rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 1px 1px 0 rgba(3,4,9,.30196078431372547);--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;position:relative;padding:0 0 2px;border:0;top:-1px;width:20px}.DocSearch-Button-Key--pressed{transform:translate3d(0,1px,0);box-shadow:var(--docsearch-key-pressed-shadow)}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:2px;box-shadow:var(--docsearch-key-shadow);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;color:var(--docsearch-muted-color);border:0;width:20px}.DocSearch-VisuallyHiddenForAccessibility{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}[class*=DocSearch]{--docsearch-primary-color: var(--vp-c-brand-1);--docsearch-highlight-color: var(--docsearch-primary-color);--docsearch-text-color: var(--vp-c-text-1);--docsearch-muted-color: var(--vp-c-text-2);--docsearch-searchbox-shadow: none;--docsearch-searchbox-background: transparent;--docsearch-searchbox-focus-background: transparent;--docsearch-key-gradient: transparent;--docsearch-key-shadow: none;--docsearch-modal-background: var(--vp-c-bg-soft);--docsearch-footer-background: var(--vp-c-bg)}.dark [class*=DocSearch]{--docsearch-modal-shadow: none;--docsearch-footer-shadow: none;--docsearch-logo-color: var(--vp-c-text-2);--docsearch-hit-background: var(--vp-c-default-soft);--docsearch-hit-color: var(--vp-c-text-2);--docsearch-hit-shadow: none}.DocSearch-Button{display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:48px;height:55px;background:transparent;transition:border-color .25s}.DocSearch-Button:hover{background:transparent}.DocSearch-Button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.DocSearch-Button:focus:not(:focus-visible){outline:none!important}@media (min-width: 768px){.DocSearch-Button{justify-content:flex-start;border:1px solid transparent;border-radius:8px;padding:0 10px 0 12px;width:100%;height:40px;background-color:var(--vp-c-bg-alt)}.DocSearch-Button:hover{border-color:var(--vp-c-brand-1);background:var(--vp-c-bg-alt)}}.DocSearch-Button .DocSearch-Button-Container{display:flex;align-items:center}.DocSearch-Button .DocSearch-Search-Icon{position:relative;width:16px;height:16px;color:var(--vp-c-text-1);fill:currentColor;transition:color .5s}.DocSearch-Button:hover .DocSearch-Search-Icon{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Search-Icon{top:1px;margin-right:8px;width:14px;height:14px;color:var(--vp-c-text-2)}}.DocSearch-Button .DocSearch-Button-Placeholder{display:none;margin-top:2px;padding:0 16px 0 0;font-size:13px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.DocSearch-Button:hover .DocSearch-Button-Placeholder{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Placeholder{display:inline-block}}.DocSearch-Button .DocSearch-Button-Keys{direction:ltr;display:none;min-width:auto}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Keys{display:flex;align-items:center}}.DocSearch-Button .DocSearch-Button-Key{display:block;margin:2px 0 0;border:1px solid var(--vp-c-divider);border-right:none;border-radius:4px 0 0 4px;padding-left:6px;min-width:0;width:auto;height:22px;line-height:22px;font-family:var(--vp-font-family-base);font-size:12px;font-weight:500;transition:color .5s,border-color .5s}.DocSearch-Button .DocSearch-Button-Key+.DocSearch-Button-Key{border-right:1px solid var(--vp-c-divider);border-left:none;border-radius:0 4px 4px 0;padding-left:2px;padding-right:6px}.DocSearch-Button .DocSearch-Button-Key:first-child{font-size:0!important}.DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"Ctrl";font-size:12px;letter-spacing:normal;color:var(--docsearch-muted-color)}.mac .DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"⌘"}.DocSearch-Button .DocSearch-Button-Key:first-child>*{display:none}.DocSearch-Search-Icon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' stroke-width='1.6' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='m14.386 14.386 4.088 4.088-4.088-4.088A7.533 7.533 0 1 1 3.733 3.733a7.533 7.533 0 0 1 10.653 10.653z'/%3E%3C/svg%3E")}.VPNavBarSearch{display:flex;align-items:center}@media (min-width: 768px){.VPNavBarSearch{flex-grow:1;padding-left:24px}}@media (min-width: 960px){.VPNavBarSearch{padding-left:32px}}.dark .DocSearch-Footer{border-top:1px solid var(--vp-c-divider)}.DocSearch-Form{border:1px solid var(--vp-c-brand-1);background-color:var(--vp-c-white)}.dark .DocSearch-Form{background-color:var(--vp-c-default-soft)}.DocSearch-Screen-Icon>svg{margin:auto}.VPNavBarSocialLinks[data-v-0394ad82]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-0394ad82]{display:flex;align-items:center}}.title[data-v-ab179fa1]{display:flex;align-items:center;border-bottom:1px solid transparent;width:100%;height:var(--vp-nav-height);font-size:16px;font-weight:600;color:var(--vp-c-text-1);transition:opacity .25s}@media (min-width: 960px){.title[data-v-ab179fa1]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-ab179fa1]{border-bottom-color:var(--vp-c-divider)}}[data-v-ab179fa1] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-88af2de4]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-88af2de4]{display:flex;align-items:center}}.title[data-v-88af2de4]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-19c990f1]{position:relative;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap;transition:background-color .5s}.VPNavBar.has-local-nav[data-v-19c990f1]{background-color:var(--vp-nav-bg-color)}@media (min-width: 960px){.VPNavBar.has-local-nav[data-v-19c990f1]{background-color:transparent}.VPNavBar[data-v-19c990f1]:not(.has-sidebar):not(.top){background-color:var(--vp-nav-bg-color)}}.wrapper[data-v-19c990f1]{padding:0 8px 0 24px}@media (min-width: 768px){.wrapper[data-v-19c990f1]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar .wrapper[data-v-19c990f1]{padding:0}}.container[data-v-19c990f1]{display:flex;justify-content:space-between;margin:0 auto;max-width:calc(var(--vp-layout-max-width) - 64px);height:var(--vp-nav-height);pointer-events:none}.container>.title[data-v-19c990f1],.container>.content[data-v-19c990f1]{pointer-events:none}.container[data-v-19c990f1] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-19c990f1]{max-width:100%}}.title[data-v-19c990f1]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-19c990f1]{position:absolute;top:0;left:0;z-index:2;padding:0 32px;width:var(--vp-sidebar-width);height:var(--vp-nav-height);background-color:transparent}}@media (min-width: 1440px){.VPNavBar.has-sidebar .title[data-v-19c990f1]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}.content[data-v-19c990f1]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-19c990f1]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-19c990f1]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.content-body[data-v-19c990f1]{display:flex;justify-content:flex-end;align-items:center;height:var(--vp-nav-height);transition:background-color .5s}@media (min-width: 960px){.VPNavBar:not(.top) .content-body[data-v-19c990f1]{position:relative;background-color:var(--vp-nav-bg-color)}.VPNavBar:not(.has-sidebar):not(.top) .content-body[data-v-19c990f1]{background-color:transparent}}@media (max-width: 767px){.content-body[data-v-19c990f1]{column-gap:.5rem}}.menu+.translations[data-v-19c990f1]:before,.menu+.appearance[data-v-19c990f1]:before,.menu+.social-links[data-v-19c990f1]:before,.translations+.appearance[data-v-19c990f1]:before,.appearance+.social-links[data-v-19c990f1]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-19c990f1]:before,.translations+.appearance[data-v-19c990f1]:before{margin-right:16px}.appearance+.social-links[data-v-19c990f1]:before{margin-left:16px}.social-links[data-v-19c990f1]{margin-right:-8px}.divider[data-v-19c990f1]{width:100%;height:1px}@media (min-width: 960px){.VPNavBar.has-sidebar .divider[data-v-19c990f1]{padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .divider[data-v-19c990f1]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.divider-line[data-v-19c990f1]{width:100%;height:1px;transition:background-color .5s}.VPNavBar.has-local-nav .divider-line[data-v-19c990f1]{background-color:var(--vp-c-gutter)}@media (min-width: 960px){.VPNavBar:not(.top) .divider-line[data-v-19c990f1]{background-color:var(--vp-c-gutter)}.VPNavBar:not(.has-sidebar):not(.top) .divider[data-v-19c990f1]{background-color:var(--vp-c-gutter)}}.VPNavScreenAppearance[data-v-2d7af913]{display:flex;justify-content:space-between;align-items:center;border-radius:8px;padding:12px 14px 12px 16px;background-color:var(--vp-c-bg-soft)}.text[data-v-2d7af913]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenMenuLink[data-v-05f27b2a]{display:block;border-bottom:1px solid var(--vp-c-divider);padding:12px 0 11px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:border-color .25s,color .25s}.VPNavScreenMenuLink[data-v-05f27b2a]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupLink[data-v-19976ae1]{display:block;margin-left:12px;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-1);transition:color .25s}.VPNavScreenMenuGroupLink[data-v-19976ae1]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupSection[data-v-8133b170]{display:block}.title[data-v-8133b170]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-ff6087d4]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-ff6087d4]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-ff6087d4]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-ff6087d4]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-ff6087d4]{padding-bottom:6px;color:var(--vp-c-brand-1)}.VPNavScreenMenuGroup.open .button-icon[data-v-ff6087d4]{transform:rotate(45deg)}.button[data-v-ff6087d4]{display:flex;justify-content:space-between;align-items:center;padding:12px 4px 11px 0;width:100%;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.button[data-v-ff6087d4]:hover{color:var(--vp-c-brand-1)}.button-icon[data-v-ff6087d4]{transition:transform .25s}.group[data-v-ff6087d4]:first-child{padding-top:0}.group+.group[data-v-ff6087d4],.group+.item[data-v-ff6087d4]{padding-top:4px}.VPNavScreenTranslations[data-v-858fe1a4]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-858fe1a4]{height:auto}.title[data-v-858fe1a4]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-858fe1a4]{font-size:16px}.icon.lang[data-v-858fe1a4]{margin-right:8px}.icon.chevron[data-v-858fe1a4]{margin-left:4px}.list[data-v-858fe1a4]{padding:4px 0 0 24px}.link[data-v-858fe1a4]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-cc5739dd]{position:fixed;top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 1px);right:0;bottom:0;left:0;padding:0 32px;width:100%;background-color:var(--vp-nav-screen-bg-color);overflow-y:auto;transition:background-color .5s;pointer-events:auto}.VPNavScreen.fade-enter-active[data-v-cc5739dd],.VPNavScreen.fade-leave-active[data-v-cc5739dd]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-cc5739dd],.VPNavScreen.fade-leave-active .container[data-v-cc5739dd]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-cc5739dd],.VPNavScreen.fade-leave-to[data-v-cc5739dd]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-cc5739dd],.VPNavScreen.fade-leave-to .container[data-v-cc5739dd]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-cc5739dd]{display:none}}.container[data-v-cc5739dd]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-cc5739dd],.menu+.appearance[data-v-cc5739dd],.translations+.appearance[data-v-cc5739dd]{margin-top:24px}.menu+.social-links[data-v-cc5739dd]{margin-top:16px}.appearance+.social-links[data-v-cc5739dd]{margin-top:16px}.VPNav[data-v-ae24b3ad]{position:relative;top:var(--vp-layout-top-height, 0px);left:0;z-index:var(--vp-z-index-nav);width:100%;pointer-events:none;transition:background-color .5s}@media (min-width: 960px){.VPNav[data-v-ae24b3ad]{position:fixed}}.VPSidebarItem.level-0[data-v-93e7e794]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-93e7e794]{padding-bottom:10px}.item[data-v-93e7e794]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-93e7e794]{cursor:pointer}.indicator[data-v-93e7e794]{position:absolute;top:6px;bottom:6px;left:-17px;width:2px;border-radius:2px;transition:background-color .25s}.VPSidebarItem.level-2.is-active>.item>.indicator[data-v-93e7e794],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-93e7e794],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-93e7e794],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-93e7e794]{background-color:var(--vp-c-brand-1)}.link[data-v-93e7e794]{display:flex;align-items:center;flex-grow:1}.text[data-v-93e7e794]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-93e7e794]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-93e7e794],.VPSidebarItem.level-2 .text[data-v-93e7e794],.VPSidebarItem.level-3 .text[data-v-93e7e794],.VPSidebarItem.level-4 .text[data-v-93e7e794],.VPSidebarItem.level-5 .text[data-v-93e7e794]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-93e7e794],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-93e7e794],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-93e7e794],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-93e7e794],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-93e7e794],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-93e7e794]{color:var(--vp-c-brand-1)}.VPSidebarItem.level-0.has-active>.item>.text[data-v-93e7e794],.VPSidebarItem.level-1.has-active>.item>.text[data-v-93e7e794],.VPSidebarItem.level-2.has-active>.item>.text[data-v-93e7e794],.VPSidebarItem.level-3.has-active>.item>.text[data-v-93e7e794],.VPSidebarItem.level-4.has-active>.item>.text[data-v-93e7e794],.VPSidebarItem.level-5.has-active>.item>.text[data-v-93e7e794],.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-93e7e794],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-93e7e794],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-93e7e794],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-93e7e794],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-93e7e794],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-93e7e794]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-93e7e794],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-93e7e794],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-93e7e794],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-93e7e794],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-93e7e794],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-93e7e794]{color:var(--vp-c-brand-1)}.caret[data-v-93e7e794]{display:flex;justify-content:center;align-items:center;margin-right:-7px;width:32px;height:32px;color:var(--vp-c-text-3);cursor:pointer;transition:color .25s;flex-shrink:0}.item:hover .caret[data-v-93e7e794]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-93e7e794]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-93e7e794]{font-size:18px;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-93e7e794]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-93e7e794],.VPSidebarItem.level-2 .items[data-v-93e7e794],.VPSidebarItem.level-3 .items[data-v-93e7e794],.VPSidebarItem.level-4 .items[data-v-93e7e794],.VPSidebarItem.level-5 .items[data-v-93e7e794]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-93e7e794]{display:none}.VPSidebar[data-v-575e6a36]{position:fixed;top:var(--vp-layout-top-height, 0px);bottom:0;left:0;z-index:var(--vp-z-index-sidebar);padding:32px 32px 96px;width:calc(100vw - 64px);max-width:320px;background-color:var(--vp-sidebar-bg-color);opacity:0;box-shadow:var(--vp-c-shadow-3);overflow-x:hidden;overflow-y:auto;transform:translate(-100%);transition:opacity .5s,transform .25s ease;overscroll-behavior:contain}.VPSidebar.open[data-v-575e6a36]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-575e6a36]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-575e6a36]{padding-top:var(--vp-nav-height);width:var(--vp-sidebar-width);max-width:100%;background-color:var(--vp-sidebar-bg-color);opacity:1;visibility:visible;box-shadow:none;transform:translate(0)}}@media (min-width: 1440px){.VPSidebar[data-v-575e6a36]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}@media (min-width: 960px){.curtain[data-v-575e6a36]{position:sticky;top:-64px;left:0;z-index:1;margin-top:calc(var(--vp-nav-height) * -1);margin-right:-32px;margin-left:-32px;height:var(--vp-nav-height);background-color:var(--vp-sidebar-bg-color)}}.nav[data-v-575e6a36]{outline:0}.group+.group[data-v-575e6a36]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-575e6a36]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSkipLink[data-v-0f60ec36]{top:8px;left:8px;padding:8px 16px;z-index:999;border-radius:8px;font-size:12px;font-weight:700;text-decoration:none;color:var(--vp-c-brand-1);box-shadow:var(--vp-shadow-3);background-color:var(--vp-c-bg)}.VPSkipLink[data-v-0f60ec36]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-0f60ec36]{top:14px;left:16px}}.Layout[data-v-5d98c3a5]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-3d121b4a]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPHomeSponsors[data-v-3d121b4a]{margin:96px 0}@media (min-width: 768px){.VPHomeSponsors[data-v-3d121b4a]{margin:128px 0}}.VPHomeSponsors[data-v-3d121b4a]{padding:0 24px}@media (min-width: 768px){.VPHomeSponsors[data-v-3d121b4a]{padding:0 48px}}@media (min-width: 960px){.VPHomeSponsors[data-v-3d121b4a]{padding:0 64px}}.container[data-v-3d121b4a]{margin:0 auto;max-width:1152px}.love[data-v-3d121b4a]{margin:0 auto;width:fit-content;font-size:28px;color:var(--vp-c-text-3)}.icon[data-v-3d121b4a]{display:inline-block}.message[data-v-3d121b4a]{margin:0 auto;padding-top:10px;max-width:320px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.sponsors[data-v-3d121b4a]{padding-top:32px}.action[data-v-3d121b4a]{padding-top:40px;text-align:center}.VPTeamPage[data-v-7c57f839]{margin:96px 0}@media (min-width: 768px){.VPTeamPage[data-v-7c57f839]{margin:128px 0}}.VPHome .VPTeamPageTitle[data-v-7c57f839-s]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPTeamPageSection+.VPTeamPageSection[data-v-7c57f839-s],.VPTeamMembers+.VPTeamPageSection[data-v-7c57f839-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-7c57f839-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-7c57f839-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-7c57f839-s],.VPTeamMembers+.VPTeamPageSection[data-v-7c57f839-s]{margin-top:96px}}.VPTeamMembers[data-v-7c57f839-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-7c57f839-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-7c57f839-s]{padding:0 64px}}.VPTeamPageTitle[data-v-bf2cbdac]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-bf2cbdac]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-bf2cbdac]{padding:80px 64px 48px}}.title[data-v-bf2cbdac]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-bf2cbdac]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-bf2cbdac]{margin:0 auto;max-width:512px;padding-top:12px;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 768px){.lead[data-v-bf2cbdac]{max-width:592px;letter-spacing:.15px;line-height:28px;font-size:20px}}.VPTeamPageSection[data-v-b1a88750]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-b1a88750]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-b1a88750]{padding:0 64px}}.title[data-v-b1a88750]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-b1a88750]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-b1a88750]{position:relative;display:inline-block;padding:0 24px;letter-spacing:0;line-height:32px;font-size:20px;font-weight:500;background-color:var(--vp-c-bg)}.lead[data-v-b1a88750]{margin:0 auto;max-width:480px;padding-top:12px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.members[data-v-b1a88750]{padding-top:40px}.VPTeamMembersItem[data-v-f3fa364a]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-f3fa364a]{padding:32px}.VPTeamMembersItem.small .data[data-v-f3fa364a]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-f3fa364a]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-f3fa364a]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-f3fa364a]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-f3fa364a]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-f3fa364a]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-f3fa364a]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-f3fa364a]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-f3fa364a]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-f3fa364a]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-f3fa364a]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-f3fa364a]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-f3fa364a]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-f3fa364a]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-f3fa364a]{text-align:center}.avatar[data-v-f3fa364a]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-f3fa364a]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-f3fa364a]{margin:0;font-weight:600}.affiliation[data-v-f3fa364a]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-f3fa364a]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-f3fa364a]:hover{color:var(--vp-c-brand-1)}.desc[data-v-f3fa364a]{margin:0 auto}.desc[data-v-f3fa364a] a{font-weight:500;color:var(--vp-c-brand-1);text-decoration-style:dotted;transition:color .25s}.links[data-v-f3fa364a]{display:flex;justify-content:center;height:56px}.sp-link[data-v-f3fa364a]{display:flex;justify-content:center;align-items:center;text-align:center;padding:16px;font-size:14px;font-weight:500;color:var(--vp-c-sponsor);background-color:var(--vp-c-bg-soft);transition:color .25s,background-color .25s}.sp .sp-link.link[data-v-f3fa364a]:hover,.sp .sp-link.link[data-v-f3fa364a]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-f3fa364a]{margin-right:8px;font-size:16px}.VPTeamMembers.small .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-6cb0dbc4]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-6cb0dbc4]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-6cb0dbc4]{max-width:876px}.VPTeamMembers.medium .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-6cb0dbc4]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-6cb0dbc4]{max-width:760px}.container[data-v-6cb0dbc4]{display:grid;gap:24px;margin:0 auto;max-width:1152px}:root{--vp-c-default-1: var(--vp-c-gray-1);--vp-c-default-2: var(--vp-c-gray-2);--vp-c-default-3: var(--vp-c-gray-3);--vp-c-default-soft: var(--vp-c-gray-soft);--vp-c-brand-1: var(--vp-c-indigo-1);--vp-c-brand-2: var(--vp-c-indigo-2);--vp-c-brand-3: var(--vp-c-indigo-3);--vp-c-brand-soft: var(--vp-c-indigo-soft);--vp-c-tip-1: var(--vp-c-brand-1);--vp-c-tip-2: var(--vp-c-brand-2);--vp-c-tip-3: var(--vp-c-brand-3);--vp-c-tip-soft: var(--vp-c-brand-soft);--vp-c-warning-1: var(--vp-c-yellow-1);--vp-c-warning-2: var(--vp-c-yellow-2);--vp-c-warning-3: var(--vp-c-yellow-3);--vp-c-warning-soft: var(--vp-c-yellow-soft);--vp-c-danger-1: var(--vp-c-red-1);--vp-c-danger-2: var(--vp-c-red-2);--vp-c-danger-3: var(--vp-c-red-3);--vp-c-danger-soft: var(--vp-c-red-soft)}:root{--vp-button-brand-border: transparent;--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand-3);--vp-button-brand-hover-border: transparent;--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-2);--vp-button-brand-active-border: transparent;--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-c-brand-1)}:root{--vp-home-hero-name-color: transparent;--vp-home-hero-name-background: -webkit-linear-gradient( 120deg, #bd34fe 30%, #41d1ff );--vp-home-hero-image-background-image: linear-gradient( -45deg, #bd34fe 50%, #47caff 50% );--vp-home-hero-image-filter: blur(44px)}@media (min-width: 640px){:root{--vp-home-hero-image-filter: blur(56px)}}@media (min-width: 960px){:root{--vp-home-hero-image-filter: blur(68px)}}:root{--vp-custom-block-tip-border: transparent;--vp-custom-block-tip-text: var(--vp-c-text-1);--vp-custom-block-tip-bg: var(--vp-c-brand-soft);--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft)}.DocSearch{--docsearch-primary-color: var(--vp-c-brand-1) !important}.DemoIframe[data-v-d2bc7091]{margin:16px 0}.DemoIframe .iframeWrapper[data-v-d2bc7091]{margin-top:10px;padding:10px;border:1px solid var(--vp-c-divider, #ccc);border-radius:8px}.DemoIframe iframe[data-v-d2bc7091]{border:0;width:100%}.VPLocalSearchBox[data-v-43c4f204]{position:fixed;z-index:100;top:0;right:0;bottom:0;left:0;display:flex}.backdrop[data-v-43c4f204]{position:absolute;top:0;right:0;bottom:0;left:0;background:var(--vp-backdrop-bg-color);transition:opacity .5s}.shell[data-v-43c4f204]{position:relative;padding:12px;margin:64px auto;display:flex;flex-direction:column;gap:16px;background:var(--vp-local-search-bg);width:min(100vw - 60px,900px);height:min-content;max-height:min(100vh - 128px,900px);border-radius:6px}@media (max-width: 767px){.shell[data-v-43c4f204]{margin:0;width:100vw;height:100vh;max-height:none;border-radius:0}}.search-bar[data-v-43c4f204]{border:1px solid var(--vp-c-divider);border-radius:4px;display:flex;align-items:center;padding:0 12px;cursor:text}@media (max-width: 767px){.search-bar[data-v-43c4f204]{padding:0 8px}}.search-bar[data-v-43c4f204]:focus-within{border-color:var(--vp-c-brand-1)}.local-search-icon[data-v-43c4f204]{display:block;font-size:18px}.navigate-icon[data-v-43c4f204]{display:block;font-size:14px}.search-icon[data-v-43c4f204]{margin:8px}@media (max-width: 767px){.search-icon[data-v-43c4f204]{display:none}}.search-input[data-v-43c4f204]{padding:6px 12px;font-size:inherit;width:100%}@media (max-width: 767px){.search-input[data-v-43c4f204]{padding:6px 4px}}.search-actions[data-v-43c4f204]{display:flex;gap:4px}@media (any-pointer: coarse){.search-actions[data-v-43c4f204]{gap:8px}}@media (min-width: 769px){.search-actions.before[data-v-43c4f204]{display:none}}.search-actions button[data-v-43c4f204]{padding:8px}.search-actions button[data-v-43c4f204]:not([disabled]):hover,.toggle-layout-button.detailed-list[data-v-43c4f204]{color:var(--vp-c-brand-1)}.search-actions button.clear-button[data-v-43c4f204]:disabled{opacity:.37}.search-keyboard-shortcuts[data-v-43c4f204]{font-size:.8rem;opacity:75%;display:flex;flex-wrap:wrap;gap:16px;line-height:14px}.search-keyboard-shortcuts span[data-v-43c4f204]{display:flex;align-items:center;gap:4px}@media (max-width: 767px){.search-keyboard-shortcuts[data-v-43c4f204]{display:none}}.search-keyboard-shortcuts kbd[data-v-43c4f204]{background:#8080801a;border-radius:4px;padding:3px 6px;min-width:24px;display:inline-block;text-align:center;vertical-align:middle;border:1px solid rgba(128,128,128,.15);box-shadow:0 2px 2px #0000001a}.results[data-v-43c4f204]{display:flex;flex-direction:column;gap:6px;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain}.result[data-v-43c4f204]{display:flex;align-items:center;gap:8px;border-radius:4px;transition:none;line-height:1rem;border:solid 2px var(--vp-local-search-result-border);outline:none}.result>div[data-v-43c4f204]{margin:12px;width:100%;overflow:hidden}@media (max-width: 767px){.result>div[data-v-43c4f204]{margin:8px}}.titles[data-v-43c4f204]{display:flex;flex-wrap:wrap;gap:4px;position:relative;z-index:1001;padding:2px 0}.title[data-v-43c4f204]{display:flex;align-items:center;gap:4px}.title.main[data-v-43c4f204]{font-weight:500}.title-icon[data-v-43c4f204]{opacity:.5;font-weight:500;color:var(--vp-c-brand-1)}.title svg[data-v-43c4f204]{opacity:.5}.result.selected[data-v-43c4f204]{--vp-local-search-result-bg: var(--vp-local-search-result-selected-bg);border-color:var(--vp-local-search-result-selected-border)}.excerpt-wrapper[data-v-43c4f204]{position:relative}.excerpt[data-v-43c4f204]{opacity:75%;pointer-events:none;max-height:140px;overflow:hidden;position:relative;opacity:.5;margin-top:4px}.result.selected .excerpt[data-v-43c4f204]{opacity:1}.excerpt[data-v-43c4f204] *{font-size:.8rem!important;line-height:130%!important}.titles[data-v-43c4f204] mark,.excerpt[data-v-43c4f204] mark{background-color:var(--vp-local-search-highlight-bg);color:var(--vp-local-search-highlight-text);border-radius:2px;padding:0 2px}.excerpt[data-v-43c4f204] .vp-code-group .tabs{display:none}.excerpt[data-v-43c4f204] .vp-code-group div[class*=language-]{border-radius:8px!important}.excerpt-gradient-bottom[data-v-43c4f204]{position:absolute;bottom:-1px;left:0;width:100%;height:8px;background:linear-gradient(transparent,var(--vp-local-search-result-bg));z-index:1000}.excerpt-gradient-top[data-v-43c4f204]{position:absolute;top:-1px;left:0;width:100%;height:8px;background:linear-gradient(var(--vp-local-search-result-bg),transparent);z-index:1000}.result.selected .titles[data-v-43c4f204],.result.selected .title-icon[data-v-43c4f204]{color:var(--vp-c-brand-1)!important}.no-results[data-v-43c4f204]{font-size:.9rem;text-align:center;padding:12px}svg[data-v-43c4f204]{flex:none}
diff --git a/assets/v1_api.md.8ChaXgfD.js b/assets/v1_api.md.8ChaXgfD.js
new file mode 100644
index 0000000..76ff84f
--- /dev/null
+++ b/assets/v1_api.md.8ChaXgfD.js
@@ -0,0 +1,110 @@
+import{_ as a,c as t,o as e,a5 as s}from"./chunks/framework.BthLuVtL.js";const E=JSON.parse('{"title":"API","description":"","frontmatter":{},"headers":[],"relativePath":"v1/api.md","filePath":"v1/api.md"}'),i={name:"v1/api.md"},n=s(`API
Exported
useHeTree
: Main React hook. This library does not export components, you need to use the renderTree
render tree returned by this function.walkTreeData
, walkTreeDataGenerator
, findTreeData
, filterTreeData
, openParentsInTreeData
, updateCheckedInTreeData
: Methods for processing and traversing tree data.sortFlatData
, walkFlatData
, walkFlatDataGenerator
, convertIndexToTreeIndexInFlatData
, addToFlatData
, removeByIdInFlatData
, openParentsInFlatData
, updateCheckedInFlatData
: Methods for processing and traversing flat data.walkParentsGenerator
: To iterate over another special kind of data. This data is like HTMLElement
, which contains a key pointing to the parent node like parentElement
.defaultProps
: The default value of useHeTree
options.Id
: node id, parent id. Type: string | number
.Stat
: Node information.HeTreeProps
: Options for useHeTree
.useHeTree
import { useHeTree } from "he-tree-react";
+const {/* return */} = useHeTree({/* options */}) // prettier-ignore
Name Type Default Description data Array Data. Check Data Types. dataType 'flat', 'tree' 'flat' Data Types idKey string 'id' key of id 名. parentIdKey string 'parent_id' key of the parent id. For flat data only. childrenKey string 'children' key of children nodes. For tree data only. indent number 20 Node indentation, unit is px. dragOpen boolean false Whether to enable the function "Open node when dragging over node". dragOpenDelay number 600 The waiting time to open the node when dragging over the node. The unit is milliseconds. onDragOpen function(stat): void
The callback of "Open node when dragging over node". direction 'lrt', 'rtl' 'ltr' Display direction, ltr is displayed from left to right, rtl is the opposite. rootId string, null null The parent id of a node that has not parent in flat data. virtual boolean false Whether to enable virtualization. Used to improve performance when there is a lot of data. keepPlaceholder boolean false Whether to retain placeholder when dragging out of the tree. It is recommended to enable this only on one tree page. openIds Array All open nodes' id. checkedIds Array All checked nodes' id. isFunctionReactive boolean false Whether to listen for change of the callback functions. Reference Name Type Description renderNode (stat)=> ReactNode
Node render. renderNodeBox ({stat, attrs, isPlaceholder})=> ReactNode
nodeBox's render. Reference. onChange (newData)=>void
Callback on data change canDrag (stat)=>boolean, null, undefined, void
Whether a node draggable. Returning null, undefined, void
means inheriting the parent node.canDrop (stat, index)=>boolean, null, undefined, void
Whether a node droppable. Returning null, undefined, void
means inheriting the parent node. The parameter index
may be empty. If it is not empty, it indicates the position.customDragImage (event, stat)=> void
Called event.dataTransfer.setDragImage
to custom drag image. Reference.onDragStart (event, stat)=> void
onExternalDragOver (event)=>boolean
Called when drag from external. Must return a Boolean value to indicate whether to handle this drag. onDragOver (event, stat, isExternal)=> void
isExternal
indicates whether the drag is from outside.onDragEnd (event, stat, isOutside)=>void
Called on dragend and this drag is started in this tree. stat
is the stat of the dragged node. isOutside
indicates whether it ended outside the tree.onExternalDrop (event, parentStat, index)=>void
Called when the external drag ends on this tree. parentStat is the stat of the target parent node, and when it is empty, it represents the root of the tree. Index is the target position, the index of the node among siblings. Return of
useHeTree
useHeTree
is an object, including some states and methods. Note, this object will change every time. Do not rely on this object, but you can rely on the properties of this object. The properties are as follows:Name Type Description renderTree (options?: { className?: string, style?: React.CSSProperties }): ReactNode
Tree render. Options can be passed in className
and style
to control the style of the root element.getStat (idOrNodeOrStat)=>stat
Get stat by id, or node data, or stat object. allIds Array The ids of all nodes. rootIds Array The ids of all root nodes rootNodes Array All root nodes. In tree data, it is same with options.data
.rootStats Array All root nodes' stat. placeholder {parentStat, index, level}
Drag placeholder info. Null if it does not exist. draggingStat stat
When a drag is initiated from this tree, the stat of the dragged node. Null if it does not exist. dragOverStat stat
Dragging over node's stat. May be null. visibleIds Array All visible nodes' id. attrsList Array All visible nodes' attrs. virtualListRef ref
ref
of virtual list component, Check virtual list.scrollToNode (idOrNodeOrStat)=>boolean
Scroll to node. The argument can be id, node or stat. If node not found or invisible, it return false
. ExamplewalkTreeDataGenerator
for of
. Executing skipChildren()
in the loop will skip all child nodes of the node, and executing exitWalk
will end the traversal.for (const [
+ node,
+ { parent, parents, siblings, index, skipChildren, exitWalk },
+] of walkTreeDataGenerator(data, "children")) {
+ // ...
+}
walkTreeData
skipChildren()
in the callback method will skip all child nodes of the node, and executing exitWalk
will end the traversal.walkTreeDataGenerator(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // ...
+ },
+ "children"
+);
findTreeData
Array.prototype.find
. Returns the first node found. Executing skipChildren()
in the callback method will skip all child nodes of the node, and executing exitWalk
will end the traversal.let foundNode = findTreeData(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // return node.id === 1;
+ },
+ "children"
+);
filterTreeData
Array.prototype.filter
. Returns all nodes found. Executing skipChildren()
in the callback method will skip all child nodes of the node, and executing exitWalk
will end the traversal.let nodes = filterTreeData(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // return node.id > 1;
+ },
+ "children"
+);
openParentsInTreeData
(
+ treeData,
+ openIds: Id[],
+ idOrIds: Id | Id[],
+ options?: {idKey: string, childrenKey: string}
+): newOpenIds
updateCheckedInTreeData
checked
status of a single node or multiple nodes. This will update both their children and parents. Reference.(
+ treeData,
+ checkedIds: Id[],
+ idOrIds: Id | Id[],
+ checked: boolean,
+ options?: {idKey: string, childrenKey: string}
+): [newCheckedIds, newSemiCheckedIds]
sortFlatData
(
+ flatData,
+ options?: {idKey: string, parentIdKey: string}
+): sortedData
walkFlatDataGenerator
for of
. Executing skipChildren()
in the loop will skip all the child nodes of the node, and executing exitWalk
will end the traversal. Make sure the order of your data is correct before using it.siblings
, but has treeIndex, id, pid
. treeIndex is the index of the node in the tree.for (const [
+ node,
+ { parent, parents, index, treeIndex, id, pid, skipChildren, exitWalk },
+] of walkFlatDataGenerator(flatData, {
+ idKey: "id",
+ parentIdKey: "parent_id",
+})) {
+ // ...
+}
walkFlatData
skipChildren()
in the callback method will skip all child nodes of the node, and executing exitWalk
will end the traversal. Before using, make sure that the order of your data is correct.walkFlatData(
+ flatData,
+ (
+ node,
+ { parent, parents, index, treeIndex, id, pid, skipChildren, exitWalk }
+ ) => {
+ // ...
+ },
+ {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+);
openParentsInFlatData
(
+ flatData,
+ openIds: Id[],
+ idOrIds: Id | Id[],
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): newOpenIds
updateCheckedInFlatData
checked
status of a single node or multiple nodes. This will update both their children and parents. Make sure your data is in the correct order before using it. Reference.(
+ flatData,
+ checkedIds: Id[],
+ idOrIds: Id | Id[],
+ checked: boolean,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): [newCheckedIds, newSemiCheckedIds]
convertIndexToTreeIndexInFlatData
(
+ flatData,
+ parentId: Id | null,
+ indexInSiblings: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): treeIndex
addToFlatData
useImmer
. Reference(
+ flatData,
+ newNode,
+ index: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+):void
removeByIdInFlatData
useImmer
. Reference(
+ flatData,
+ removeId: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): removedData
walkParentsGenerator
HTMLElement
, which contains keys pointing to the parent node like parentElement
.(
+ node,
+ parentKeyOrGetter: string | ((node) => parent | undefined),
+ options?: {
+ withSelf: boolean;
+ }
+): Generator
parentKeyOrGetter
can be a string or a method that returns the parent. options.withSelf
indicates whether to include the node self. Returns Generator. Here is an example of traversing HTMLElement:let el = document.querySelector("div");
+for (const parent of walkParentsGenerator(el, "parentElement", {
+ withSelf: true,
+})) {
+ // ...
+}
Stat
stat
contains information related to the node. Read-only. The properties are as follows:
`,64),d=[n];function l(r,p,h,o,k,c){return e(),t("div",null,d)}const y=a(i,[["render",l]]);export{E as __pageData,y as default};
diff --git a/assets/v1_api.md.8ChaXgfD.lean.js b/assets/v1_api.md.8ChaXgfD.lean.js
new file mode 100644
index 0000000..3fe422e
--- /dev/null
+++ b/assets/v1_api.md.8ChaXgfD.lean.js
@@ -0,0 +1 @@
+import{_ as a,c as t,o as e,a5 as s}from"./chunks/framework.BthLuVtL.js";const E=JSON.parse('{"title":"API","description":"","frontmatter":{},"headers":[],"relativePath":"v1/api.md","filePath":"v1/api.md"}'),i={name:"v1/api.md"},n=s("",64),d=[n];function l(r,p,h,o,k,c){return e(),t("div",null,d)}const y=a(i,[["render",l]]);export{E as __pageData,y as default};
diff --git a/assets/v1_examples.md.DA1MhYZf.js b/assets/v1_examples.md.DA1MhYZf.js
new file mode 100644
index 0000000..7331db9
--- /dev/null
+++ b/assets/v1_examples.md.DA1MhYZf.js
@@ -0,0 +1,715 @@
+import{_ as k,E as t,c as l,J as i,m as h,a as n,a5 as a,o as p}from"./chunks/framework.BthLuVtL.js";const N=JSON.parse('{"title":"Examples","description":"","frontmatter":{},"headers":[],"relativePath":"v1/examples.md","filePath":"v1/examples.md"}'),E={name:"v1/examples.md"},e=h("h1",{id:"examples",tabindex:"-1"},[n("Examples "),h("a",{class:"header-anchor",href:"#examples","aria-label":'Permalink to "Examples"'},"")],-1),d=h("h2",{id:"custom-style",tabindex:"-1"},[n("Custom Style "),h("a",{class:"header-anchor",href:"#custom-style","aria-label":'Permalink to "Custom Style"'},"")],-1),r=a(`Name Type Description _isStat boolean Indicates whether it is a stat object node object node data id Id id pid Id, null parent id parent object, null parent data parentStat stat, null parent stat childIds Id[] children object[] childStats stat[] stats of children siblingIds Id[] siblings object[] sibling nodes siblingStats stat[] stats of siblings index number node's index in siblings level number node's depth in tree. Start from 1 open boolean checked boolean draggable boolean import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree, placeholder } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key} className="my-node-box">
+ {isPlaceholder ? <div className="my-placeholder">DROP HERE</div>
+ : <div className="my-node">
+ <span className="drag-handler" draggable={stat.draggable}>{dragIcon()}</span>
+ {stat.node.name}
+ </div>
+ }
+ </div>
+ ),
+ })
+ return <>
+ <h3 style={{ margin: '0 0 0 110px', padding: '20px 0 0px' }}>Draggable Tree</h3>
+ <div>
+ {renderTree({ className: \`my-tree \${placeholder ? 'dragging' : 'no-dragging'}\` })}
+ </div>
+ <style>{\`
+ .my-tree{
+ width: 300px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ margin: 20px;
+ padding: 20px;
+ }
+ .my-placeholder{
+ height:40px;
+ border: 1px dashed blue;
+ border-radius: 3px;
+ background-color: #f3ffff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: small;
+ }
+ /*.no-dragging .my-node-box:hover{
+ background-color: #eee;
+ }*/
+ .my-node-box:not(:last-child){
+ margin-bottom: 10px;
+ }
+ .my-node{
+ padding: 5px 10px;
+ padding-left: 30px;
+ border: 1px solid #e2e2e2;
+ border-radius: 3px;
+ background-color: #f0f0f0;
+ display: flex;
+ align-items: center;
+ position: relative;
+ box-shadow: 1px 1px 3px 0px rgb(0 0 0 / 19%);
+ }
+ .no-dragging .my-node:hover{
+ background-color: #ebfeff;
+ }
+ .drag-handler{
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 30px;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: grab;
+ }
+ .drag-handler:hover{
+ background-color: #f0f0f0;
+ }
+ .my-node svg{
+ width:16px;
+ }
+ \`}</style>
+ </>
+}
+
+function dragIcon() {
+ return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>drag-horizontal-variant</title><path d="M21 11H3V9H21V11M21 13H3V15H21V13Z" /></svg>
+}
Flat Data
`,3),g=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Tree-shaped Data
`,3),y=a(`import { useHeTree } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const [data, setdata] = useState(() => [
+ {
+ id: 1,
+ name: "Root Category",
+ children: [
+ {
+ id: 2,
+ name: "Technology",
+ children: [
+ {
+ id: 5,
+ name: "Hardware",
+ children: [
+ {
+ id: 10,
+ name: "Computer Components",
+ children: [],
+ },
+ ],
+ },
+ {
+ id: 4,
+ name: "Programming",
+ children: [
+ {
+ id: 8,
+ name: "Python",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 3,
+ name: "Science",
+ children: [
+ {
+ id: 7,
+ name: "Biology",
+ children: [],
+ },
+ {
+ id: 6,
+ name: "Physics",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]);
+ const { renderTree } = useHeTree({
+ data,
+ dataType: 'tree',
+ childrenKey: 'children',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Trigger Element
`,3),F=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button draggable={draggable}>Drag</button>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Placeholder
`,3),C=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? <div className="my-drag-placeholder">drop here</div>
+ : <div className="mynode">{stat.node.name}</div>
+ }
+ </div>
+ ),
+ })
+ return <div>
+ {renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ <style>{\`
+ .mytree [data-node-box]{
+ padding: 5px 0;
+ }
+ .mytree [data-node-box]:hover{
+ background-color: #eee;
+ }
+ .mytree .he-tree-drag-placeholder{
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ border: 1px dashed red;
+ }
+ .mynode{
+ padding-left:5px;
+ }
+ \`}</style>
+ </div>
+}
Open
`,3),o=a(`import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setopenIds(allIds)}>Open All</button>
+ <button onClick={() => setopenIds([])}>Close All</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Checked
`,3),c=a(`import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [checkedIds, setcheckedIds] = useState<Id[]>([]);
+ const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]);
+ const handleChecked = (id: Id, checked: boolean) => {
+ const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys);
+ setcheckedIds(r[0]);
+ setsemiCheckedIds(r[1]);
+ }
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ checkedIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} />
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ Checked: {JSON.stringify(checkedIds)} <br />
+ Semi-Checked: {JSON.stringify(semiCheckedIds)}
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Draggable & Droppable
`,3),B=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined),
+ canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined),
+ canDropToRoot: (index) => false,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Open when drag onto
`,3),A=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ dragOpen: true,
+ onDragOpen(stat) {
+ handleOpen(stat.id, true)
+ },
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Flat Data
`,3),D=a(`import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef, useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ let newData = [...data];
+ addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ setdata(newData);
+ }
+ const remove = (id: Id) => {
+ let newData = [...data];
+ removeByIdInFlatData(newData, id as number, keys)
+ setdata(newData);
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Flat Data with immer
`,3),u=a(`import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ });
+ }
+ const remove = (id: Id) => {
+ setdata(draft => {
+ removeByIdInFlatData(draft, id as number, keys)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ draft.find(node => node.id === id)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Tree Data with immer
`,3),m=a(`import { useHeTree, findTreeData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const CHILDREN = 'children'
+ const keys = { idKey: 'id', childrenKey: CHILDREN };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]);
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], })
+ })
+ }
+ const remove = (id: Id, pid: Id | null) => {
+ setdata(draft => {
+ const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN]
+ children.splice(children.findIndex(t => t.id === id), 1)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'tree',
+ onChange: setdata,
+ renderNode: ({ id, pid, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id, pid)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Drag from External
`,3),_=a(`import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react";
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ onExternalDragOver: (e) => true,
+ onExternalDrop: (e, parentStat, index) => {
+ setdata(draft => {
+ const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" }
+ addToFlatData(draft, newNode, index, keys)
+ })
+ },
+ })
+ return <div>
+ <button draggable={true}>Drag me in to the tree</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Big Data
`,3),q=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
Scroll to Node
`,3),b=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds, scrollToNode } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => scrollToNode(910)}>Scroll to 910</button>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
Guide
Installation
npm install he-tree-react
pnpm add he-tree-react
yarn add he-tree-react
Data Types
null
means there is no parent. The order of flat data must be the same as the tree, you can use the sortFlatData
method to sort the data when initializing the data.[
+ { id: 1, pid: null },
+ { id: 2, pid: 1 },
+ { id: 3, pid: 2 },
+];
dataType: 'tree'
.[
+ {
+ id: 1,
+ children: [
+ {
+ id: 2,
+ children: [{ id: 3 }],
+ },
+ ],
+ },
+];
id, pid, children
in the data are not fixed. In the options, use idKey, parentIdKey, childrenKey
to indicate the corresponding key names in your data.No Components
useHeTree
. Use the returned renderTree
to render the tree. The advantage of this is that in addition to renderTree
, useHeTree
will also return some internal states and methods, which can be easily obtained.import { useHeTree } from "he-tree-react";
+
+export default function App() {
+ const { renderTree } = useHeTree({...})
+ return <div>
+ {renderTree()}
+ </div>
+}
Options
useHeTree
is the primary function used, its first parameter is an options object. The required options are data
, and at least one of renderNode, renderNodeBox
must be present. Other important options include:dataType
, indicating data type. Available values: flat
, default. Flat data.tree
, tree-shaped data.idKey, parentIdKey
, the default values are id
and parent_id
. Needed when using flat data. Although there are default values, it is better to explicitly state them.childrenKey
, the default is children
. Needed when using tree-shaped data. Although there are default values, it is better to explicitly state them.onChange
, a function called when data changes, the parameter is new data. If your tree will not change then this is not required.isFunctionReactive
, boolean. Default false
. useHeTree
options include many callback functions, such as onChange, canDrop
. isFunctionReactive
can be used to control whether to listen for changes to these callback functions. If your callback functions and data
change synchronously, you do not need to enable this. Otherwise, you need to enable this and use React's useCallback
or useMemo
to cache all your callback functions to avoid performance issues.useHeTree
API documentation for more information.Tips
stat
, information related to a single node. Most of the parameters in callback functions have stat
. Refer to Stat
API.node
, the data of the node. You can get node data through stat.node
.getStat
, through this function you can get stat
, the only parameter can be id, node, stat
. This function is in the return object of useHeTree
: const {getStat} = useHeTree({...})
.tsx
format, if you need the js
format, you can use any ts js online converter.Use Flat Data
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Use Tree-shaped Data
import { useHeTree } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const [data, setdata] = useState(() => [
+ {
+ id: 1,
+ name: "Root Category",
+ children: [
+ {
+ id: 2,
+ name: "Technology",
+ children: [
+ {
+ id: 5,
+ name: "Hardware",
+ children: [
+ {
+ id: 10,
+ name: "Computer Components",
+ children: [],
+ },
+ ],
+ },
+ {
+ id: 4,
+ name: "Programming",
+ children: [
+ {
+ id: 8,
+ name: "Python",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 3,
+ name: "Science",
+ children: [
+ {
+ id: 7,
+ name: "Biology",
+ children: [],
+ },
+ {
+ id: 6,
+ name: "Physics",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]);
+ const { renderTree } = useHeTree({
+ data,
+ dataType: 'tree',
+ childrenKey: 'children',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Custom Drag Trigger Element
draggable
attribute to any child element of the node.import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button draggable={draggable}>Drag</button>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
HTML and Style of Node
<div
+ draggable="true"
+ data-key="1"
+ data-level="1"
+ data-node-box="true"
+ style="padding-left: 0px;"
+>
+ <div>Node</div>
+</div>
div
above. Use the renderNode
option to control the rendering of the inner div. For example: renderNode: ({node}) => <div>{node.name}</div>
.nodeBox
, don't modify its padding-left, padding-right
. Use the indent
option to control the indentation of the node. If you want to control the rendering of nodeBox
or the drag placeholder, you can use the renderNodeBox
option, which will override renderNode
. The standard renderNodeBox
is as follows:renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? (
+ <div
+ className="he-tree-drag-placeholder"
+ style={{ minHeight: "20px", border: "1px dashed blue" }}
+ />
+ ) : (
+ <div>{/* node area */}</div>
+ )}
+ </div>
+);
Custom Drag Placeholder and Node Box
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? <div className="my-drag-placeholder">drop here</div>
+ : <div className="mynode">{stat.node.name}</div>
+ }
+ </div>
+ ),
+ })
+ return <div>
+ {renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ <style>{\`
+ .mytree [data-node-box]{
+ padding: 5px 0;
+ }
+ .mytree [data-node-box]:hover{
+ background-color: #eee;
+ }
+ .mytree .he-tree-drag-placeholder{
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ border: 1px dashed red;
+ }
+ .mynode{
+ padding-left:5px;
+ }
+ \`}</style>
+ </div>
+}
Open & Close
openIds
option to indicate the open nodes.open
status of the node can be obtained through stat.open
.allIds
returned by useHeTree
contains the ids of all nodes.openParentsInFlatData
. For tree data: openParentsInTreeData
.import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setopenIds(allIds)}>Open All</button>
+ <button onClick={() => setopenIds([])}>Close All</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Checked
checkedIds
to indicate the checked nodes.checked
status of this node can be obtained through stat.checked
.checkedIds
for one or more nodes after the checked
status changes. Flat data: updateCheckedInFlatData
. Tree data: \`updateCheckedInTreeData. checked
is cascading. If you don't want to cascade updates, replace it with your own logic.import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [checkedIds, setcheckedIds] = useState<Id[]>([]);
+ const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]);
+ const handleChecked = (id: Id, checked: boolean) => {
+ const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys);
+ setcheckedIds(r[0]);
+ setsemiCheckedIds(r[1]);
+ }
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ checkedIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} />
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ Checked: {JSON.stringify(checkedIds)} <br />
+ Semi-Checked: {JSON.stringify(semiCheckedIds)}
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
draggable & droppable
canDrag
, whether the node can be dragged.canDrop
, whether the node can be dropped.canDropToRoot
, whether the tree root can be dropped.import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined),
+ canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined),
+ canDropToRoot: (index) => false,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Technology
and its sub-nodes can be dragged. Science
and its sub-nodes cannot be dragged.Science
and its sub-nodes can be dropped. Technology
and its sub-nodes cannot be dropped.Open when dragging over
dragOpen
, whether to enable, default false
.dragOpenDelay
, delay, default 600
milliseconds.onDragOpen
, the function called when the node is opened.import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ dragOpen: true,
+ onDragOpen(stat) {
+ handleOpen(stat.id, true)
+ },
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Data
immer
.npm install immer use-immer
pnpm add immer use-immer
yarn add immer use-immer
Update Flat Data
addToFlatData
. removeByIdInFlatData
. These 2 methods will modify original data, so pass copy to it, or use immer
.import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef, useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ let newData = [...data];
+ addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ setdata(newData);
+ }
+ const remove = (id: Id) => {
+ let newData = [...data];
+ removeByIdInFlatData(newData, id as number, keys)
+ setdata(newData);
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Flat Data with immer
useImmer
instead of React's useState
.import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ });
+ }
+ const remove = (id: Id) => {
+ setdata(draft => {
+ removeByIdInFlatData(draft, id as number, keys)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ draft.find(node => node.id === id)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Tree Data with immer
useImmer
instead of React's useState
. findTreeData
is like Array.prototype.find
.import { useHeTree, findTreeData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const CHILDREN = 'children'
+ const keys = { idKey: 'id', childrenKey: CHILDREN };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]);
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], })
+ })
+ }
+ const remove = (id: Id, pid: Id | null) => {
+ setdata(draft => {
+ const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN]
+ children.splice(children.findIndex(t => t.id === id), 1)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'tree',
+ onChange: setdata,
+ renderNode: ({ id, pid, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id, pid)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Drag from External
onExternalDragOver
: Indicate whether to handle external drag.onExternalDrop
: Callback when drop from external.import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react";
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ onExternalDragOver: (e) => true,
+ onExternalDrop: (e, parentStat, index) => {
+ setdata(draft => {
+ const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" }
+ addToFlatData(draft, newNode, index, keys)
+ })
+ },
+ })
+ return <div>
+ <button draggable={true}>Drag me in to the tree</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Big Data
virtual
to enable virtual list. Remember to set height for tree.import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
Touch & Mobile Device
Others
',5);function D(m,_,b,q,f,v){const i=n("DemoIframe");return t(),k("div",null,[p,a(i,{url:"/base_flat_data"}),e,a(i,{url:"/base_tree_data"}),E,a(i,{url:"/custom_drag_trigger_flat_data"}),d,a(i,{url:"/customize_placeholder_and_node_box"}),r,a(i,{url:"/open_ids"}),g,a(i,{url:"/checked_ids"}),y,a(i,{url:"/draggable_droppable"}),F,a(i,{url:"/dragopen"}),o,a(i,{url:"/update_data"}),c,a(i,{url:"/update_flat_data_with_immer"}),C,a(i,{url:"/update_tree_data_with_immer"}),B,a(i,{url:"/external_drag"}),u,a(i,{url:"/virtual_list"}),A])}const x=h(l,[["render",D]]);export{I as __pageData,x as default};
diff --git a/assets/v1_guide.md.DSikYvGc.lean.js b/assets/v1_guide.md.DSikYvGc.lean.js
new file mode 100644
index 0000000..f4c55b5
--- /dev/null
+++ b/assets/v1_guide.md.DSikYvGc.lean.js
@@ -0,0 +1 @@
+import{_ as h,E as n,c as k,J as a,a5 as s,o as t}from"./chunks/framework.BthLuVtL.js";const I=JSON.parse('{"title":"Guide","description":"","frontmatter":{},"headers":[],"relativePath":"v1/guide.md","filePath":"v1/guide.md"}'),l={name:"v1/guide.md"},p=s("",18),e=s("",2),E=s("",3),d=s("",9),r=s("",3),g=s("",3),y=s("",4),F=s("",5),o=s("",6),c=s("",3),C=s("",3),B=s("",4),u=s("",3),A=s("",5);function D(m,_,b,q,f,v){const i=n("DemoIframe");return t(),k("div",null,[p,a(i,{url:"/base_flat_data"}),e,a(i,{url:"/base_tree_data"}),E,a(i,{url:"/custom_drag_trigger_flat_data"}),d,a(i,{url:"/customize_placeholder_and_node_box"}),r,a(i,{url:"/open_ids"}),g,a(i,{url:"/checked_ids"}),y,a(i,{url:"/draggable_droppable"}),F,a(i,{url:"/dragopen"}),o,a(i,{url:"/update_data"}),c,a(i,{url:"/update_flat_data_with_immer"}),C,a(i,{url:"/update_tree_data_with_immer"}),B,a(i,{url:"/external_drag"}),u,a(i,{url:"/virtual_list"}),A])}const x=h(l,[["render",D]]);export{I as __pageData,x as default};
diff --git a/assets/zh_index.md.Ct9xdv7q.js b/assets/zh_index.md.Ct9xdv7q.js
new file mode 100644
index 0000000..658ac1b
--- /dev/null
+++ b/assets/zh_index.md.Ct9xdv7q.js
@@ -0,0 +1 @@
+import{_ as e,c as t,o as i,a5 as a}from"./chunks/framework.BthLuVtL.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"He Tree React","tagline":"React 可拖拽树组件","actions":[{"theme":"brand","text":"开始使用","link":"./v1/guide"},{"theme":"alt","text":"演示","link":"./v1/examples"}]},"features":[{"title":"可拖拽","details":"拖拽时显示占位框辅助用户选择位置."},{"title":"高性能","details":"支持虚拟列表, 从容处理大量数据."},{"title":"易定制","details":"结构简单, 很少内置样式, 可以容易的修改样式和UI."}]},"headers":[],"relativePath":"zh/index.md","filePath":"zh/index.md"}'),l={name:"zh/index.md"},n=a('direction
: from right to left.customDragImage
: custom drag image.rootId
: the parent id of root nodes in flat data.keepPlaceholder
: whether to retain the drag placeholder node when dragging outside the tree. Default is false
.scrollToNode
: Scroll to a node.功能
',2),o=[n];function r(s,_,d,c,h,m){return i(),t("div",null,o)}const x=e(l,[["render",r]]);export{u as __pageData,x as default};
diff --git a/assets/zh_index.md.Ct9xdv7q.lean.js b/assets/zh_index.md.Ct9xdv7q.lean.js
new file mode 100644
index 0000000..4c010db
--- /dev/null
+++ b/assets/zh_index.md.Ct9xdv7q.lean.js
@@ -0,0 +1 @@
+import{_ as e,c as t,o as i,a5 as a}from"./chunks/framework.BthLuVtL.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"He Tree React","tagline":"React 可拖拽树组件","actions":[{"theme":"brand","text":"开始使用","link":"./v1/guide"},{"theme":"alt","text":"演示","link":"./v1/examples"}]},"features":[{"title":"可拖拽","details":"拖拽时显示占位框辅助用户选择位置."},{"title":"高性能","details":"支持虚拟列表, 从容处理大量数据."},{"title":"易定制","details":"结构简单, 很少内置样式, 可以容易的修改样式和UI."}]},"headers":[],"relativePath":"zh/index.md","filePath":"zh/index.md"}'),l={name:"zh/index.md"},n=a("",2),o=[n];function r(s,_,d,c,h,m){return i(),t("div",null,o)}const x=e(l,[["render",r]]);export{u as __pageData,x as default};
diff --git a/assets/zh_v1_api.md.BHPyDvs2.js b/assets/zh_v1_api.md.BHPyDvs2.js
new file mode 100644
index 0000000..09622f5
--- /dev/null
+++ b/assets/zh_v1_api.md.BHPyDvs2.js
@@ -0,0 +1,110 @@
+import{_ as a,c as s,o as t,a5 as e}from"./chunks/framework.BthLuVtL.js";const g=JSON.parse('{"title":"API","description":"","frontmatter":{},"headers":[],"relativePath":"zh/v1/api.md","filePath":"zh/v1/api.md"}'),i={name:"zh/v1/api.md"},n=e(`API
导出
useHeTree
: 主要的 React hook. 本库没有导出组件, 你需要使用此函数返回的renderTree
渲染树.walkTreeData
, walkTreeDataGenerator
, findTreeData
, filterTreeData
, openParentsInTreeData
, updateCheckedInTreeData
: 用来处理和遍历树形数据的方法.sortFlatData
, walkFlatData
, walkFlatDataGenerator
, convertIndexToTreeIndexInFlatData
, addToFlatData
, removeByIdInFlatData
, openParentsInFlatData
, updateCheckedInFlatData
: 用来处理和遍历扁平数据的方法.walkParentsGenerator
: 遍历另一种特殊数据的方法. 这种数据类似HTMLElement
, 其中包含类似于parentElement
的指向父节点的键.defaultProps
: useHeTree
的选项的默认值.Id
: 节点 id, 父级 id. 类型: string | number
.Stat
: 节点的相关信息.HeTreeProps
: useHeTree
的选项.useHeTree
import { useHeTree } from "he-tree-react";
+const {/* return */} = useHeTree({/* options */}) // prettier-ignore
名称 类型 默认值 描述 data Array 数据. 参考数据类型. dataType 'flat', 'tree' 'flat' 数据类型 idKey string 'id' 你的数据中 id 的键名. parentIdKey string 'parent_id' 你的数据中父级 id 的键名. 仅用于扁平数据. childrenKey string 'children' 你的数据中子级的键名. 仅用于树形数据. indent number 20 节点缩进, 单位是 px. dragOpen boolean false 是否启用功能"拖拽到节点上时打开节点". dragOpenDelay number 600 拖拽到节点上时打开节点的等待时间. 单位是毫秒. onDragOpen function(stat): void
拖拽到节点上时打开节点的回调. direction 'lrt', 'rtl' 'ltr' 显示方向, ltr 是从左往右显示, rtl 与之相反. rootId string, null null 使用扁平数据时, 没有父级的节点的父级 id. virtual boolean false 是否启用虚拟化. 当数据非常多时用来提高性能. keepPlaceholder boolean false 当拖拽离开树的范围, 是否要保留占位元素. 建议只在一个树的页面开启此项. openIds Array 所有打开节点的 id. checkedIds Array 所有勾选的节点的 id. isFunctionReactive boolean false 是否监听回调函数的改变. 参考 名称 类型 描述 renderNode (stat)=> ReactNode
节点的渲染函数. renderNodeBox ({stat, attrs, isPlaceholder})=> ReactNode
nodeBox 的渲染函数. 参考. onChange (newData)=>void
数据发生改变时调用. canDrag (stat)=>boolean, null, undefined, void
节点是否可拖拽. 返回 null, undefined, void
表示继承父节点.canDrop (stat, index)=>boolean, null, undefined, void
节点是否可放入. 返回 null, undefined, void
表示继承父节点. 参数index
可能为空, 不为空时表示将要放入节点的子级的位置.customDragImage (event, stat)=> void
调用 event.dataTransfer.setDragImage
自定义 drag image. 参考.onDragStart (event, stat)=> void
当拖拽开始时 onExternalDragOver (event)=>boolean
当拖拽来自外部时调用. 你必选返回布尔值表示是否处理此拖拽. onDragOver (event, stat, isExternal)=> void
当拖拽到树上方时, isExternal
表示此次拖拽是否来自外部.onDragEnd (event, stat, isOutside)=>void
当此树发起的拖拽结束时调用. stat 是此次拖拽的节点的 stat.isOutside 表示是否在树外部结束. onExternalDrop (event, parentStat, index)=>void
当外部拖拽在此树结束时调用. parentStat 是目标父节点的 stat, 为空时代表树的根级. index 是目标位置, 即节点在兄弟节点中的索引. useHeTree
的返回 useHeTree
的返回是对象, 包含了一些 states 和方法. 注意, 这个对象每次更新都会改变, 不要依赖这个对象, 可以依赖这个对象的属性. 属性如下:名称 类型 描述 renderTree (options?: { className?: string, style?: React.CSSProperties }): ReactNode
渲染树. 参数可以传入 className
和style
控制根元素的样式.getStat (idOrNodeOrStat)=>stat
根据 id, 节点数据或 stat, 获得对应的 stat. allIds 数组 所有节点的 id. rootIds 数组 树根级的所有节点的 id. rootNodes 数组 树根级的所有节点的数据. 如果是树形数据, 它就是选项中的 data
.rootStats 数组 树根级的所有节点的 stat. placeholder {parentStat, index, level}
拖拽时占位节点的信息. 占位节点不存在时为空. draggingStat stat
由此树发起拖拽时, 被拖拽的节点的 stat. 不存在时为空. dragOverStat stat
拖拽到其上面的节点. 可能为空. visibleIds 数组 显示的所有节点的 id. attrsList 数组 显示的所有节点的 attrs. virtualListRef ref
虚拟列表组件的 ref, 参考虚拟列表. scrollToNode (idOrNodeOrStat)=>boolean
滚动到节点. 参数可以是 id, 节点数据或 stat. 如果节点未找到或未显示, 返回 false
. 例子walkTreeDataGenerator
for of
遍历树形数据的方法. 循环中执行skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历.for (const [
+ node,
+ { parent, parents, siblings, index, skipChildren, exitWalk },
+] of walkTreeDataGenerator(data, "children")) {
+ // ...
+}
walkTreeData
skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历.walkTreeDataGenerator(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // ...
+ },
+ "children"
+);
findTreeData
Array.prototype.find
. 返回找到的第一个节点. 回调方法中执行skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历.let foundNode = findTreeData(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // return node.id === 1;
+ },
+ "children"
+);
filterTreeData
Array.prototype.filter
. 返回找到的所有节点. 回调方法中执行skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历.let nodes = filterTreeData(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // return node.id > 1;
+ },
+ "children"
+);
openParentsInTreeData
(
+ treeData,
+ openIds: Id[],
+ idOrIds: Id | Id[],
+ options?: {idKey: string, childrenKey: string}
+): newOpenIds
updateCheckedInTreeData
checked
状态. 这将同时更新它们的子节点和父节点. 参考.(
+ treeData,
+ checkedIds: Id[],
+ idOrIds: Id | Id[],
+ checked: boolean,
+ options?: {idKey: string, childrenKey: string}
+): [newCheckedIds, newSemiCheckedIds]
sortFlatData
(
+ flatData,
+ options?: {idKey: string, parentIdKey: string}
+): sortedData
walkFlatDataGenerator
for of
遍历扁平数据的方法. 循环中执行skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历. 使用前需确保你的数据的顺序是正确的.walkTreeDataGenerator
, 少了siblings
, 多了 treeIndex, id, pid
. treeIndex
是节点在整个树中的索引.for (const [
+ node,
+ { parent, parents, index, treeIndex, id, pid, skipChildren, exitWalk },
+] of walkFlatDataGenerator(flatData, {
+ idKey: "id",
+ parentIdKey: "parent_id",
+})) {
+ // ...
+}
walkFlatData
skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历. 使用前需确保你的数据的顺序是正确的.walkFlatData(
+ flatData,
+ (
+ node,
+ { parent, parents, index, treeIndex, id, pid, skipChildren, exitWalk }
+ ) => {
+ // ...
+ },
+ {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+);
openParentsInFlatData
(
+ flatData,
+ openIds: Id[],
+ idOrIds: Id | Id[],
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): newOpenIds
updateCheckedInFlatData
checked
状态. 这将同时更新它们的子节点和父节点. 用前需确保你的数据的顺序是正确的. 参考.(
+ flatData,
+ checkedIds: Id[],
+ idOrIds: Id | Id[],
+ checked: boolean,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): [newCheckedIds, newSemiCheckedIds]
convertIndexToTreeIndexInFlatData
(
+ flatData,
+ parentId: Id | null,
+ indexInSiblings: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): treeIndex
addToFlatData
useImmer
一起使用. 参考(
+ flatData,
+ newNode,
+ index: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+):void
removeByIdInFlatData
useImmer
一起使用. 参考(
+ flatData,
+ removeId: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): removedData
walkParentsGenerator
HTMLElement
, 其中包含类似于parentElement
的指向父节点的键.(
+ node,
+ parentKeyOrGetter: string | ((node) => parent | undefined),
+ options?: {
+ withSelf: boolean;
+ }
+): Generator
parentKeyOrGetter
可以是字符串或者返回父级的方法. options.withSelf
表示是否包括传入的节点. 返回 Generator. 下面是遍历 HTMLElement 的例子:let el = document.querySelector("div");
+for (const parent of walkParentsGenerator(el, "parentElement", {
+ withSelf: true,
+})) {
+ // ...
+}
Stat
stat
包括和节点有关的信息. 只读. 属性如下:
`,64),d=[n];function l(p,h,r,k,o,c){return t(),s("div",null,d)}const y=a(i,[["render",l]]);export{g as __pageData,y as default};
diff --git a/assets/zh_v1_api.md.BHPyDvs2.lean.js b/assets/zh_v1_api.md.BHPyDvs2.lean.js
new file mode 100644
index 0000000..398cf3e
--- /dev/null
+++ b/assets/zh_v1_api.md.BHPyDvs2.lean.js
@@ -0,0 +1 @@
+import{_ as a,c as s,o as t,a5 as e}from"./chunks/framework.BthLuVtL.js";const g=JSON.parse('{"title":"API","description":"","frontmatter":{},"headers":[],"relativePath":"zh/v1/api.md","filePath":"zh/v1/api.md"}'),i={name:"zh/v1/api.md"},n=e("",64),d=[n];function l(p,h,r,k,o,c){return t(),s("div",null,d)}const y=a(i,[["render",l]]);export{g as __pageData,y as default};
diff --git a/assets/zh_v1_examples.md.N-IWpcWm.js b/assets/zh_v1_examples.md.N-IWpcWm.js
new file mode 100644
index 0000000..6fc1a67
--- /dev/null
+++ b/assets/zh_v1_examples.md.N-IWpcWm.js
@@ -0,0 +1,715 @@
+import{_ as k,E as t,c as l,J as i,m as h,a as n,a5 as a,o as p}from"./chunks/framework.BthLuVtL.js";const w=JSON.parse('{"title":"示例","description":"","frontmatter":{},"headers":[],"relativePath":"zh/v1/examples.md","filePath":"zh/v1/examples.md"}'),E={name:"zh/v1/examples.md"},e=h("h1",{id:"示例",tabindex:"-1"},[n("示例 "),h("a",{class:"header-anchor",href:"#示例","aria-label":'Permalink to "示例"'},"")],-1),d=h("h2",{id:"自定义样式",tabindex:"-1"},[n("自定义样式 "),h("a",{class:"header-anchor",href:"#自定义样式","aria-label":'Permalink to "自定义样式"'},"")],-1),r=a(`名称 类型 描述 _isStat boolean 表明是否是 stat 对象 node object 节点的数据 id Id id pid Id, null 节点的父级 id parent object, null 父节点的数据 parentStat stat, null 父节点的 stat childIds Id[] 子节点的 id 数组 children object[] 子节点数组 childStats stat[] 子节点的 stat 数组 siblingIds Id[] 兄弟节点的 id 数组 siblings object[] 兄弟节点数组 siblingStats stat[] 兄弟节点的 stat 数组 index number 节点在兄弟节点中的索引 level number 节点在树中的深度. 从 1 开始 open boolean 是否展开 checked boolean 是否勾选 draggable boolean 是否可拖动 import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree, placeholder } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key} className="my-node-box">
+ {isPlaceholder ? <div className="my-placeholder">DROP HERE</div>
+ : <div className="my-node">
+ <span className="drag-handler" draggable={stat.draggable}>{dragIcon()}</span>
+ {stat.node.name}
+ </div>
+ }
+ </div>
+ ),
+ })
+ return <>
+ <h3 style={{ margin: '0 0 0 110px', padding: '20px 0 0px' }}>Draggable Tree</h3>
+ <div>
+ {renderTree({ className: \`my-tree \${placeholder ? 'dragging' : 'no-dragging'}\` })}
+ </div>
+ <style>{\`
+ .my-tree{
+ width: 300px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ margin: 20px;
+ padding: 20px;
+ }
+ .my-placeholder{
+ height:40px;
+ border: 1px dashed blue;
+ border-radius: 3px;
+ background-color: #f3ffff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: small;
+ }
+ /*.no-dragging .my-node-box:hover{
+ background-color: #eee;
+ }*/
+ .my-node-box:not(:last-child){
+ margin-bottom: 10px;
+ }
+ .my-node{
+ padding: 5px 10px;
+ padding-left: 30px;
+ border: 1px solid #e2e2e2;
+ border-radius: 3px;
+ background-color: #f0f0f0;
+ display: flex;
+ align-items: center;
+ position: relative;
+ box-shadow: 1px 1px 3px 0px rgb(0 0 0 / 19%);
+ }
+ .no-dragging .my-node:hover{
+ background-color: #ebfeff;
+ }
+ .drag-handler{
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 30px;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: grab;
+ }
+ .drag-handler:hover{
+ background-color: #f0f0f0;
+ }
+ .my-node svg{
+ width:16px;
+ }
+ \`}</style>
+ </>
+}
+
+function dragIcon() {
+ return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>drag-horizontal-variant</title><path d="M21 11H3V9H21V11M21 13H3V15H21V13Z" /></svg>
+}
扁平数据
`,3),g=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
树形数据
`,3),y=a(`import { useHeTree } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const [data, setdata] = useState(() => [
+ {
+ id: 1,
+ name: "Root Category",
+ children: [
+ {
+ id: 2,
+ name: "Technology",
+ children: [
+ {
+ id: 5,
+ name: "Hardware",
+ children: [
+ {
+ id: 10,
+ name: "Computer Components",
+ children: [],
+ },
+ ],
+ },
+ {
+ id: 4,
+ name: "Programming",
+ children: [
+ {
+ id: 8,
+ name: "Python",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 3,
+ name: "Science",
+ children: [
+ {
+ id: 7,
+ name: "Biology",
+ children: [],
+ },
+ {
+ id: 6,
+ name: "Physics",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]);
+ const { renderTree } = useHeTree({
+ data,
+ dataType: 'tree',
+ childrenKey: 'children',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
触发元素
`,3),F=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button draggable={draggable}>Drag</button>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
占位元素
`,3),C=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? <div className="my-drag-placeholder">drop here</div>
+ : <div className="mynode">{stat.node.name}</div>
+ }
+ </div>
+ ),
+ })
+ return <div>
+ {renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ <style>{\`
+ .mytree [data-node-box]{
+ padding: 5px 0;
+ }
+ .mytree [data-node-box]:hover{
+ background-color: #eee;
+ }
+ .mytree .he-tree-drag-placeholder{
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ border: 1px dashed red;
+ }
+ .mynode{
+ padding-left:5px;
+ }
+ \`}</style>
+ </div>
+}
展开
`,3),o=a(`import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setopenIds(allIds)}>Open All</button>
+ <button onClick={() => setopenIds([])}>Close All</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
勾选
`,3),c=a(`import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [checkedIds, setcheckedIds] = useState<Id[]>([]);
+ const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]);
+ const handleChecked = (id: Id, checked: boolean) => {
+ const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys);
+ setcheckedIds(r[0]);
+ setsemiCheckedIds(r[1]);
+ }
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ checkedIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} />
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ Checked: {JSON.stringify(checkedIds)} <br />
+ Semi-Checked: {JSON.stringify(semiCheckedIds)}
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
拖拽控制
`,3),B=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined),
+ canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined),
+ canDropToRoot: (index) => false,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
拖拽时打开
`,3),A=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ dragOpen: true,
+ onDragOpen(stat) {
+ handleOpen(stat.id, true)
+ },
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
更新扁平数据
`,3),D=a(`import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef, useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ let newData = [...data];
+ addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ setdata(newData);
+ }
+ const remove = (id: Id) => {
+ let newData = [...data];
+ removeByIdInFlatData(newData, id as number, keys)
+ setdata(newData);
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
更新扁平数据使用 immer
`,3),u=a(`import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ });
+ }
+ const remove = (id: Id) => {
+ setdata(draft => {
+ removeByIdInFlatData(draft, id as number, keys)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ draft.find(node => node.id === id)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
更新树形数据使用 immer
`,3),m=a(`import { useHeTree, findTreeData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const CHILDREN = 'children'
+ const keys = { idKey: 'id', childrenKey: CHILDREN };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]);
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], })
+ })
+ }
+ const remove = (id: Id, pid: Id | null) => {
+ setdata(draft => {
+ const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN]
+ children.splice(children.findIndex(t => t.id === id), 1)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'tree',
+ onChange: setdata,
+ renderNode: ({ id, pid, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id, pid)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
外部拖拽交互
`,3),_=a(`import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react";
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ onExternalDragOver: (e) => true,
+ onExternalDrop: (e, parentStat, index) => {
+ setdata(draft => {
+ const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" }
+ addToFlatData(draft, newNode, index, keys)
+ })
+ },
+ })
+ return <div>
+ <button draggable={true}>Drag me in to the tree</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
大数据, 虚拟列表
`,3),q=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
滚动到节点
`,3),b=a(`import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds, scrollToNode } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => scrollToNode(910)}>Scroll to 910</button>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
使用指南
安装
npm install he-tree-react
pnpm add he-tree-react
yarn add he-tree-react
数据类型
id
, 父级 id, null
代表没有父级. 扁平数据的顺序必须跟树一样, 你可以在初始化数据时使用sortFlatData
方法给数据排序.[
+ { id: 1, pid: null },
+ { id: 2, pid: 1 },
+ { id: 3, pid: 2 },
+];
children
数组包含子节点. 如果未指定id
, 此库将使用节点在树中的索引作为id
. 使用树形数据时需设置dataType: 'tree'
.[
+ {
+ id: 1,
+ children: [
+ {
+ id: 2,
+ children: [{ id: 3 }],
+ },
+ ],
+ },
+];
id, pid, children
不是固定的. 在设置中, 使用idKey, parentIdKey, childrenKey
表明你的数据中的对应键名.没有组件
useHeTree
. 使用它返回的renderTree
渲染树. 这样做的好处是除了renderTree
, useHeTree
还会返回一些内部状态和方法, 可以轻松的被获取.import { useHeTree } from "he-tree-react";
+
+export default function App() {
+ const { renderTree } = useHeTree({...})
+ return <div>
+ {renderTree()}
+ </div>
+}
选项
useHeTree
是主要使用的函数, 它的第一个参数是选项对象. 必须的选项有data
, 必须两者中有一个的是renderNode, renderNodeBox
. 其他重要选项是:dataType
, 表明数据类型. 可用值: flat
, 默认. 扁平数据.tree
, 树形数据.idKey, parentIdKey
, 默认值是id
和parent_id
. 使用扁平数据时需要. 虽然有默认值, 但还是建议写明更好.childrenKey
, 默认是children
. 使用树形数据时需要. 虽然有默认值, 但还是建议写明更好.onChange
, 数据改变时调用的函数, 参数是新数据. 如果你的树不会改变则不需要.isFunctionReactive
, 布尔. 默认false
. useHeTree
选项中包含许多回调函数, 如onChange, canDrop
. isFunctionReactive
可用来控制是否监听这些回调函数的改变. 如果你的回调函数和data
是同步改变的, 则不用启用此项. 否则你需要启用此项, 并且用 React 的useCallback
或useMemo
缓存你的所有回调函数以避免性能问题.提示
stat
, 单个节点的相关信息. 大部分回调函数的参数里有stat
. 参考Stat
API.node
, 节点的数据. 通过stat.node
可以获取节点数据.getStat
, 通过此函数可以获取stat
, 唯一参数可以是id, node, stat
. 此函数在useHeTree
的返回对象中: const {getStat} = useHeTree({...})
.tsx
格式, 如果你需要js
格式, 可以使用任意 ts js 在线转换器.基础使用-扁平数据
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
基础使用-树形数据
import { useHeTree } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const [data, setdata] = useState(() => [
+ {
+ id: 1,
+ name: "Root Category",
+ children: [
+ {
+ id: 2,
+ name: "Technology",
+ children: [
+ {
+ id: 5,
+ name: "Hardware",
+ children: [
+ {
+ id: 10,
+ name: "Computer Components",
+ children: [],
+ },
+ ],
+ },
+ {
+ id: 4,
+ name: "Programming",
+ children: [
+ {
+ id: 8,
+ name: "Python",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 3,
+ name: "Science",
+ children: [
+ {
+ id: 7,
+ name: "Biology",
+ children: [],
+ },
+ {
+ id: 6,
+ name: "Physics",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]);
+ const { renderTree } = useHeTree({
+ data,
+ dataType: 'tree',
+ childrenKey: 'children',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
自定义拖拽触发元素
draggable
属性即可.import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button draggable={draggable}>Drag</button>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
节点 HTML 结构和样式
<div
+ draggable="true"
+ data-key="1"
+ data-level="1"
+ data-node-box="true"
+ style="padding-left: 0px;"
+>
+ <div>Node</div>
+</div>
renderNode
选项控制内层 div 的渲染. 如: renderNode: ({node}) => <div>{node.name}</div>
.nodeBox
, 不要修改它的padding-left, padding-right
. 使用选项indent
控制节点的缩进. 如果你想控制nodeBox
或拖拽占位节点的渲染, 可以使用renderNodeBox
选项, 这将覆盖renderNode
. 标准的renderNodeBox
如下:renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? (
+ <div
+ className="he-tree-drag-placeholder"
+ style={{ minHeight: "20px", border: "1px dashed blue" }}
+ />
+ ) : (
+ <div>{/* node area */}</div>
+ )}
+ </div>
+);
自定义拖拽占位节点和 node box
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? <div className="my-drag-placeholder">drop here</div>
+ : <div className="mynode">{stat.node.name}</div>
+ }
+ </div>
+ ),
+ })
+ return <div>
+ {renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ <style>{\`
+ .mytree [data-node-box]{
+ padding: 5px 0;
+ }
+ .mytree [data-node-box]:hover{
+ background-color: #eee;
+ }
+ .mytree .he-tree-drag-placeholder{
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ border: 1px dashed red;
+ }
+ .mynode{
+ padding-left:5px;
+ }
+ \`}</style>
+ </div>
+}
节点的展开与折叠
openIds
表明展开的节点.stat.open
获取该节点的open
状态.useHeTree
返回的allIds
包含所有节点的 id.openParentsInFlatData
. 树形数据: openParentsInTreeData
.import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setopenIds(allIds)}>Open All</button>
+ <button onClick={() => setopenIds([])}>Close All</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
节点的勾选
checkedIds
表明勾选的节点.stat.checked
获取该节点的checked
状态.checked
变动后的checkedIds
. 扁平数据: updateCheckedInFlatData
. 树形数据: \`updateCheckedInTreeData. checked
的更新是级联的. 如果你不想级联更新, 使用你自己的逻辑替代.import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [checkedIds, setcheckedIds] = useState<Id[]>([]);
+ const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]);
+ const handleChecked = (id: Id, checked: boolean) => {
+ const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys);
+ setcheckedIds(r[0]);
+ setsemiCheckedIds(r[1]);
+ }
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ checkedIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} />
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ Checked: {JSON.stringify(checkedIds)} <br />
+ Semi-Checked: {JSON.stringify(semiCheckedIds)}
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
控制是否可拖拽, 可放入
canDrag
, 节点是否可拖拽.canDrop
, 节点是否可放入.canDropToRoot
, 树根是否可放入.import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined),
+ canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined),
+ canDropToRoot: (index) => false,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Technology
及子节点可以拖拽. Science
及子节点不可以拖拽.Science
及子节点可以放入. Technology
及子节点不可以放入.拖拽到节点上时打开节点
dragOpen
, 是否启用, 默认false
.dragOpenDelay
, 延时, 默认 600
毫秒.onDragOpen
, 打开节点时调用的函数.import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ dragOpen: true,
+ onDragOpen(stat) {
+ handleOpen(stat.id, true)
+ },
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
更新数据
immer
.npm install immer use-immer
pnpm add immer use-immer
yarn add immer use-immer
使用内置方法更新扁平数据
addToFlatData
: 增加节点. removeByIdInFlatData
: 删除节点. 这两个方法都会改变原数据, 所以把原数据的复制传给它, 或者与immer
一起使用.import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef, useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ let newData = [...data];
+ addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ setdata(newData);
+ }
+ const remove = (id: Id) => {
+ let newData = [...data];
+ removeByIdInFlatData(newData, id as number, keys)
+ setdata(newData);
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
使用 immer 更新扁平数据
useImmer
替代 React 的useState
.import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ });
+ }
+ const remove = (id: Id) => {
+ setdata(draft => {
+ removeByIdInFlatData(draft, id as number, keys)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ draft.find(node => node.id === id)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
使用 immer 更新树形数据
useImmer
替代 React 的useState
. findTreeData
方法类似数组的find
方法.import { useHeTree, findTreeData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const CHILDREN = 'children'
+ const keys = { idKey: 'id', childrenKey: CHILDREN };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]);
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], })
+ })
+ }
+ const remove = (id: Id, pid: Id | null) => {
+ setdata(draft => {
+ const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN]
+ children.splice(children.findIndex(t => t.id === id), 1)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'tree',
+ onChange: setdata,
+ renderNode: ({ id, pid, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id, pid)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
从外部发起的拖拽
onExternalDragOver
: 表明是否处理外部拖拽.onExternalDrop
: 当外部拖拽放入树中时调用的回调函数.import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react";
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ onExternalDragOver: (e) => true,
+ onExternalDrop: (e, parentStat, index) => {
+ setdata(draft => {
+ const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" }
+ addToFlatData(draft, newNode, index, keys)
+ })
+ },
+ })
+ return <div>
+ <button draggable={true}>Drag me in to the tree</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
超大数据
virtual
启用虚拟列表功能. 记得给树设置可见区域高度.import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
触摸 & 移动设备
其他
',5);function D(m,_,q,b,v,f){const i=n("DemoIframe");return t(),k("div",null,[p,a(i,{url:"/base_flat_data"}),E,a(i,{url:"/base_tree_data"}),e,a(i,{url:"/custom_drag_trigger_flat_data"}),d,a(i,{url:"/customize_placeholder_and_node_box"}),r,a(i,{url:"/open_ids"}),g,a(i,{url:"/checked_ids"}),y,a(i,{url:"/draggable_droppable"}),F,a(i,{url:"/dragopen"}),o,a(i,{url:"/update_data"}),c,a(i,{url:"/update_flat_data_with_immer"}),C,a(i,{url:"/update_tree_data_with_immer"}),B,a(i,{url:"/external_drag"}),A,a(i,{url:"/virtual_list"}),u])}const x=h(l,[["render",D]]);export{I as __pageData,x as default};
diff --git a/assets/zh_v1_guide.md.Cm8pKdlq.lean.js b/assets/zh_v1_guide.md.Cm8pKdlq.lean.js
new file mode 100644
index 0000000..c1e8f0a
--- /dev/null
+++ b/assets/zh_v1_guide.md.Cm8pKdlq.lean.js
@@ -0,0 +1 @@
+import{_ as h,E as n,c as k,J as a,a5 as s,o as t}from"./chunks/framework.BthLuVtL.js";const I=JSON.parse('{"title":"使用指南","description":"","frontmatter":{},"headers":[],"relativePath":"zh/v1/guide.md","filePath":"zh/v1/guide.md"}'),l={name:"zh/v1/guide.md"},p=s("",19),E=s("",2),e=s("",3),d=s("",9),r=s("",3),g=s("",4),y=s("",4),F=s("",5),o=s("",6),c=s("",3),C=s("",3),B=s("",4),A=s("",3),u=s("",5);function D(m,_,q,b,v,f){const i=n("DemoIframe");return t(),k("div",null,[p,a(i,{url:"/base_flat_data"}),E,a(i,{url:"/base_tree_data"}),e,a(i,{url:"/custom_drag_trigger_flat_data"}),d,a(i,{url:"/customize_placeholder_and_node_box"}),r,a(i,{url:"/open_ids"}),g,a(i,{url:"/checked_ids"}),y,a(i,{url:"/draggable_droppable"}),F,a(i,{url:"/dragopen"}),o,a(i,{url:"/update_data"}),c,a(i,{url:"/update_flat_data_with_immer"}),C,a(i,{url:"/update_tree_data_with_immer"}),B,a(i,{url:"/external_drag"}),A,a(i,{url:"/virtual_list"}),u])}const x=h(l,[["render",D]]);export{I as __pageData,x as default};
diff --git a/hashmap.json b/hashmap.json
new file mode 100644
index 0000000..43a1348
--- /dev/null
+++ b/hashmap.json
@@ -0,0 +1 @@
+{"zh_v1_api.md":"BHPyDvs2","zh_v1_examples.md":"N-IWpcWm","v1_api.md":"8ChaXgfD","zh_index.md":"Ct9xdv7q","index.md":"Dnn-p8GL","v1_examples.md":"DA1MhYZf","v1_guide.md":"DSikYvGc","zh_v1_guide.md":"Cm8pKdlq"}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..37bd354
--- /dev/null
+++ b/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ direction
: 从右往左显示.customDragImage
: 自定义 drag image.rootId
: 使用扁平数据时, 顶级节点的父 id.keepPlaceholder
: 拖拽到树外时, 是否要保留拖拽占位节点. 默认false
.scrollToNode
: 滚动到指定节点.Features
API
Exported
useHeTree
: Main React hook. This library does not export components, you need to use the renderTree
render tree returned by this function.walkTreeData
, walkTreeDataGenerator
, findTreeData
, filterTreeData
, openParentsInTreeData
, updateCheckedInTreeData
: Methods for processing and traversing tree data.sortFlatData
, walkFlatData
, walkFlatDataGenerator
, convertIndexToTreeIndexInFlatData
, addToFlatData
, removeByIdInFlatData
, openParentsInFlatData
, updateCheckedInFlatData
: Methods for processing and traversing flat data.walkParentsGenerator
: To iterate over another special kind of data. This data is like HTMLElement
, which contains a key pointing to the parent node like parentElement
.defaultProps
: The default value of useHeTree
options.Id
: node id, parent id. Type: string | number
.Stat
: Node information.HeTreeProps
: Options for useHeTree
.useHeTree
import { useHeTree } from "he-tree-react";
+const {/* return */} = useHeTree({/* options */}) // prettier-ignore
Name Type Default Description data Array Data. Check Data Types. dataType 'flat', 'tree' 'flat' Data Types idKey string 'id' key of id 名. parentIdKey string 'parent_id' key of the parent id. For flat data only. childrenKey string 'children' key of children nodes. For tree data only. indent number 20 Node indentation, unit is px. dragOpen boolean false Whether to enable the function "Open node when dragging over node". dragOpenDelay number 600 The waiting time to open the node when dragging over the node. The unit is milliseconds. onDragOpen function(stat): void
The callback of "Open node when dragging over node". direction 'lrt', 'rtl' 'ltr' Display direction, ltr is displayed from left to right, rtl is the opposite. rootId string, null null The parent id of a node that has not parent in flat data. virtual boolean false Whether to enable virtualization. Used to improve performance when there is a lot of data. keepPlaceholder boolean false Whether to retain placeholder when dragging out of the tree. It is recommended to enable this only on one tree page. openIds Array All open nodes' id. checkedIds Array All checked nodes' id. isFunctionReactive boolean false Whether to listen for change of the callback functions. Reference Name Type Description renderNode (stat)=> ReactNode
Node render. renderNodeBox ({stat, attrs, isPlaceholder})=> ReactNode
nodeBox's render. Reference. onChange (newData)=>void
Callback on data change canDrag (stat)=>boolean, null, undefined, void
Whether a node draggable. Returning null, undefined, void
means inheriting the parent node.canDrop (stat, index)=>boolean, null, undefined, void
Whether a node droppable. Returning null, undefined, void
means inheriting the parent node. The parameter index
may be empty. If it is not empty, it indicates the position.customDragImage (event, stat)=> void
Called event.dataTransfer.setDragImage
to custom drag image. Reference.onDragStart (event, stat)=> void
onExternalDragOver (event)=>boolean
Called when drag from external. Must return a Boolean value to indicate whether to handle this drag. onDragOver (event, stat, isExternal)=> void
isExternal
indicates whether the drag is from outside.onDragEnd (event, stat, isOutside)=>void
Called on dragend and this drag is started in this tree. stat
is the stat of the dragged node. isOutside
indicates whether it ended outside the tree.onExternalDrop (event, parentStat, index)=>void
Called when the external drag ends on this tree. parentStat is the stat of the target parent node, and when it is empty, it represents the root of the tree. Index is the target position, the index of the node among siblings. Return of
useHeTree
useHeTree
is an object, including some states and methods. Note, this object will change every time. Do not rely on this object, but you can rely on the properties of this object. The properties are as follows:Name Type Description renderTree (options?: { className?: string, style?: React.CSSProperties }): ReactNode
Tree render. Options can be passed in className
and style
to control the style of the root element.getStat (idOrNodeOrStat)=>stat
Get stat by id, or node data, or stat object. allIds Array The ids of all nodes. rootIds Array The ids of all root nodes rootNodes Array All root nodes. In tree data, it is same with options.data
.rootStats Array All root nodes' stat. placeholder {parentStat, index, level}
Drag placeholder info. Null if it does not exist. draggingStat stat
When a drag is initiated from this tree, the stat of the dragged node. Null if it does not exist. dragOverStat stat
Dragging over node's stat. May be null. visibleIds Array All visible nodes' id. attrsList Array All visible nodes' attrs. virtualListRef ref
ref
of virtual list component, Check virtual list.scrollToNode (idOrNodeOrStat)=>boolean
Scroll to node. The argument can be id, node or stat. If node not found or invisible, it return false
. ExamplewalkTreeDataGenerator
for of
. Executing skipChildren()
in the loop will skip all child nodes of the node, and executing exitWalk
will end the traversal.for (const [
+ node,
+ { parent, parents, siblings, index, skipChildren, exitWalk },
+] of walkTreeDataGenerator(data, "children")) {
+ // ...
+}
walkTreeData
skipChildren()
in the callback method will skip all child nodes of the node, and executing exitWalk
will end the traversal.walkTreeDataGenerator(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // ...
+ },
+ "children"
+);
findTreeData
Array.prototype.find
. Returns the first node found. Executing skipChildren()
in the callback method will skip all child nodes of the node, and executing exitWalk
will end the traversal.let foundNode = findTreeData(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // return node.id === 1;
+ },
+ "children"
+);
filterTreeData
Array.prototype.filter
. Returns all nodes found. Executing skipChildren()
in the callback method will skip all child nodes of the node, and executing exitWalk
will end the traversal.let nodes = filterTreeData(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // return node.id > 1;
+ },
+ "children"
+);
openParentsInTreeData
(
+ treeData,
+ openIds: Id[],
+ idOrIds: Id | Id[],
+ options?: {idKey: string, childrenKey: string}
+): newOpenIds
updateCheckedInTreeData
checked
status of a single node or multiple nodes. This will update both their children and parents. Reference.(
+ treeData,
+ checkedIds: Id[],
+ idOrIds: Id | Id[],
+ checked: boolean,
+ options?: {idKey: string, childrenKey: string}
+): [newCheckedIds, newSemiCheckedIds]
sortFlatData
(
+ flatData,
+ options?: {idKey: string, parentIdKey: string}
+): sortedData
walkFlatDataGenerator
for of
. Executing skipChildren()
in the loop will skip all the child nodes of the node, and executing exitWalk
will end the traversal. Make sure the order of your data is correct before using it.siblings
, but has treeIndex, id, pid
. treeIndex is the index of the node in the tree.for (const [
+ node,
+ { parent, parents, index, treeIndex, id, pid, skipChildren, exitWalk },
+] of walkFlatDataGenerator(flatData, {
+ idKey: "id",
+ parentIdKey: "parent_id",
+})) {
+ // ...
+}
walkFlatData
skipChildren()
in the callback method will skip all child nodes of the node, and executing exitWalk
will end the traversal. Before using, make sure that the order of your data is correct.walkFlatData(
+ flatData,
+ (
+ node,
+ { parent, parents, index, treeIndex, id, pid, skipChildren, exitWalk }
+ ) => {
+ // ...
+ },
+ {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+);
openParentsInFlatData
(
+ flatData,
+ openIds: Id[],
+ idOrIds: Id | Id[],
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): newOpenIds
updateCheckedInFlatData
checked
status of a single node or multiple nodes. This will update both their children and parents. Make sure your data is in the correct order before using it. Reference.(
+ flatData,
+ checkedIds: Id[],
+ idOrIds: Id | Id[],
+ checked: boolean,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): [newCheckedIds, newSemiCheckedIds]
convertIndexToTreeIndexInFlatData
(
+ flatData,
+ parentId: Id | null,
+ indexInSiblings: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): treeIndex
addToFlatData
useImmer
. Reference(
+ flatData,
+ newNode,
+ index: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+):void
removeByIdInFlatData
useImmer
. Reference(
+ flatData,
+ removeId: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): removedData
walkParentsGenerator
HTMLElement
, which contains keys pointing to the parent node like parentElement
.(
+ node,
+ parentKeyOrGetter: string | ((node) => parent | undefined),
+ options?: {
+ withSelf: boolean;
+ }
+): Generator
parentKeyOrGetter
can be a string or a method that returns the parent. options.withSelf
indicates whether to include the node self. Returns Generator. Here is an example of traversing HTMLElement:let el = document.querySelector("div");
+for (const parent of walkParentsGenerator(el, "parentElement", {
+ withSelf: true,
+})) {
+ // ...
+}
Stat
stat
contains information related to the node. Read-only. The properties are as follows:Name Type Description _isStat boolean Indicates whether it is a stat object node object node data id Id id pid Id, null parent id parent object, null parent data parentStat stat, null parent stat childIds Id[] children object[] childStats stat[] stats of children siblingIds Id[] siblings object[] sibling nodes siblingStats stat[] stats of siblings index number node's index in siblings level number node's depth in tree. Start from 1 open boolean checked boolean draggable boolean Examples
Custom Style
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree, placeholder } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key} className="my-node-box">
+ {isPlaceholder ? <div className="my-placeholder">DROP HERE</div>
+ : <div className="my-node">
+ <span className="drag-handler" draggable={stat.draggable}>{dragIcon()}</span>
+ {stat.node.name}
+ </div>
+ }
+ </div>
+ ),
+ })
+ return <>
+ <h3 style={{ margin: '0 0 0 110px', padding: '20px 0 0px' }}>Draggable Tree</h3>
+ <div>
+ {renderTree({ className: `my-tree ${placeholder ? 'dragging' : 'no-dragging'}` })}
+ </div>
+ <style>{`
+ .my-tree{
+ width: 300px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ margin: 20px;
+ padding: 20px;
+ }
+ .my-placeholder{
+ height:40px;
+ border: 1px dashed blue;
+ border-radius: 3px;
+ background-color: #f3ffff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: small;
+ }
+ /*.no-dragging .my-node-box:hover{
+ background-color: #eee;
+ }*/
+ .my-node-box:not(:last-child){
+ margin-bottom: 10px;
+ }
+ .my-node{
+ padding: 5px 10px;
+ padding-left: 30px;
+ border: 1px solid #e2e2e2;
+ border-radius: 3px;
+ background-color: #f0f0f0;
+ display: flex;
+ align-items: center;
+ position: relative;
+ box-shadow: 1px 1px 3px 0px rgb(0 0 0 / 19%);
+ }
+ .no-dragging .my-node:hover{
+ background-color: #ebfeff;
+ }
+ .drag-handler{
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 30px;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: grab;
+ }
+ .drag-handler:hover{
+ background-color: #f0f0f0;
+ }
+ .my-node svg{
+ width:16px;
+ }
+ `}</style>
+ </>
+}
+
+function dragIcon() {
+ return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>drag-horizontal-variant</title><path d="M21 11H3V9H21V11M21 13H3V15H21V13Z" /></svg>
+}
Flat Data
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Tree-shaped Data
import { useHeTree } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const [data, setdata] = useState(() => [
+ {
+ id: 1,
+ name: "Root Category",
+ children: [
+ {
+ id: 2,
+ name: "Technology",
+ children: [
+ {
+ id: 5,
+ name: "Hardware",
+ children: [
+ {
+ id: 10,
+ name: "Computer Components",
+ children: [],
+ },
+ ],
+ },
+ {
+ id: 4,
+ name: "Programming",
+ children: [
+ {
+ id: 8,
+ name: "Python",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 3,
+ name: "Science",
+ children: [
+ {
+ id: 7,
+ name: "Biology",
+ children: [],
+ },
+ {
+ id: 6,
+ name: "Physics",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]);
+ const { renderTree } = useHeTree({
+ data,
+ dataType: 'tree',
+ childrenKey: 'children',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Trigger Element
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button draggable={draggable}>Drag</button>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Placeholder
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? <div className="my-drag-placeholder">drop here</div>
+ : <div className="mynode">{stat.node.name}</div>
+ }
+ </div>
+ ),
+ })
+ return <div>
+ {renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ <style>{`
+ .mytree [data-node-box]{
+ padding: 5px 0;
+ }
+ .mytree [data-node-box]:hover{
+ background-color: #eee;
+ }
+ .mytree .he-tree-drag-placeholder{
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ border: 1px dashed red;
+ }
+ .mynode{
+ padding-left:5px;
+ }
+ `}</style>
+ </div>
+}
Open
import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setopenIds(allIds)}>Open All</button>
+ <button onClick={() => setopenIds([])}>Close All</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Checked
import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [checkedIds, setcheckedIds] = useState<Id[]>([]);
+ const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]);
+ const handleChecked = (id: Id, checked: boolean) => {
+ const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys);
+ setcheckedIds(r[0]);
+ setsemiCheckedIds(r[1]);
+ }
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ checkedIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} />
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ Checked: {JSON.stringify(checkedIds)} <br />
+ Semi-Checked: {JSON.stringify(semiCheckedIds)}
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Draggable & Droppable
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined),
+ canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined),
+ canDropToRoot: (index) => false,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Open when drag onto
import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ dragOpen: true,
+ onDragOpen(stat) {
+ handleOpen(stat.id, true)
+ },
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Flat Data
import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef, useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ let newData = [...data];
+ addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ setdata(newData);
+ }
+ const remove = (id: Id) => {
+ let newData = [...data];
+ removeByIdInFlatData(newData, id as number, keys)
+ setdata(newData);
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Flat Data with immer
import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ });
+ }
+ const remove = (id: Id) => {
+ setdata(draft => {
+ removeByIdInFlatData(draft, id as number, keys)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ draft.find(node => node.id === id)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Tree Data with immer
import { useHeTree, findTreeData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const CHILDREN = 'children'
+ const keys = { idKey: 'id', childrenKey: CHILDREN };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]);
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], })
+ })
+ }
+ const remove = (id: Id, pid: Id | null) => {
+ setdata(draft => {
+ const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN]
+ children.splice(children.findIndex(t => t.id === id), 1)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'tree',
+ onChange: setdata,
+ renderNode: ({ id, pid, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id, pid)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Drag from External
import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react";
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ onExternalDragOver: (e) => true,
+ onExternalDrop: (e, parentStat, index) => {
+ setdata(draft => {
+ const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" }
+ addToFlatData(draft, newNode, index, keys)
+ })
+ },
+ })
+ return <div>
+ <button draggable={true}>Drag me in to the tree</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Big Data
import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
Scroll to Node
import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds, scrollToNode } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => scrollToNode(910)}>Scroll to 910</button>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
Guide
Installation
npm install he-tree-react
pnpm add he-tree-react
yarn add he-tree-react
Data Types
null
means there is no parent. The order of flat data must be the same as the tree, you can use the sortFlatData
method to sort the data when initializing the data.[
+ { id: 1, pid: null },
+ { id: 2, pid: 1 },
+ { id: 3, pid: 2 },
+];
dataType: 'tree'
.[
+ {
+ id: 1,
+ children: [
+ {
+ id: 2,
+ children: [{ id: 3 }],
+ },
+ ],
+ },
+];
id, pid, children
in the data are not fixed. In the options, use idKey, parentIdKey, childrenKey
to indicate the corresponding key names in your data.No Components
useHeTree
. Use the returned renderTree
to render the tree. The advantage of this is that in addition to renderTree
, useHeTree
will also return some internal states and methods, which can be easily obtained.import { useHeTree } from "he-tree-react";
+
+export default function App() {
+ const { renderTree } = useHeTree({...})
+ return <div>
+ {renderTree()}
+ </div>
+}
Options
useHeTree
is the primary function used, its first parameter is an options object. The required options are data
, and at least one of renderNode, renderNodeBox
must be present. Other important options include:dataType
, indicating data type. Available values: flat
, default. Flat data.tree
, tree-shaped data.idKey, parentIdKey
, the default values are id
and parent_id
. Needed when using flat data. Although there are default values, it is better to explicitly state them.childrenKey
, the default is children
. Needed when using tree-shaped data. Although there are default values, it is better to explicitly state them.onChange
, a function called when data changes, the parameter is new data. If your tree will not change then this is not required.isFunctionReactive
, boolean. Default false
. useHeTree
options include many callback functions, such as onChange, canDrop
. isFunctionReactive
can be used to control whether to listen for changes to these callback functions. If your callback functions and data
change synchronously, you do not need to enable this. Otherwise, you need to enable this and use React's useCallback
or useMemo
to cache all your callback functions to avoid performance issues.useHeTree
API documentation for more information.Tips
stat
, information related to a single node. Most of the parameters in callback functions have stat
. Refer to Stat
API.node
, the data of the node. You can get node data through stat.node
.getStat
, through this function you can get stat
, the only parameter can be id, node, stat
. This function is in the return object of useHeTree
: const {getStat} = useHeTree({...})
.tsx
format, if you need the js
format, you can use any ts js online converter.Use Flat Data
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Use Tree-shaped Data
import { useHeTree } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const [data, setdata] = useState(() => [
+ {
+ id: 1,
+ name: "Root Category",
+ children: [
+ {
+ id: 2,
+ name: "Technology",
+ children: [
+ {
+ id: 5,
+ name: "Hardware",
+ children: [
+ {
+ id: 10,
+ name: "Computer Components",
+ children: [],
+ },
+ ],
+ },
+ {
+ id: 4,
+ name: "Programming",
+ children: [
+ {
+ id: 8,
+ name: "Python",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 3,
+ name: "Science",
+ children: [
+ {
+ id: 7,
+ name: "Biology",
+ children: [],
+ },
+ {
+ id: 6,
+ name: "Physics",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]);
+ const { renderTree } = useHeTree({
+ data,
+ dataType: 'tree',
+ childrenKey: 'children',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Custom Drag Trigger Element
draggable
attribute to any child element of the node.import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button draggable={draggable}>Drag</button>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
HTML and Style of Node
<div
+ draggable="true"
+ data-key="1"
+ data-level="1"
+ data-node-box="true"
+ style="padding-left: 0px;"
+>
+ <div>Node</div>
+</div>
div
above. Use the renderNode
option to control the rendering of the inner div. For example: renderNode: ({node}) => <div>{node.name}</div>
.nodeBox
, don't modify its padding-left, padding-right
. Use the indent
option to control the indentation of the node. If you want to control the rendering of nodeBox
or the drag placeholder, you can use the renderNodeBox
option, which will override renderNode
. The standard renderNodeBox
is as follows:renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? (
+ <div
+ className="he-tree-drag-placeholder"
+ style={{ minHeight: "20px", border: "1px dashed blue" }}
+ />
+ ) : (
+ <div>{/* node area */}</div>
+ )}
+ </div>
+);
Custom Drag Placeholder and Node Box
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? <div className="my-drag-placeholder">drop here</div>
+ : <div className="mynode">{stat.node.name}</div>
+ }
+ </div>
+ ),
+ })
+ return <div>
+ {renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ <style>{`
+ .mytree [data-node-box]{
+ padding: 5px 0;
+ }
+ .mytree [data-node-box]:hover{
+ background-color: #eee;
+ }
+ .mytree .he-tree-drag-placeholder{
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ border: 1px dashed red;
+ }
+ .mynode{
+ padding-left:5px;
+ }
+ `}</style>
+ </div>
+}
Open & Close
openIds
option to indicate the open nodes.open
status of the node can be obtained through stat.open
.allIds
returned by useHeTree
contains the ids of all nodes.openParentsInFlatData
. For tree data: openParentsInTreeData
.import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setopenIds(allIds)}>Open All</button>
+ <button onClick={() => setopenIds([])}>Close All</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Checked
checkedIds
to indicate the checked nodes.checked
status of this node can be obtained through stat.checked
.checkedIds
for one or more nodes after the checked
status changes. Flat data: updateCheckedInFlatData
. Tree data: `updateCheckedInTreeData. checked
is cascading. If you don't want to cascade updates, replace it with your own logic.import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [checkedIds, setcheckedIds] = useState<Id[]>([]);
+ const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]);
+ const handleChecked = (id: Id, checked: boolean) => {
+ const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys);
+ setcheckedIds(r[0]);
+ setsemiCheckedIds(r[1]);
+ }
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ checkedIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} />
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ Checked: {JSON.stringify(checkedIds)} <br />
+ Semi-Checked: {JSON.stringify(semiCheckedIds)}
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
draggable & droppable
canDrag
, whether the node can be dragged.canDrop
, whether the node can be dropped.canDropToRoot
, whether the tree root can be dropped.import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined),
+ canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined),
+ canDropToRoot: (index) => false,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Technology
and its sub-nodes can be dragged. Science
and its sub-nodes cannot be dragged.Science
and its sub-nodes can be dropped. Technology
and its sub-nodes cannot be dropped.Open when dragging over
dragOpen
, whether to enable, default false
.dragOpenDelay
, delay, default 600
milliseconds.onDragOpen
, the function called when the node is opened.import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ dragOpen: true,
+ onDragOpen(stat) {
+ handleOpen(stat.id, true)
+ },
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Data
immer
.npm install immer use-immer
pnpm add immer use-immer
yarn add immer use-immer
Update Flat Data
addToFlatData
. removeByIdInFlatData
. These 2 methods will modify original data, so pass copy to it, or use immer
.import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef, useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ let newData = [...data];
+ addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ setdata(newData);
+ }
+ const remove = (id: Id) => {
+ let newData = [...data];
+ removeByIdInFlatData(newData, id as number, keys)
+ setdata(newData);
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Flat Data with immer
useImmer
instead of React's useState
.import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ });
+ }
+ const remove = (id: Id) => {
+ setdata(draft => {
+ removeByIdInFlatData(draft, id as number, keys)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ draft.find(node => node.id === id)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Update Tree Data with immer
useImmer
instead of React's useState
. findTreeData
is like Array.prototype.find
.import { useHeTree, findTreeData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const CHILDREN = 'children'
+ const keys = { idKey: 'id', childrenKey: CHILDREN };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]);
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], })
+ })
+ }
+ const remove = (id: Id, pid: Id | null) => {
+ setdata(draft => {
+ const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN]
+ children.splice(children.findIndex(t => t.id === id), 1)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'tree',
+ onChange: setdata,
+ renderNode: ({ id, pid, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id, pid)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Drag from External
onExternalDragOver
: Indicate whether to handle external drag.onExternalDrop
: Callback when drop from external.import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react";
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ onExternalDragOver: (e) => true,
+ onExternalDrop: (e, parentStat, index) => {
+ setdata(draft => {
+ const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" }
+ addToFlatData(draft, newNode, index, keys)
+ })
+ },
+ })
+ return <div>
+ <button draggable={true}>Drag me in to the tree</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Big Data
virtual
to enable virtual list. Remember to set height for tree.import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
Touch & Mobile Device
Others
direction
: from right to left.customDragImage
: custom drag image.rootId
: the parent id of root nodes in flat data.keepPlaceholder
: whether to retain the drag placeholder node when dragging outside the tree. Default is false
.scrollToNode
: Scroll to a node.API
导出
useHeTree
: 主要的 React hook. 本库没有导出组件, 你需要使用此函数返回的renderTree
渲染树.walkTreeData
, walkTreeDataGenerator
, findTreeData
, filterTreeData
, openParentsInTreeData
, updateCheckedInTreeData
: 用来处理和遍历树形数据的方法.sortFlatData
, walkFlatData
, walkFlatDataGenerator
, convertIndexToTreeIndexInFlatData
, addToFlatData
, removeByIdInFlatData
, openParentsInFlatData
, updateCheckedInFlatData
: 用来处理和遍历扁平数据的方法.walkParentsGenerator
: 遍历另一种特殊数据的方法. 这种数据类似HTMLElement
, 其中包含类似于parentElement
的指向父节点的键.defaultProps
: useHeTree
的选项的默认值.Id
: 节点 id, 父级 id. 类型: string | number
.Stat
: 节点的相关信息.HeTreeProps
: useHeTree
的选项.useHeTree
import { useHeTree } from "he-tree-react";
+const {/* return */} = useHeTree({/* options */}) // prettier-ignore
名称 类型 默认值 描述 data Array 数据. 参考数据类型. dataType 'flat', 'tree' 'flat' 数据类型 idKey string 'id' 你的数据中 id 的键名. parentIdKey string 'parent_id' 你的数据中父级 id 的键名. 仅用于扁平数据. childrenKey string 'children' 你的数据中子级的键名. 仅用于树形数据. indent number 20 节点缩进, 单位是 px. dragOpen boolean false 是否启用功能"拖拽到节点上时打开节点". dragOpenDelay number 600 拖拽到节点上时打开节点的等待时间. 单位是毫秒. onDragOpen function(stat): void
拖拽到节点上时打开节点的回调. direction 'lrt', 'rtl' 'ltr' 显示方向, ltr 是从左往右显示, rtl 与之相反. rootId string, null null 使用扁平数据时, 没有父级的节点的父级 id. virtual boolean false 是否启用虚拟化. 当数据非常多时用来提高性能. keepPlaceholder boolean false 当拖拽离开树的范围, 是否要保留占位元素. 建议只在一个树的页面开启此项. openIds Array 所有打开节点的 id. checkedIds Array 所有勾选的节点的 id. isFunctionReactive boolean false 是否监听回调函数的改变. 参考 名称 类型 描述 renderNode (stat)=> ReactNode
节点的渲染函数. renderNodeBox ({stat, attrs, isPlaceholder})=> ReactNode
nodeBox 的渲染函数. 参考. onChange (newData)=>void
数据发生改变时调用. canDrag (stat)=>boolean, null, undefined, void
节点是否可拖拽. 返回 null, undefined, void
表示继承父节点.canDrop (stat, index)=>boolean, null, undefined, void
节点是否可放入. 返回 null, undefined, void
表示继承父节点. 参数index
可能为空, 不为空时表示将要放入节点的子级的位置.customDragImage (event, stat)=> void
调用 event.dataTransfer.setDragImage
自定义 drag image. 参考.onDragStart (event, stat)=> void
当拖拽开始时 onExternalDragOver (event)=>boolean
当拖拽来自外部时调用. 你必选返回布尔值表示是否处理此拖拽. onDragOver (event, stat, isExternal)=> void
当拖拽到树上方时, isExternal
表示此次拖拽是否来自外部.onDragEnd (event, stat, isOutside)=>void
当此树发起的拖拽结束时调用. stat 是此次拖拽的节点的 stat.isOutside 表示是否在树外部结束. onExternalDrop (event, parentStat, index)=>void
当外部拖拽在此树结束时调用. parentStat 是目标父节点的 stat, 为空时代表树的根级. index 是目标位置, 即节点在兄弟节点中的索引. useHeTree
的返回 useHeTree
的返回是对象, 包含了一些 states 和方法. 注意, 这个对象每次更新都会改变, 不要依赖这个对象, 可以依赖这个对象的属性. 属性如下:名称 类型 描述 renderTree (options?: { className?: string, style?: React.CSSProperties }): ReactNode
渲染树. 参数可以传入 className
和style
控制根元素的样式.getStat (idOrNodeOrStat)=>stat
根据 id, 节点数据或 stat, 获得对应的 stat. allIds 数组 所有节点的 id. rootIds 数组 树根级的所有节点的 id. rootNodes 数组 树根级的所有节点的数据. 如果是树形数据, 它就是选项中的 data
.rootStats 数组 树根级的所有节点的 stat. placeholder {parentStat, index, level}
拖拽时占位节点的信息. 占位节点不存在时为空. draggingStat stat
由此树发起拖拽时, 被拖拽的节点的 stat. 不存在时为空. dragOverStat stat
拖拽到其上面的节点. 可能为空. visibleIds 数组 显示的所有节点的 id. attrsList 数组 显示的所有节点的 attrs. virtualListRef ref
虚拟列表组件的 ref, 参考虚拟列表. scrollToNode (idOrNodeOrStat)=>boolean
滚动到节点. 参数可以是 id, 节点数据或 stat. 如果节点未找到或未显示, 返回 false
. 例子walkTreeDataGenerator
for of
遍历树形数据的方法. 循环中执行skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历.for (const [
+ node,
+ { parent, parents, siblings, index, skipChildren, exitWalk },
+] of walkTreeDataGenerator(data, "children")) {
+ // ...
+}
walkTreeData
skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历.walkTreeDataGenerator(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // ...
+ },
+ "children"
+);
findTreeData
Array.prototype.find
. 返回找到的第一个节点. 回调方法中执行skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历.let foundNode = findTreeData(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // return node.id === 1;
+ },
+ "children"
+);
filterTreeData
Array.prototype.filter
. 返回找到的所有节点. 回调方法中执行skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历.let nodes = filterTreeData(
+ data,
+ (node, { parent, parents, siblings, index, skipChildren, exitWalk }) => {
+ // return node.id > 1;
+ },
+ "children"
+);
openParentsInTreeData
(
+ treeData,
+ openIds: Id[],
+ idOrIds: Id | Id[],
+ options?: {idKey: string, childrenKey: string}
+): newOpenIds
updateCheckedInTreeData
checked
状态. 这将同时更新它们的子节点和父节点. 参考.(
+ treeData,
+ checkedIds: Id[],
+ idOrIds: Id | Id[],
+ checked: boolean,
+ options?: {idKey: string, childrenKey: string}
+): [newCheckedIds, newSemiCheckedIds]
sortFlatData
(
+ flatData,
+ options?: {idKey: string, parentIdKey: string}
+): sortedData
walkFlatDataGenerator
for of
遍历扁平数据的方法. 循环中执行skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历. 使用前需确保你的数据的顺序是正确的.walkTreeDataGenerator
, 少了siblings
, 多了 treeIndex, id, pid
. treeIndex
是节点在整个树中的索引.for (const [
+ node,
+ { parent, parents, index, treeIndex, id, pid, skipChildren, exitWalk },
+] of walkFlatDataGenerator(flatData, {
+ idKey: "id",
+ parentIdKey: "parent_id",
+})) {
+ // ...
+}
walkFlatData
skipChildren()
将跳过该节点的所有子节点, 执行exitWalk
将结束遍历. 使用前需确保你的数据的顺序是正确的.walkFlatData(
+ flatData,
+ (
+ node,
+ { parent, parents, index, treeIndex, id, pid, skipChildren, exitWalk }
+ ) => {
+ // ...
+ },
+ {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+);
openParentsInFlatData
(
+ flatData,
+ openIds: Id[],
+ idOrIds: Id | Id[],
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): newOpenIds
updateCheckedInFlatData
checked
状态. 这将同时更新它们的子节点和父节点. 用前需确保你的数据的顺序是正确的. 参考.(
+ flatData,
+ checkedIds: Id[],
+ idOrIds: Id | Id[],
+ checked: boolean,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): [newCheckedIds, newSemiCheckedIds]
convertIndexToTreeIndexInFlatData
(
+ flatData,
+ parentId: Id | null,
+ indexInSiblings: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): treeIndex
addToFlatData
useImmer
一起使用. 参考(
+ flatData,
+ newNode,
+ index: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+):void
removeByIdInFlatData
useImmer
一起使用. 参考(
+ flatData,
+ removeId: Id | null,
+ options?: {
+ idKey: "id",
+ parentIdKey: "parent_id",
+ }
+): removedData
walkParentsGenerator
HTMLElement
, 其中包含类似于parentElement
的指向父节点的键.(
+ node,
+ parentKeyOrGetter: string | ((node) => parent | undefined),
+ options?: {
+ withSelf: boolean;
+ }
+): Generator
parentKeyOrGetter
可以是字符串或者返回父级的方法. options.withSelf
表示是否包括传入的节点. 返回 Generator. 下面是遍历 HTMLElement 的例子:let el = document.querySelector("div");
+for (const parent of walkParentsGenerator(el, "parentElement", {
+ withSelf: true,
+})) {
+ // ...
+}
Stat
stat
包括和节点有关的信息. 只读. 属性如下:名称 类型 描述 _isStat boolean 表明是否是 stat 对象 node object 节点的数据 id Id id pid Id, null 节点的父级 id parent object, null 父节点的数据 parentStat stat, null 父节点的 stat childIds Id[] 子节点的 id 数组 children object[] 子节点数组 childStats stat[] 子节点的 stat 数组 siblingIds Id[] 兄弟节点的 id 数组 siblings object[] 兄弟节点数组 siblingStats stat[] 兄弟节点的 stat 数组 index number 节点在兄弟节点中的索引 level number 节点在树中的深度. 从 1 开始 open boolean 是否展开 checked boolean 是否勾选 draggable boolean 是否可拖动 示例
自定义样式
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree, placeholder } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key} className="my-node-box">
+ {isPlaceholder ? <div className="my-placeholder">DROP HERE</div>
+ : <div className="my-node">
+ <span className="drag-handler" draggable={stat.draggable}>{dragIcon()}</span>
+ {stat.node.name}
+ </div>
+ }
+ </div>
+ ),
+ })
+ return <>
+ <h3 style={{ margin: '0 0 0 110px', padding: '20px 0 0px' }}>Draggable Tree</h3>
+ <div>
+ {renderTree({ className: `my-tree ${placeholder ? 'dragging' : 'no-dragging'}` })}
+ </div>
+ <style>{`
+ .my-tree{
+ width: 300px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ margin: 20px;
+ padding: 20px;
+ }
+ .my-placeholder{
+ height:40px;
+ border: 1px dashed blue;
+ border-radius: 3px;
+ background-color: #f3ffff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: small;
+ }
+ /*.no-dragging .my-node-box:hover{
+ background-color: #eee;
+ }*/
+ .my-node-box:not(:last-child){
+ margin-bottom: 10px;
+ }
+ .my-node{
+ padding: 5px 10px;
+ padding-left: 30px;
+ border: 1px solid #e2e2e2;
+ border-radius: 3px;
+ background-color: #f0f0f0;
+ display: flex;
+ align-items: center;
+ position: relative;
+ box-shadow: 1px 1px 3px 0px rgb(0 0 0 / 19%);
+ }
+ .no-dragging .my-node:hover{
+ background-color: #ebfeff;
+ }
+ .drag-handler{
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 30px;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: grab;
+ }
+ .drag-handler:hover{
+ background-color: #f0f0f0;
+ }
+ .my-node svg{
+ width:16px;
+ }
+ `}</style>
+ </>
+}
+
+function dragIcon() {
+ return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>drag-horizontal-variant</title><path d="M21 11H3V9H21V11M21 13H3V15H21V13Z" /></svg>
+}
扁平数据
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
树形数据
import { useHeTree } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const [data, setdata] = useState(() => [
+ {
+ id: 1,
+ name: "Root Category",
+ children: [
+ {
+ id: 2,
+ name: "Technology",
+ children: [
+ {
+ id: 5,
+ name: "Hardware",
+ children: [
+ {
+ id: 10,
+ name: "Computer Components",
+ children: [],
+ },
+ ],
+ },
+ {
+ id: 4,
+ name: "Programming",
+ children: [
+ {
+ id: 8,
+ name: "Python",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 3,
+ name: "Science",
+ children: [
+ {
+ id: 7,
+ name: "Biology",
+ children: [],
+ },
+ {
+ id: 6,
+ name: "Physics",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]);
+ const { renderTree } = useHeTree({
+ data,
+ dataType: 'tree',
+ childrenKey: 'children',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
触发元素
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button draggable={draggable}>Drag</button>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
占位元素
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? <div className="my-drag-placeholder">drop here</div>
+ : <div className="mynode">{stat.node.name}</div>
+ }
+ </div>
+ ),
+ })
+ return <div>
+ {renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ <style>{`
+ .mytree [data-node-box]{
+ padding: 5px 0;
+ }
+ .mytree [data-node-box]:hover{
+ background-color: #eee;
+ }
+ .mytree .he-tree-drag-placeholder{
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ border: 1px dashed red;
+ }
+ .mynode{
+ padding-left:5px;
+ }
+ `}</style>
+ </div>
+}
展开
import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setopenIds(allIds)}>Open All</button>
+ <button onClick={() => setopenIds([])}>Close All</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
勾选
import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [checkedIds, setcheckedIds] = useState<Id[]>([]);
+ const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]);
+ const handleChecked = (id: Id, checked: boolean) => {
+ const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys);
+ setcheckedIds(r[0]);
+ setsemiCheckedIds(r[1]);
+ }
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ checkedIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} />
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ Checked: {JSON.stringify(checkedIds)} <br />
+ Semi-Checked: {JSON.stringify(semiCheckedIds)}
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
拖拽控制
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined),
+ canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined),
+ canDropToRoot: (index) => false,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
拖拽时打开
import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ dragOpen: true,
+ onDragOpen(stat) {
+ handleOpen(stat.id, true)
+ },
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
更新扁平数据
import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef, useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ let newData = [...data];
+ addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ setdata(newData);
+ }
+ const remove = (id: Id) => {
+ let newData = [...data];
+ removeByIdInFlatData(newData, id as number, keys)
+ setdata(newData);
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
更新扁平数据使用 immer
import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ });
+ }
+ const remove = (id: Id) => {
+ setdata(draft => {
+ removeByIdInFlatData(draft, id as number, keys)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ draft.find(node => node.id === id)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
更新树形数据使用 immer
import { useHeTree, findTreeData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const CHILDREN = 'children'
+ const keys = { idKey: 'id', childrenKey: CHILDREN };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]);
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], })
+ })
+ }
+ const remove = (id: Id, pid: Id | null) => {
+ setdata(draft => {
+ const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN]
+ children.splice(children.findIndex(t => t.id === id), 1)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'tree',
+ onChange: setdata,
+ renderNode: ({ id, pid, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id, pid)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
外部拖拽交互
import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react";
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ onExternalDragOver: (e) => true,
+ onExternalDrop: (e, parentStat, index) => {
+ setdata(draft => {
+ const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" }
+ addToFlatData(draft, newNode, index, keys)
+ })
+ },
+ })
+ return <div>
+ <button draggable={true}>Drag me in to the tree</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
大数据, 虚拟列表
import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
滚动到节点
import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds, scrollToNode } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => scrollToNode(910)}>Scroll to 910</button>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
使用指南
安装
npm install he-tree-react
pnpm add he-tree-react
yarn add he-tree-react
数据类型
id
, 父级 id, null
代表没有父级. 扁平数据的顺序必须跟树一样, 你可以在初始化数据时使用sortFlatData
方法给数据排序.[
+ { id: 1, pid: null },
+ { id: 2, pid: 1 },
+ { id: 3, pid: 2 },
+];
children
数组包含子节点. 如果未指定id
, 此库将使用节点在树中的索引作为id
. 使用树形数据时需设置dataType: 'tree'
.[
+ {
+ id: 1,
+ children: [
+ {
+ id: 2,
+ children: [{ id: 3 }],
+ },
+ ],
+ },
+];
id, pid, children
不是固定的. 在设置中, 使用idKey, parentIdKey, childrenKey
表明你的数据中的对应键名.没有组件
useHeTree
. 使用它返回的renderTree
渲染树. 这样做的好处是除了renderTree
, useHeTree
还会返回一些内部状态和方法, 可以轻松的被获取.import { useHeTree } from "he-tree-react";
+
+export default function App() {
+ const { renderTree } = useHeTree({...})
+ return <div>
+ {renderTree()}
+ </div>
+}
选项
useHeTree
是主要使用的函数, 它的第一个参数是选项对象. 必须的选项有data
, 必须两者中有一个的是renderNode, renderNodeBox
. 其他重要选项是:dataType
, 表明数据类型. 可用值: flat
, 默认. 扁平数据.tree
, 树形数据.idKey, parentIdKey
, 默认值是id
和parent_id
. 使用扁平数据时需要. 虽然有默认值, 但还是建议写明更好.childrenKey
, 默认是children
. 使用树形数据时需要. 虽然有默认值, 但还是建议写明更好.onChange
, 数据改变时调用的函数, 参数是新数据. 如果你的树不会改变则不需要.isFunctionReactive
, 布尔. 默认false
. useHeTree
选项中包含许多回调函数, 如onChange, canDrop
. isFunctionReactive
可用来控制是否监听这些回调函数的改变. 如果你的回调函数和data
是同步改变的, 则不用启用此项. 否则你需要启用此项, 并且用 React 的useCallback
或useMemo
缓存你的所有回调函数以避免性能问题.提示
stat
, 单个节点的相关信息. 大部分回调函数的参数里有stat
. 参考Stat
API.node
, 节点的数据. 通过stat.node
可以获取节点数据.getStat
, 通过此函数可以获取stat
, 唯一参数可以是id, node, stat
. 此函数在useHeTree
的返回对象中: const {getStat} = useHeTree({...})
.tsx
格式, 如果你需要js
格式, 可以使用任意 ts js 在线转换器.基础使用-扁平数据
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ const [data, setdata] = useState(() => sortFlatData([
+ {
+ id: 1,
+ parent_id: null,
+ name: "Root Category",
+ },
+ {
+ id: 2,
+ parent_id: 1,
+ name: "Technology",
+ },
+ {
+ id: 5,
+ parent_id: 2,
+ name: "Hardware",
+ },
+ {
+ id: 10,
+ parent_id: 5,
+ name: "Computer Components",
+ },
+ {
+ id: 4,
+ parent_id: 2,
+ name: "Programming",
+ },
+ {
+ id: 8,
+ parent_id: 4,
+ name: "Python",
+ },
+ {
+ id: 3,
+ parent_id: 1,
+ name: "Science",
+ },
+ {
+ id: 7,
+ parent_id: 3,
+ name: "Biology",
+ },
+ {
+ id: 6,
+ parent_id: 3,
+ name: "Physics",
+ },
+ ], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
基础使用-树形数据
import { useHeTree } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const [data, setdata] = useState(() => [
+ {
+ id: 1,
+ name: "Root Category",
+ children: [
+ {
+ id: 2,
+ name: "Technology",
+ children: [
+ {
+ id: 5,
+ name: "Hardware",
+ children: [
+ {
+ id: 10,
+ name: "Computer Components",
+ children: [],
+ },
+ ],
+ },
+ {
+ id: 4,
+ name: "Programming",
+ children: [
+ {
+ id: 8,
+ name: "Python",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 3,
+ name: "Science",
+ children: [
+ {
+ id: 7,
+ name: "Biology",
+ children: [],
+ },
+ {
+ id: 6,
+ name: "Physics",
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]);
+ const { renderTree } = useHeTree({
+ data,
+ dataType: 'tree',
+ childrenKey: 'children',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked }) => <div>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
自定义拖拽触发元素
draggable
属性即可.import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button draggable={draggable}>Drag</button>
+ {node.name}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
节点 HTML 结构和样式
<div
+ draggable="true"
+ data-key="1"
+ data-level="1"
+ data-node-box="true"
+ style="padding-left: 0px;"
+>
+ <div>Node</div>
+</div>
renderNode
选项控制内层 div 的渲染. 如: renderNode: ({node}) => <div>{node.name}</div>
.nodeBox
, 不要修改它的padding-left, padding-right
. 使用选项indent
控制节点的缩进. 如果你想控制nodeBox
或拖拽占位节点的渲染, 可以使用renderNodeBox
选项, 这将覆盖renderNode
. 标准的renderNodeBox
如下:renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? (
+ <div
+ className="he-tree-drag-placeholder"
+ style={{ minHeight: "20px", border: "1px dashed blue" }}
+ />
+ ) : (
+ <div>{/* node area */}</div>
+ )}
+ </div>
+);
自定义拖拽占位节点和 node box
import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
+ <div {...attrs} key={attrs.key}>
+ {isPlaceholder ? <div className="my-drag-placeholder">drop here</div>
+ : <div className="mynode">{stat.node.name}</div>
+ }
+ </div>
+ ),
+ })
+ return <div>
+ {renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ <style>{`
+ .mytree [data-node-box]{
+ padding: 5px 0;
+ }
+ .mytree [data-node-box]:hover{
+ background-color: #eee;
+ }
+ .mytree .he-tree-drag-placeholder{
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ border: 1px dashed red;
+ }
+ .mynode{
+ padding-left:5px;
+ }
+ `}</style>
+ </div>
+}
节点的展开与折叠
openIds
表明展开的节点.stat.open
获取该节点的open
状态.useHeTree
返回的allIds
包含所有节点的 id.openParentsInFlatData
. 树形数据: openParentsInTreeData
.import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setopenIds(allIds)}>Open All</button>
+ <button onClick={() => setopenIds([])}>Close All</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button>
+ <button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
节点的勾选
checkedIds
表明勾选的节点.stat.checked
获取该节点的checked
状态.checked
变动后的checkedIds
. 扁平数据: updateCheckedInFlatData
. 树形数据: `updateCheckedInTreeData. checked
的更新是级联的. 如果你不想级联更新, 使用你自己的逻辑替代.import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [checkedIds, setcheckedIds] = useState<Id[]>([]);
+ const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]);
+ const handleChecked = (id: Id, checked: boolean) => {
+ const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys);
+ setcheckedIds(r[0]);
+ setsemiCheckedIds(r[1]);
+ }
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ checkedIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} />
+ {node.name} - {id}
+ </div>,
+ })
+ return <div>
+ Checked: {JSON.stringify(checkedIds)} <br />
+ Semi-Checked: {JSON.stringify(semiCheckedIds)}
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
控制是否可拖拽, 可放入
canDrag
, 节点是否可拖拽.canDrop
, 节点是否可放入.canDropToRoot
, 树根是否可放入.import { useHeTree, sortFlatData } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined),
+ canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined),
+ canDropToRoot: (index) => false,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
Technology
及子节点可以拖拽. Science
及子节点不可以拖拽.Science
及子节点可以放入. Technology
及子节点不可以放入.拖拽到节点上时打开节点
dragOpen
, 是否启用, 默认false
.dragOpenDelay
, 延时, 默认 600
毫秒.onDragOpen
, 打开节点时调用的函数.import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {node.name} - {id}
+ </div>,
+ dragOpen: true,
+ onDragOpen(stat) {
+ handleOpen(stat.id, true)
+ },
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
更新数据
immer
.npm install immer use-immer
pnpm add immer use-immer
yarn add immer use-immer
使用内置方法更新扁平数据
addToFlatData
: 增加节点. removeByIdInFlatData
: 删除节点. 这两个方法都会改变原数据, 所以把原数据的复制传给它, 或者与immer
一起使用.import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef, useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ let newData = [...data];
+ addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ setdata(newData);
+ }
+ const remove = (id: Id) => {
+ let newData = [...data];
+ removeByIdInFlatData(newData, id as number, keys)
+ setdata(newData);
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
使用 immer 更新扁平数据
useImmer
替代 React 的useState
.import {
+ useHeTree, sortFlatData,
+ addToFlatData, removeByIdInFlatData
+} from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys)
+ });
+ }
+ const remove = (id: Id) => {
+ setdata(draft => {
+ removeByIdInFlatData(draft, id as number, keys)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ draft.find(node => node.id === id)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
使用 immer 更新树形数据
useImmer
替代 React 的useState
. findTreeData
方法类似数组的find
方法.import { useHeTree, findTreeData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useRef } from 'react';
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const CHILDREN = 'children'
+ const keys = { idKey: 'id', childrenKey: CHILDREN };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]);
+ const add = (pid: Id) => {
+ let id = parseInt(Math.random().toString().substring(2, 5));
+ setdata(draft => {
+ findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], })
+ })
+ }
+ const remove = (id: Id, pid: Id | null) => {
+ setdata(draft => {
+ const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN]
+ children.splice(children.findIndex(t => t.id === id), 1)
+ })
+ }
+ const edit = (id: Id) => {
+ let newName = prompt("Enter new name")
+ setdata(draft => {
+ if (newName) {
+ findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName
+ }
+ })
+ }
+ const initialData = useRef<typeof data>();
+ initialData.current = initialData.current || data;
+ const { renderTree } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'tree',
+ onChange: setdata,
+ renderNode: ({ id, pid, node, draggable }) => <div>
+ <button draggable={draggable}>👉</button>
+ {node.name} - {id} -
+ <button onClick={() => add(id)}>+</button>
+ <button onClick={() => remove(id, pid)}>-</button>
+ <button onClick={() => edit(id)}>Edit</button>
+ </div>,
+ })
+ return <div>
+ <button onClick={() => setdata(initialData.current!)}>Restore</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
从外部发起的拖拽
onExternalDragOver
: 表明是否处理外部拖拽.onExternalDrop
: 当外部拖拽放入树中时调用的回调函数.import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react";
+import { useImmer } from "use-immer";
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'parent_id' };
+ // prettier-ignore
+ const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ {node.name} - {id}
+ </div>,
+ onExternalDragOver: (e) => true,
+ onExternalDrop: (e, parentStat, index) => {
+ setdata(draft => {
+ const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" }
+ addToFlatData(draft, newNode, index, keys)
+ })
+ },
+ })
+ return <div>
+ <button draggable={true}>Drag me in to the tree</button>
+ {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
超大数据
virtual
启用虚拟列表功能. 记得给树设置可见区域高度.import { useHeTree, sortFlatData } from "he-tree-react";
+import type { Id } from "he-tree-react";
+import { useState } from 'react';
+
+export default function BasePage() {
+ const keys = { idKey: 'id', parentIdKey: 'pid' };
+ // prettier-ignore
+ const [data, setdata] = useState(() => sortFlatData(createData(), keys));
+ const [openIds, setopenIds] = useState<Id[] | undefined>([]);
+ const handleOpen = (id: Id, open: boolean) => {
+ if (open) {
+ setopenIds([...(openIds || allIds), id]);
+ } else {
+ setopenIds((openIds || allIds).filter((i) => i !== id));
+ }
+ }
+ const { renderTree, allIds } = useHeTree({
+ ...keys,
+ data,
+ dataType: 'flat',
+ onChange: setdata,
+ openIds,
+ virtual: true,
+ renderNode: ({ id, node, open, checked, draggable }) => <div>
+ <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
+ {id}
+ </div>,
+ })
+ return <div>
+ {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
+ </div>
+}
+
+// generate 10000 nodes
+function createData() {
+ const genId = () => result.length
+ const result: { id: number, pid: number | null }[] = [];
+ for (let i = 0; i < 1000; i++) {
+ let id1 = genId()
+ result.push({ id: id1, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id1 })
+ }
+ let id2 = genId()
+ result.push({ id: id2, pid: null })
+ for (let j = 0; j < 4; j++) {
+ result.push({ id: genId(), pid: id2 })
+ }
+ }
+ return result;
+}
触摸 & 移动设备
其他
direction
: 从右往左显示.customDragImage
: 自定义 drag image.rootId
: 使用扁平数据时, 顶级节点的父 id.keepPlaceholder
: 拖拽到树外时, 是否要保留拖拽占位节点. 默认false
.scrollToNode
: 滚动到指定节点.