@ticatec/uniface-element 0.3.15 → 0.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/TreeNode_README.md +577 -0
- package/dist/lib/TreeNode_README_CN.md +577 -0
- package/dist/lib/TreeNodes.d.ts +366 -45
- package/dist/lib/TreeNodes.js +394 -137
- package/dist/lib/index.d.ts +3 -4
- package/dist/lib/index.js +1 -2
- package/dist/list-box/index.d.ts +2 -2
- package/dist/list-box/types.d.ts +11 -1
- package/dist/nav-menu/Menus.d.ts +2 -2
- package/dist/nav-menu/Menus.js +6 -4
- package/dist/ticatec-uniface-web.css +29 -5
- package/dist/tree-view/README.md +143 -17
- package/dist/tree-view/README_CN.md +141 -1
- package/dist/tree-view/TreeNodeView.svelte +23 -9
- package/dist/tree-view/TreeNodeView.svelte.d.ts +5 -5
- package/dist/tree-view/TreeView.svelte +45 -5
- package/dist/tree-view/TreeView.svelte.d.ts +4 -2
- package/dist/tree-view/Types.d.ts +12 -8
- package/dist/tree-view/index.d.ts +3 -2
- package/package.json +1 -1
|
@@ -2054,17 +2054,41 @@
|
|
|
2054
2054
|
|
|
2055
2055
|
.uniface-tree-view {
|
|
2056
2056
|
position: relative;
|
|
2057
|
+
display: flex;
|
|
2058
|
+
flex-direction: column;
|
|
2057
2059
|
background-color: var(--uniface-default-page-content-bg, #ffffff);
|
|
2058
2060
|
color: var(--uniface-default-text-color, #374151);
|
|
2059
2061
|
box-sizing: border-box;
|
|
2060
2062
|
overflow: hidden;
|
|
2061
|
-
}
|
|
2062
|
-
.uniface-tree-view > div {
|
|
2063
|
-
width: 100%;
|
|
2064
2063
|
height: 100%;
|
|
2065
|
-
|
|
2066
|
-
|
|
2064
|
+
}
|
|
2065
|
+
.uniface-tree-view .tree-view-title {
|
|
2066
|
+
flex: 0 0 auto;
|
|
2067
|
+
padding: 12px 16px;
|
|
2068
|
+
background-color: var(--uniface-default-page-content-bg, #ffffff);
|
|
2069
|
+
border-bottom: 1px solid var(--uniface-default-border-color, #e5e7eb);
|
|
2070
|
+
cursor: pointer;
|
|
2071
|
+
user-select: none;
|
|
2072
|
+
z-index: 10;
|
|
2073
|
+
overflow: hidden;
|
|
2074
|
+
}
|
|
2075
|
+
.uniface-tree-view .tree-view-title .title-text {
|
|
2076
|
+
font-size: 16px;
|
|
2077
|
+
font-weight: 600;
|
|
2078
|
+
color: var(--uniface-default-text-color, #374151);
|
|
2079
|
+
white-space: nowrap;
|
|
2080
|
+
overflow: hidden;
|
|
2081
|
+
text-overflow: ellipsis;
|
|
2082
|
+
display: block;
|
|
2083
|
+
max-width: 100%;
|
|
2084
|
+
}
|
|
2085
|
+
.uniface-tree-view .tree-view-title:hover {
|
|
2086
|
+
background-color: var(--uniface-treeview-item-bg, #EEF4FF);
|
|
2087
|
+
}
|
|
2088
|
+
.uniface-tree-view .tree-view-content {
|
|
2089
|
+
flex: 1;
|
|
2067
2090
|
overflow: auto;
|
|
2091
|
+
padding-right: 8px;
|
|
2068
2092
|
user-select: none;
|
|
2069
2093
|
}
|
|
2070
2094
|
.uniface-tree-view .tree-node {
|
package/dist/tree-view/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npm install @ticatec/uniface-element
|
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
14
|
import TreeView, { type LazyLoader, type NodeVisibleFun, type OnNodeSelectionChange } from "@ticatec/uniface-element/TreeView";
|
|
15
|
-
import TreeNodes, {
|
|
15
|
+
import TreeNodes, { TreeNode, NodeViewOptions } from "@ticatec/uniface-element/TreeNodes";
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Basic Usage
|
|
@@ -68,6 +68,7 @@ import TreeNodes, { type TreeNode } from "@ticatec/uniface-element/TreeNodes";
|
|
|
68
68
|
| `textField` | `string \| GetText<any>` | Required | Field name or function to get node text |
|
|
69
69
|
| `activeNode` | `any` | `null` | Currently selected/active node |
|
|
70
70
|
| `style` | `string` | `""` | Additional CSS styles |
|
|
71
|
+
| `title` | `string` | `undefined` | Optional title displayed at the top of the tree (fixed position, doesn't scroll) |
|
|
71
72
|
| `lazyLoader` | `LazyLoader \| null` | `null` | Lazy loading configuration |
|
|
72
73
|
| `isVisible` | `NodeVisibleFun` | `null` | Function to determine node visibility |
|
|
73
74
|
| `onchange` | `OnNodeSelectionChange` | `null` | Callback when node selection changes |
|
|
@@ -75,17 +76,45 @@ import TreeNodes, { type TreeNode } from "@ticatec/uniface-element/TreeNodes";
|
|
|
75
76
|
| `onblur` | `((event: FocusEvent) => void) \| null` | `null` | Callback when tree loses focus |
|
|
76
77
|
| `onContextMenu` | `OnContextMenu` | `null` | Right-click context menu handler |
|
|
77
78
|
| `checkIsDirectory` | `CheckIsDirectory<any>` | Required | Function to check if node is a directory |
|
|
79
|
+
| `version` | `number \| undefined` | `undefined` | Version number to trigger reactive updates (from TreeNodes object) |
|
|
78
80
|
| `class` | `string` | `""` | CSS class name |
|
|
79
81
|
|
|
80
82
|
## Type Definitions
|
|
81
83
|
|
|
82
84
|
### TreeNode<T>
|
|
83
85
|
```typescript
|
|
84
|
-
|
|
85
|
-
item: T; // The actual data item
|
|
86
|
-
children
|
|
87
|
-
expand
|
|
88
|
-
|
|
86
|
+
class TreeNode<T> {
|
|
87
|
+
readonly item: T; // The actual data item
|
|
88
|
+
readonly children: ReadonlyArray<TreeNode<T>>; // Child nodes (read-only)
|
|
89
|
+
expand: boolean; // Whether node is expanded
|
|
90
|
+
loading: boolean; // Whether node is loading (for lazy loading)
|
|
91
|
+
parent: TreeNode<T> | null; // Parent node reference
|
|
92
|
+
|
|
93
|
+
// Methods
|
|
94
|
+
append(childItem: T): void; // Add a child node
|
|
95
|
+
detach(): void; // Remove this node from parent
|
|
96
|
+
moveTo(newParent: TreeNode<T>): void; // Move to different parent
|
|
97
|
+
replace(newItem: T): void; // Replace node data
|
|
98
|
+
removeChild(childId: any): void; // Remove specific child
|
|
99
|
+
removeChildren(): void; // Remove all children
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### NodeViewOptions<T>
|
|
104
|
+
```typescript
|
|
105
|
+
class NodeViewOptions<T> {
|
|
106
|
+
keyField: keyof T;
|
|
107
|
+
textField: keyof T | ((data: T) => string);
|
|
108
|
+
iconField?: keyof T | ((data: T) => string);
|
|
109
|
+
cssClassField?: keyof T | ((data: T) => string);
|
|
110
|
+
styleField?: keyof T | ((data: T) => Record<string, string>);
|
|
111
|
+
|
|
112
|
+
// Methods
|
|
113
|
+
getText(data: T): string;
|
|
114
|
+
getIcon(data: T): string | undefined;
|
|
115
|
+
getCssClass(data: T): string | undefined;
|
|
116
|
+
getStyle(data: T): Record<string, string> | undefined;
|
|
117
|
+
getKey(data: T): any;
|
|
89
118
|
}
|
|
90
119
|
```
|
|
91
120
|
|
|
@@ -119,14 +148,14 @@ type NodeVisibleFun = (node: TreeNode<any>) => boolean;
|
|
|
119
148
|
let activeNode = null;
|
|
120
149
|
|
|
121
150
|
const fileSystemData = [
|
|
122
|
-
{ id: 1, name: "=� Documents", type: "folder", parent: null },
|
|
123
|
-
{ id: 2, name: "=� Pictures", type: "folder", parent: null },
|
|
124
|
-
{ id: 3, name: "=� Work", type: "folder", parent: 1 },
|
|
125
|
-
{ id: 4, name: "=� Personal", type: "folder", parent: 1 },
|
|
126
|
-
{ id: 5, name: "=� resume.pdf", type: "file", parent: 3 },
|
|
127
|
-
{ id: 6, name: "=� cover-letter.doc", type: "file", parent: 3 },
|
|
128
|
-
{ id: 7, name: "=� vacation.jpg", type: "file", parent: 2 },
|
|
129
|
-
{ id: 8, name: "=� family.png", type: "file", parent: 2 }
|
|
151
|
+
{ id: 1, name: "=� Documents", type: "folder", parent: null },
|
|
152
|
+
{ id: 2, name: "=� Pictures", type: "folder", parent: null },
|
|
153
|
+
{ id: 3, name: "=� Work", type: "folder", parent: 1 },
|
|
154
|
+
{ id: 4, name: "=� Personal", type: "folder", parent: 1 },
|
|
155
|
+
{ id: 5, name: "=� resume.pdf", type: "file", parent: 3 },
|
|
156
|
+
{ id: 6, name: "=� cover-letter.doc", type: "file", parent: 3 },
|
|
157
|
+
{ id: 7, name: "=� vacation.jpg", type: "file", parent: 2 },
|
|
158
|
+
{ id: 8, name: "=� family.png", type: "file", parent: 2 }
|
|
130
159
|
];
|
|
131
160
|
|
|
132
161
|
const fileTree = new TreeNodes({
|
|
@@ -645,10 +674,10 @@ type NodeVisibleFun = (node: TreeNode<any>) => boolean;
|
|
|
645
674
|
switch (type) {
|
|
646
675
|
case 'country': return '<
|
|
647
676
|
case 'province':
|
|
648
|
-
case 'state': return '<�';
|
|
677
|
+
case 'state': return '<�';
|
|
649
678
|
case 'district':
|
|
650
|
-
case 'city': return '<�';
|
|
651
|
-
default: return '=�';
|
|
679
|
+
case 'city': return '<�';
|
|
680
|
+
default: return '=�';
|
|
652
681
|
}
|
|
653
682
|
}
|
|
654
683
|
|
|
@@ -729,6 +758,101 @@ const treeNodes = new TreeNodes({
|
|
|
729
758
|
treeNodes.setData(flatData); // Convert flat data to tree structure
|
|
730
759
|
```
|
|
731
760
|
|
|
761
|
+
### Using TreeNode Methods
|
|
762
|
+
|
|
763
|
+
TreeNode is now a class with built-in methods for data manipulation:
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
// Get a node and manipulate it directly
|
|
767
|
+
const node = treeNodes.nodes[0];
|
|
768
|
+
|
|
769
|
+
// Add a child node
|
|
770
|
+
node.append({
|
|
771
|
+
id: 'new-child',
|
|
772
|
+
name: 'New Child',
|
|
773
|
+
parent: node.item.id
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Remove the node from tree
|
|
777
|
+
node.detach();
|
|
778
|
+
|
|
779
|
+
// Move node to different parent
|
|
780
|
+
const newParent = treeNodes.nodes[1];
|
|
781
|
+
node.moveTo(newParent);
|
|
782
|
+
|
|
783
|
+
// Replace node data
|
|
784
|
+
node.replace({
|
|
785
|
+
id: node.item.id,
|
|
786
|
+
name: 'Updated Name',
|
|
787
|
+
parent: node.item.parent
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// Remove specific child
|
|
791
|
+
node.removeChild('child-id');
|
|
792
|
+
|
|
793
|
+
// Remove all children
|
|
794
|
+
node.removeChildren();
|
|
795
|
+
|
|
796
|
+
// Access properties (read-only)
|
|
797
|
+
console.log(node.item); // Node data
|
|
798
|
+
console.log(node.children); // Read-only child array
|
|
799
|
+
console.log(node.expand); // Expanded state
|
|
800
|
+
console.log(node.parent); // Parent node
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### Using NodeViewOptions
|
|
804
|
+
|
|
805
|
+
NodeViewOptions allows you to configure how nodes are displayed:
|
|
806
|
+
|
|
807
|
+
```typescript
|
|
808
|
+
const viewOptions = new NodeViewOptions({
|
|
809
|
+
keyField: 'code',
|
|
810
|
+
textField: 'name',
|
|
811
|
+
// Or use a function for dynamic text
|
|
812
|
+
textField: (data) => `${data.code} - ${data.name}`,
|
|
813
|
+
|
|
814
|
+
// Optional: icon field
|
|
815
|
+
iconField: 'icon',
|
|
816
|
+
// Or use a function
|
|
817
|
+
iconField: (data) => data.type === 'folder' ? 'folder-icon' : 'file-icon',
|
|
818
|
+
|
|
819
|
+
// Optional: CSS class
|
|
820
|
+
cssClassField: 'status',
|
|
821
|
+
// Or use a function
|
|
822
|
+
cssClassField: (data) => data.active ? 'active-node' : 'inactive-node',
|
|
823
|
+
|
|
824
|
+
// Optional: dynamic styles
|
|
825
|
+
styleField: (data) => ({
|
|
826
|
+
color: data.color,
|
|
827
|
+
fontWeight: data.important ? 'bold' : 'normal'
|
|
828
|
+
})
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// Use in TreeView
|
|
832
|
+
<TreeView
|
|
833
|
+
nodes={treeNodes.nodes}
|
|
834
|
+
nodeViewOptions={viewOptions}
|
|
835
|
+
/>
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### Working with Tree Title
|
|
839
|
+
|
|
840
|
+
The title prop adds a fixed header to the tree view:
|
|
841
|
+
|
|
842
|
+
```typescript
|
|
843
|
+
<TreeView
|
|
844
|
+
nodes={zones.nodes}
|
|
845
|
+
title="China" // Displays "中国" at the top, doesn't scroll
|
|
846
|
+
// Clicking the title clears the activeNode selection
|
|
847
|
+
/>
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
The title is useful for:
|
|
851
|
+
- Displaying the root entity name (e.g., "China" for provinces)
|
|
852
|
+
- Providing a way to clear selection by clicking
|
|
853
|
+
- Adding context to hierarchical data
|
|
854
|
+
- Organization charts showing company name
|
|
855
|
+
|
|
732
856
|
## Accessibility
|
|
733
857
|
|
|
734
858
|
- Keyboard navigation with arrow keys and Enter
|
|
@@ -757,5 +881,7 @@ treeNodes.setData(flatData); // Convert flat data to tree structure
|
|
|
757
881
|
## Related Components
|
|
758
882
|
|
|
759
883
|
- `TreeNodes` - Tree data structure utility
|
|
884
|
+
- `TreeNode` - Tree node class with data manipulation methods
|
|
885
|
+
- `NodeViewOptions` - View configuration for tree nodes
|
|
760
886
|
- `ContextMenu` - Right-click context menu component
|
|
761
887
|
- `LazyLoader` - Async data loading interface
|
|
@@ -51,8 +51,9 @@ import TreeNodes, { type TreeNode } from "@ticatec/uniface-element/TreeNodes";
|
|
|
51
51
|
}
|
|
52
52
|
</script>
|
|
53
53
|
|
|
54
|
-
<TreeView
|
|
54
|
+
<TreeView
|
|
55
55
|
nodes={treeNodes.nodes}
|
|
56
|
+
version={treeNodes.version}
|
|
56
57
|
textField="name"
|
|
57
58
|
bind:activeNode
|
|
58
59
|
onchange={handleSelectionChange}
|
|
@@ -75,6 +76,7 @@ import TreeNodes, { type TreeNode } from "@ticatec/uniface-element/TreeNodes";
|
|
|
75
76
|
| `onblur` | `((event: FocusEvent) => void) \| null` | `null` | 树失去焦点时的回调函数 |
|
|
76
77
|
| `onContextMenu` | `OnContextMenu` | `null` | 右键上下文菜单处理程序 |
|
|
77
78
|
| `checkIsDirectory` | `CheckIsDirectory<any>` | 必需 | 判断节点是否为目录的函数 |
|
|
79
|
+
| `version` | `number \| undefined` | `undefined` | 用于触发响应式更新的版本号(从 TreeNodes 对象传入) |
|
|
78
80
|
| `class` | `string` | `""` | CSS 类名 |
|
|
79
81
|
|
|
80
82
|
## 类型定义
|
|
@@ -727,8 +729,146 @@ const treeNodes = new TreeNodes({
|
|
|
727
729
|
});
|
|
728
730
|
|
|
729
731
|
treeNodes.setData(flatData); // 将扁平数据转换为树结构
|
|
732
|
+
|
|
733
|
+
// 动态添加节点
|
|
734
|
+
treeNodes.append(newItem);
|
|
735
|
+
|
|
736
|
+
// 移除节点
|
|
737
|
+
treeNodes.removeItem(oldItem);
|
|
738
|
+
|
|
739
|
+
// 移动节点
|
|
740
|
+
treeNodes.moveTo(itemToMove, newParentId);
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
## 动态更新数据
|
|
744
|
+
|
|
745
|
+
TreeView 的每个节点都提供了便捷的方法来操作树结构:
|
|
746
|
+
|
|
747
|
+
### 节点方法
|
|
748
|
+
|
|
749
|
+
所有从 TreeNodes 获取的节点都包含以下方法:
|
|
750
|
+
|
|
751
|
+
- `node.append(childItem)` - 添加子节点到当前节点
|
|
752
|
+
- `node.remove()` - 删除当前节点(从父节点中移除)
|
|
753
|
+
- `node.replace(newItem)` - 替换当前节点的数据
|
|
754
|
+
- `node.moveTo(newParentId)` - 将当前节点移动到另一个父节点下
|
|
755
|
+
|
|
756
|
+
### 使用示例
|
|
757
|
+
|
|
758
|
+
```svelte
|
|
759
|
+
<script>
|
|
760
|
+
import TreeView from "@ticatec/uniface-element/TreeView";
|
|
761
|
+
import TreeNodes from "@ticatec/uniface-element/TreeNodes";
|
|
762
|
+
|
|
763
|
+
let activeNode = null;
|
|
764
|
+
let zones;
|
|
765
|
+
|
|
766
|
+
const treeNodes = new TreeNodes({
|
|
767
|
+
keyField: 'id',
|
|
768
|
+
textField: 'name',
|
|
769
|
+
parentKeyField: 'parent',
|
|
770
|
+
checkIsRoot: (item) => item.parent === null,
|
|
771
|
+
checkIsDirectory: (node) => node.children != null
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
treeNodes.setData(initialData);
|
|
775
|
+
zones = treeNodes;
|
|
776
|
+
|
|
777
|
+
// 添加子节点到选中的节点
|
|
778
|
+
function addNode() {
|
|
779
|
+
if (!activeNode) return;
|
|
780
|
+
|
|
781
|
+
const newItem = {
|
|
782
|
+
id: Date.now(),
|
|
783
|
+
name: "新节点",
|
|
784
|
+
parent: activeNode.item.id
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
// 方式1:直接在节点上调用 append 方法
|
|
788
|
+
activeNode.append(newItem);
|
|
789
|
+
|
|
790
|
+
// 方式2:使用 TreeNodes 的 append 方法(会自动找到父节点)
|
|
791
|
+
// treeNodes.append(newItem);
|
|
792
|
+
|
|
793
|
+
// version 会自动更新,TreeView 会重新渲染
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// 删除选中的节点
|
|
797
|
+
function deleteNode() {
|
|
798
|
+
if (activeNode) {
|
|
799
|
+
// 直接在节点上调用 remove 方法
|
|
800
|
+
activeNode.remove();
|
|
801
|
+
activeNode = null;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// 替换节点的数据
|
|
806
|
+
function renameNode() {
|
|
807
|
+
if (activeNode) {
|
|
808
|
+
const newItem = {
|
|
809
|
+
...activeNode.item,
|
|
810
|
+
name: "重命名后的节点"
|
|
811
|
+
};
|
|
812
|
+
activeNode.replace(newItem);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// 移动节点到另一个父节点
|
|
817
|
+
function moveNode() {
|
|
818
|
+
if (activeNode) {
|
|
819
|
+
const newParentId = prompt("输入新的父节点 ID:");
|
|
820
|
+
if (newParentId) {
|
|
821
|
+
activeNode.moveTo(newParentId);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
</script>
|
|
826
|
+
|
|
827
|
+
<div>
|
|
828
|
+
<button on:click={addNode} disabled={!activeNode}>添加子节点</button>
|
|
829
|
+
<button on:click={deleteNode} disabled={!activeNode}>删除节点</button>
|
|
830
|
+
<button on:click={renameNode} disabled={!activeNode}>重命名</button>
|
|
831
|
+
<button on:click={moveNode} disabled={!activeNode}>移动节点</button>
|
|
832
|
+
|
|
833
|
+
<TreeView
|
|
834
|
+
nodes={treeNodes.nodes}
|
|
835
|
+
version={treeNodes.version}
|
|
836
|
+
textField="name"
|
|
837
|
+
bind:activeNode
|
|
838
|
+
checkIsDirectory={node => node.children != null}
|
|
839
|
+
/>
|
|
840
|
+
</div>
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### TreeNodes 类方法
|
|
844
|
+
|
|
845
|
+
如果你只有一个数据项,可以使用 `TreeNodes` 类的方法:
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
// 添加新节点(会自动根据 parentKeyField 找到父节点)
|
|
849
|
+
treeNodes.append(newItem);
|
|
850
|
+
|
|
851
|
+
// 注意:删除、替换、移动操作建议直接在节点上调用方法
|
|
852
|
+
// 因为这些操作通常针对特定的节点实例
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
### Lazy 节点注意事项
|
|
856
|
+
|
|
857
|
+
对于使用 lazyLoader 的节点(尚未展开加载的节点),直接添加子节点会给出警告:
|
|
858
|
+
|
|
859
|
+
```javascript
|
|
860
|
+
// 如果父节点是 lazy 节点且尚未加载
|
|
861
|
+
if (parentNode.children == null) {
|
|
862
|
+
console.warn('Cannot add child: parent is a lazy-loaded node');
|
|
863
|
+
// 节点会被添加到 nodeMap,但不会添加到 parent.children
|
|
864
|
+
// 这样不会干扰后续的 lazyLoader 行为
|
|
865
|
+
}
|
|
730
866
|
```
|
|
731
867
|
|
|
868
|
+
建议在使用前先展开节点,触发 lazyLoader 加载子节点。
|
|
869
|
+
|
|
870
|
+
**重要提示**:必须将 `treeNodes.version` 传递给 `TreeView` 的 `version` 属性,以确保在数据变化时组件能够正确重新渲染。
|
|
871
|
+
|
|
732
872
|
## 可访问性
|
|
733
873
|
|
|
734
874
|
- 支持使用箭头键和回车键进行键盘导航
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {TreeLazyLoader, NodeVisibleFun, OnNodeSelectionChange} from "./Types";
|
|
4
4
|
import iconLoading from "../assets/loading.gif"
|
|
5
5
|
import utils from "../common/utils";
|
|
6
6
|
import type {OnContextMenu} from "../context-menu/ContextMenuItem";
|
|
7
|
-
import type {CheckIsDirectory, GetText,
|
|
7
|
+
import type {CheckIsDirectory, GetText, ITreeNode} from "../lib/TreeNodes";
|
|
8
8
|
|
|
9
|
-
export let node:
|
|
9
|
+
export let node: ITreeNode<any>;
|
|
10
10
|
export let textField: string | GetText<any>;
|
|
11
|
-
export let lazyLoader:
|
|
11
|
+
export let lazyLoader: TreeLazyLoader | null;
|
|
12
12
|
export let activeNode: any;
|
|
13
13
|
export let isVisible: NodeVisibleFun;
|
|
14
14
|
export let onContextMenu: OnContextMenu;
|
|
15
15
|
export let onNodeSelectionChange: OnNodeSelectionChange;
|
|
16
16
|
|
|
17
|
-
export let checkIsDirectory: CheckIsDirectory<
|
|
17
|
+
export let checkIsDirectory: CheckIsDirectory<ITreeNode<any>>;
|
|
18
18
|
|
|
19
19
|
let loading: boolean = false;
|
|
20
20
|
|
|
@@ -26,7 +26,14 @@
|
|
|
26
26
|
if (node.children == null && lazyLoader != null) {
|
|
27
27
|
try {
|
|
28
28
|
loading = true;
|
|
29
|
-
|
|
29
|
+
const loadedChildren = await lazyLoader.load(node);
|
|
30
|
+
// 通过 append 方法添加子节点
|
|
31
|
+
for (const child of loadedChildren) {
|
|
32
|
+
node.append({
|
|
33
|
+
...child,
|
|
34
|
+
expand: child.expand ?? false
|
|
35
|
+
});
|
|
36
|
+
}
|
|
30
37
|
} finally {
|
|
31
38
|
loading = false;
|
|
32
39
|
}
|
|
@@ -39,11 +46,18 @@
|
|
|
39
46
|
if (respond) {
|
|
40
47
|
respond = false;
|
|
41
48
|
if (isDirectory()) {
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
// 如果是懒加载模式且没有子节点,先加载子节点
|
|
50
|
+
if (lazyLoader != null && !node.children) {
|
|
44
51
|
await loadChildren();
|
|
52
|
+
// 加载完成后,如果有子节点则展开
|
|
53
|
+
const children = node.children;
|
|
54
|
+
if (children && children.length > 0) {
|
|
55
|
+
node.expand = true;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// 非懒加载模式或已有子节点,直接切换展开状态
|
|
59
|
+
node.expand = !node.expand;
|
|
45
60
|
}
|
|
46
|
-
node.expand = !node.expand;
|
|
47
61
|
}
|
|
48
62
|
await utils.sleep(0.2);
|
|
49
63
|
respond = true;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TreeLazyLoader, NodeVisibleFun, OnNodeSelectionChange } from "./Types";
|
|
2
2
|
import type { OnContextMenu } from "../context-menu/ContextMenuItem";
|
|
3
|
-
import type { CheckIsDirectory, GetText,
|
|
3
|
+
import type { CheckIsDirectory, GetText, ITreeNode } from "../lib/TreeNodes";
|
|
4
4
|
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
5
5
|
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
6
6
|
$$bindings?: Bindings;
|
|
@@ -15,14 +15,14 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
|
|
|
15
15
|
z_$$bindings?: Bindings;
|
|
16
16
|
}
|
|
17
17
|
declare const TreeNodeView: $$__sveltets_2_IsomorphicComponent<{
|
|
18
|
-
node:
|
|
18
|
+
node: ITreeNode<any>;
|
|
19
19
|
textField: string | GetText<any>;
|
|
20
|
-
lazyLoader:
|
|
20
|
+
lazyLoader: TreeLazyLoader | null;
|
|
21
21
|
activeNode: any;
|
|
22
22
|
isVisible: NodeVisibleFun;
|
|
23
23
|
onContextMenu: OnContextMenu;
|
|
24
24
|
onNodeSelectionChange: OnNodeSelectionChange;
|
|
25
|
-
checkIsDirectory: CheckIsDirectory<
|
|
25
|
+
checkIsDirectory: CheckIsDirectory<ITreeNode<any>>;
|
|
26
26
|
}, {
|
|
27
27
|
[evt: string]: CustomEvent<any>;
|
|
28
28
|
}, {}, {}, string>;
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import type {LazyLoader, NodeVisibleFun, OnNodeSelectionChange} from "./Types";
|
|
5
5
|
import TreeNodeView from "./TreeNodeView.svelte";
|
|
6
6
|
import type {OnContextMenu} from "../context-menu/ContextMenuItem";
|
|
7
|
-
import type {CheckIsDirectory, GetText,
|
|
7
|
+
import type {CheckIsDirectory, GetText, ITreeNode} from "../lib/TreeNodes";
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
export let nodes: Array<
|
|
10
|
+
export let nodes: Array<ITreeNode<any>>;
|
|
11
11
|
export let textField: string | GetText<any>;
|
|
12
12
|
export let style: string = "";
|
|
13
13
|
export let lazyLoader: LazyLoader | null = null;
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
export let onblur: ((event: FocusEvent) => void) | null = null;
|
|
20
20
|
export let onContextMenu: OnContextMenu = null as unknown as OnContextMenu;
|
|
21
21
|
export let checkIsDirectory: CheckIsDirectory<any>;
|
|
22
|
+
export let version: number | undefined = undefined;
|
|
23
|
+
export let title: string | undefined = undefined;
|
|
22
24
|
let className: string = "";
|
|
23
25
|
|
|
24
|
-
const onNodeSelectionChange = async (node:
|
|
26
|
+
const onNodeSelectionChange = async (node: ITreeNode<any>): Promise<boolean> => {
|
|
25
27
|
let accept: boolean = (await onchange?.(node));
|
|
26
28
|
accept = accept == null ? true : accept;
|
|
27
29
|
if (accept == null || accept == true) {
|
|
@@ -30,10 +32,48 @@
|
|
|
30
32
|
return accept;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
const handleTitleClick = async () => {
|
|
36
|
+
let accept: boolean = (await onchange?.(null));
|
|
37
|
+
accept = accept == null ? true : accept;
|
|
38
|
+
if (accept == null || accept == true) {
|
|
39
|
+
activeNode = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 通过 version 触发响应式更新:当 version 变化时,重新读取 nodes
|
|
44
|
+
// 这样可以确保在节点数据变化时,TreeView 能够正确重新渲染
|
|
45
|
+
$: reactiveNodes = version !== undefined ? nodes : nodes;
|
|
46
|
+
|
|
47
|
+
// 当 activeNode 变化时,如果是 lazy 节点且未加载,自动触发加载
|
|
48
|
+
$: if (activeNode && lazyLoader && activeNode.children == null) {
|
|
49
|
+
const loadLazyNode = async () => {
|
|
50
|
+
if (activeNode.children == null && lazyLoader != null) {
|
|
51
|
+
try {
|
|
52
|
+
const loadedChildren = await lazyLoader.load(activeNode);
|
|
53
|
+
// 通过 append 方法添加子节点
|
|
54
|
+
for (const child of loadedChildren) {
|
|
55
|
+
activeNode.append({
|
|
56
|
+
...child,
|
|
57
|
+
expand: child.expand ?? false
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Failed to load lazy node:', error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
loadLazyNode();
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
</script>
|
|
34
69
|
<div class="uniface-tree-view {className}" {style} on:focus={onfocus} on:blur={onblur} tabindex="0">
|
|
35
|
-
|
|
36
|
-
|
|
70
|
+
{#if title}
|
|
71
|
+
<div class="tree-view-title" on:click={handleTitleClick}>
|
|
72
|
+
<span class="title-text">{title}</span>
|
|
73
|
+
</div>
|
|
74
|
+
{/if}
|
|
75
|
+
<div class="tree-view-content">
|
|
76
|
+
{#each reactiveNodes as node}
|
|
37
77
|
<TreeNodeView {activeNode} {node} {textField} {lazyLoader} {onNodeSelectionChange} {isVisible} {checkIsDirectory} {onContextMenu}/>
|
|
38
78
|
{/each}
|
|
39
79
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LazyLoader, NodeVisibleFun, OnNodeSelectionChange } from "./Types";
|
|
2
2
|
import type { OnContextMenu } from "../context-menu/ContextMenuItem";
|
|
3
|
-
import type { CheckIsDirectory, GetText,
|
|
3
|
+
import type { CheckIsDirectory, GetText, ITreeNode } from "../lib/TreeNodes";
|
|
4
4
|
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
5
5
|
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
6
6
|
$$bindings?: Bindings;
|
|
@@ -15,7 +15,7 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
|
|
|
15
15
|
z_$$bindings?: Bindings;
|
|
16
16
|
}
|
|
17
17
|
declare const TreeView: $$__sveltets_2_IsomorphicComponent<{
|
|
18
|
-
nodes: Array<
|
|
18
|
+
nodes: Array<ITreeNode<any>>;
|
|
19
19
|
textField: string | GetText<any>;
|
|
20
20
|
style?: string;
|
|
21
21
|
lazyLoader?: LazyLoader | null;
|
|
@@ -27,6 +27,8 @@ declare const TreeView: $$__sveltets_2_IsomorphicComponent<{
|
|
|
27
27
|
onblur?: ((event: FocusEvent) => void) | null;
|
|
28
28
|
onContextMenu?: OnContextMenu;
|
|
29
29
|
checkIsDirectory: CheckIsDirectory<any>;
|
|
30
|
+
version?: number | undefined;
|
|
31
|
+
title?: string | undefined;
|
|
30
32
|
}, {
|
|
31
33
|
[evt: string]: CustomEvent<any>;
|
|
32
34
|
}, {}, {
|
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
import type { CheckIsDirectory,
|
|
2
|
-
export type OnNodeSelectionChange = (node:
|
|
1
|
+
import type { CheckIsDirectory, ITreeNode } from "../lib/TreeNodes";
|
|
2
|
+
export type OnNodeSelectionChange = (node: ITreeNode<any>) => Promise<boolean>;
|
|
3
3
|
/**
|
|
4
4
|
* 节点是否显示
|
|
5
5
|
*/
|
|
6
|
-
export type NodeVisibleFun = (node:
|
|
6
|
+
export type NodeVisibleFun = (node: ITreeNode<any>) => boolean;
|
|
7
7
|
/**
|
|
8
8
|
* 加载子节点
|
|
9
9
|
*/
|
|
10
|
-
export type LoadChildrenFun = (item:
|
|
10
|
+
export type LoadChildrenFun = (item: ITreeNode<any>) => Promise<Array<ITreeNode<any>>>;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* 按需加载树节点的子节点
|
|
13
13
|
*/
|
|
14
|
-
export interface
|
|
14
|
+
export interface TreeLazyLoader {
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* 检查是否是枝节点(可能包含子节点)
|
|
17
17
|
*/
|
|
18
18
|
isBranch: CheckIsDirectory<any>;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* 加载子节点数据
|
|
21
21
|
*/
|
|
22
22
|
load: LoadChildrenFun;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* @deprecated 使用 TreeLazyLoader 代替。LazyLoader 已弃用,将在未来版本中移除。
|
|
26
|
+
*/
|
|
27
|
+
export type LazyLoader = TreeLazyLoader;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import TreeView from "./TreeView.svelte";
|
|
2
|
-
import type { LazyLoader, NodeVisibleFun, OnNodeSelectionChange, LoadChildrenFun } from "./Types";
|
|
2
|
+
import type { TreeLazyLoader, LazyLoader, NodeVisibleFun, OnNodeSelectionChange, LoadChildrenFun } from "./Types";
|
|
3
|
+
export type { ITreeNode, NodeViewOptions } from "../lib/TreeNodes";
|
|
3
4
|
export default TreeView;
|
|
4
|
-
export type { LazyLoader, NodeVisibleFun, OnNodeSelectionChange, LoadChildrenFun };
|
|
5
|
+
export type { TreeLazyLoader, LazyLoader, NodeVisibleFun, OnNodeSelectionChange, LoadChildrenFun };
|
package/package.json
CHANGED