@aiao/rxdb-model 0.0.19 → 0.0.28

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.
Files changed (182) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +43 -6
  3. package/dist/cn.d.ts +19 -0
  4. package/dist/cn.d.ts.map +1 -0
  5. package/dist/entity-capabilities.d.ts +25 -0
  6. package/dist/entity-capabilities.d.ts.map +1 -0
  7. package/dist/entity-detail/detail-tabs.d.ts +7 -1
  8. package/dist/entity-detail/detail-tabs.d.ts.map +1 -1
  9. package/dist/entity-detail/index.d.ts +5 -0
  10. package/dist/entity-detail/index.d.ts.map +1 -1
  11. package/dist/entity-detail/interfaces.d.ts +25 -1
  12. package/dist/entity-detail/interfaces.d.ts.map +1 -1
  13. package/dist/entity-field.utils.d.ts +54 -2
  14. package/dist/entity-field.utils.d.ts.map +1 -1
  15. package/dist/entity-form/form-data.d.ts +21 -0
  16. package/dist/entity-form/form-data.d.ts.map +1 -1
  17. package/dist/entity-form/form-fields.d.ts +19 -1
  18. package/dist/entity-form/form-fields.d.ts.map +1 -1
  19. package/dist/entity-form/form-validation.d.ts +13 -0
  20. package/dist/entity-form/form-validation.d.ts.map +1 -1
  21. package/dist/entity-form/index.d.ts +5 -0
  22. package/dist/entity-form/index.d.ts.map +1 -1
  23. package/dist/entity-form/interfaces.d.ts +60 -0
  24. package/dist/entity-form/interfaces.d.ts.map +1 -1
  25. package/dist/entity-list/manual-order-list.d.ts +92 -0
  26. package/dist/entity-list/manual-order-list.d.ts.map +1 -0
  27. package/dist/entity-list/types.d.ts +25 -0
  28. package/dist/entity-list/types.d.ts.map +1 -0
  29. package/dist/entity-table/columns/build-editable-columns.d.ts +24 -2
  30. package/dist/entity-table/columns/build-editable-columns.d.ts.map +1 -1
  31. package/dist/entity-table/columns/column-utils.d.ts +75 -9
  32. package/dist/entity-table/columns/column-utils.d.ts.map +1 -1
  33. package/dist/entity-table/editors/color-editor.d.ts +23 -0
  34. package/dist/entity-table/editors/color-editor.d.ts.map +1 -0
  35. package/dist/entity-table/editors/date-editor.d.ts +5 -0
  36. package/dist/entity-table/editors/date-editor.d.ts.map +1 -1
  37. package/dist/entity-table/editors/enum-editor.d.ts +10 -0
  38. package/dist/entity-table/editors/enum-editor.d.ts.map +1 -1
  39. package/dist/entity-table/editors/global-overlay-editor.d.ts +26 -3
  40. package/dist/entity-table/editors/global-overlay-editor.d.ts.map +1 -1
  41. package/dist/entity-table/editors/icon-list-editor.d.ts +12 -0
  42. package/dist/entity-table/editors/icon-list-editor.d.ts.map +1 -1
  43. package/dist/entity-table/editors/json-editor.d.ts +5 -0
  44. package/dist/entity-table/editors/json-editor.d.ts.map +1 -1
  45. package/dist/entity-table/editors/key-value-editor.d.ts +11 -1
  46. package/dist/entity-table/editors/key-value-editor.d.ts.map +1 -1
  47. package/dist/entity-table/editors/lucide-svg.d.ts +3 -2
  48. package/dist/entity-table/editors/lucide-svg.d.ts.map +1 -1
  49. package/dist/entity-table/editors/multiselect-editor.d.ts +24 -6
  50. package/dist/entity-table/editors/multiselect-editor.d.ts.map +1 -1
  51. package/dist/entity-table/editors/number-editor.d.ts +32 -2
  52. package/dist/entity-table/editors/number-editor.d.ts.map +1 -1
  53. package/dist/entity-table/editors/relation-editor.d.ts +12 -0
  54. package/dist/entity-table/editors/relation-editor.d.ts.map +1 -1
  55. package/dist/entity-table/editors/safe-list-editor.d.ts +1 -0
  56. package/dist/entity-table/editors/safe-list-editor.d.ts.map +1 -1
  57. package/dist/entity-table/editors/tags-editor.d.ts +11 -0
  58. package/dist/entity-table/editors/tags-editor.d.ts.map +1 -1
  59. package/dist/entity-table/editors/text-format-editor.d.ts +32 -0
  60. package/dist/entity-table/editors/text-format-editor.d.ts.map +1 -0
  61. package/dist/entity-table/editors/uuid-editor.d.ts +13 -0
  62. package/dist/entity-table/editors/uuid-editor.d.ts.map +1 -1
  63. package/dist/entity-table/index.d.ts +13 -3
  64. package/dist/entity-table/index.d.ts.map +1 -1
  65. package/dist/entity-table/interfaces.d.ts +38 -0
  66. package/dist/entity-table/interfaces.d.ts.map +1 -1
  67. package/dist/entity-table/vtable/table-clipboard.d.ts +30 -1
  68. package/dist/entity-table/vtable/table-clipboard.d.ts.map +1 -1
  69. package/dist/entity-table/vtable/table-factory.d.ts +23 -3
  70. package/dist/entity-table/vtable/table-factory.d.ts.map +1 -1
  71. package/dist/entity-table/vtable/table-keyboard.d.ts +18 -1
  72. package/dist/entity-table/vtable/table-keyboard.d.ts.map +1 -1
  73. package/dist/entity-table/vtable/table-operations.d.ts +42 -1
  74. package/dist/entity-table/vtable/table-operations.d.ts.map +1 -1
  75. package/dist/entity-table/vtable/table-row-move.d.ts +88 -0
  76. package/dist/entity-table/vtable/table-row-move.d.ts.map +1 -0
  77. package/dist/entity-table/vtable/table-theme.d.ts +47 -2
  78. package/dist/entity-table/vtable/table-theme.d.ts.map +1 -1
  79. package/dist/entity-table/vtable/table-tooltip.d.ts +7 -1
  80. package/dist/entity-table/vtable/table-tooltip.d.ts.map +1 -1
  81. package/dist/entity-table/vtable/vtable-compat.d.ts +33 -5
  82. package/dist/entity-table/vtable/vtable-compat.d.ts.map +1 -1
  83. package/dist/entity-value.utils.d.ts +49 -1
  84. package/dist/entity-value.utils.d.ts.map +1 -1
  85. package/dist/index.d.ts +10 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +4394 -64471
  88. package/dist/query-builder/models/operator.interface.d.ts +5 -1
  89. package/dist/query-builder/models/operator.interface.d.ts.map +1 -1
  90. package/dist/query-builder/models/query-builder-state.d.ts +31 -2
  91. package/dist/query-builder/models/query-builder-state.d.ts.map +1 -1
  92. package/dist/query-builder/models/validation.interface.d.ts +1 -1
  93. package/dist/query-builder/services/operator-registry.service.d.ts +7 -1
  94. package/dist/query-builder/services/operator-registry.service.d.ts.map +1 -1
  95. package/dist/query-builder/services/query-builder.service.d.ts +10 -4
  96. package/dist/query-builder/services/query-builder.service.d.ts.map +1 -1
  97. package/dist/query-builder/services/query-converter.service.d.ts +5 -1
  98. package/dist/query-builder/services/query-converter.service.d.ts.map +1 -1
  99. package/dist/query-builder/services/schema-parser.service.d.ts +5 -1
  100. package/dist/query-builder/services/schema-parser.service.d.ts.map +1 -1
  101. package/dist/query-builder/services/validation.service.d.ts +29 -3
  102. package/dist/query-builder/services/validation.service.d.ts.map +1 -1
  103. package/dist/query-builder/utils/date.d.ts +12 -0
  104. package/dist/query-builder/utils/date.d.ts.map +1 -1
  105. package/dist/query-builder/utils/field-tree.d.ts.map +1 -1
  106. package/dist/query-builder/utils/id.d.ts +2 -0
  107. package/dist/query-builder/utils/id.d.ts.map +1 -1
  108. package/dist/query-builder/utils/metadata-field-extractor.d.ts +13 -2
  109. package/dist/query-builder/utils/metadata-field-extractor.d.ts.map +1 -1
  110. package/dist/query-builder/utils/popover-position.d.ts +11 -0
  111. package/dist/query-builder/utils/popover-position.d.ts.map +1 -1
  112. package/dist/query-builder/utils/schema-factory.d.ts +1 -1
  113. package/dist/query-builder/utils/structural-equal.d.ts +4 -0
  114. package/dist/query-builder/utils/structural-equal.d.ts.map +1 -1
  115. package/dist/query-builder/utils/value-input.d.ts +20 -0
  116. package/dist/query-builder/utils/value-input.d.ts.map +1 -1
  117. package/dist/structural-equal.d.ts +5 -1
  118. package/dist/structural-equal.d.ts.map +1 -1
  119. package/package.json +34 -10
  120. package/src/cn.ts +21 -0
  121. package/src/entity-capabilities.ts +37 -0
  122. package/src/entity-detail/detail-tabs.ts +48 -0
  123. package/src/entity-detail/index.ts +7 -0
  124. package/src/entity-detail/interfaces.ts +55 -0
  125. package/src/entity-field.utils.ts +205 -0
  126. package/src/entity-form/form-data.ts +83 -0
  127. package/src/entity-form/form-fields.ts +48 -0
  128. package/src/entity-form/form-validation.ts +34 -0
  129. package/src/entity-form/index.ts +9 -0
  130. package/src/entity-form/interfaces.ts +116 -0
  131. package/src/entity-list/manual-order-list.ts +159 -0
  132. package/src/entity-list/types.ts +22 -0
  133. package/src/entity-table/columns/build-editable-columns.ts +184 -0
  134. package/src/entity-table/columns/column-utils.ts +657 -0
  135. package/src/entity-table/editors/color-editor.ts +239 -0
  136. package/src/entity-table/editors/date-editor.ts +169 -0
  137. package/src/entity-table/editors/enum-editor.ts +167 -0
  138. package/src/entity-table/editors/global-overlay-editor.ts +277 -0
  139. package/src/entity-table/editors/icon-list-editor.ts +130 -0
  140. package/src/entity-table/editors/json-editor.ts +212 -0
  141. package/src/entity-table/editors/key-value-editor.ts +595 -0
  142. package/src/entity-table/editors/lucide-svg.ts +78 -0
  143. package/src/entity-table/editors/multiselect-editor.ts +171 -0
  144. package/src/entity-table/editors/number-editor.ts +322 -0
  145. package/src/entity-table/editors/relation-editor.ts +316 -0
  146. package/src/entity-table/editors/safe-list-editor.ts +17 -0
  147. package/src/entity-table/editors/tags-editor.ts +239 -0
  148. package/src/entity-table/editors/text-format-editor.ts +220 -0
  149. package/src/entity-table/editors/uuid-editor.ts +198 -0
  150. package/src/entity-table/index.ts +96 -0
  151. package/src/entity-table/interfaces.ts +100 -0
  152. package/src/entity-table/vtable/table-clipboard.ts +419 -0
  153. package/src/entity-table/vtable/table-factory.ts +117 -0
  154. package/src/entity-table/vtable/table-keyboard.ts +89 -0
  155. package/src/entity-table/vtable/table-operations.ts +123 -0
  156. package/src/entity-table/vtable/table-row-move.ts +122 -0
  157. package/src/entity-table/vtable/table-theme.ts +181 -0
  158. package/src/entity-table/vtable/table-tooltip.ts +70 -0
  159. package/src/entity-table/vtable/vtable-compat.ts +84 -0
  160. package/src/entity-value.utils.ts +456 -0
  161. package/src/index.ts +58 -0
  162. package/src/query-builder/models/config.interface.ts +31 -0
  163. package/src/query-builder/models/index.ts +8 -0
  164. package/src/query-builder/models/operator.interface.ts +191 -0
  165. package/src/query-builder/models/query-builder-state.ts +222 -0
  166. package/src/query-builder/models/validation.interface.ts +184 -0
  167. package/src/query-builder/services/index.ts +9 -0
  168. package/src/query-builder/services/operator-registry.service.ts +243 -0
  169. package/src/query-builder/services/query-builder.service.ts +568 -0
  170. package/src/query-builder/services/query-converter.service.ts +153 -0
  171. package/src/query-builder/services/schema-parser.service.ts +312 -0
  172. package/src/query-builder/services/validation.service.ts +390 -0
  173. package/src/query-builder/utils/date.ts +35 -0
  174. package/src/query-builder/utils/field-tree.ts +400 -0
  175. package/src/query-builder/utils/id.ts +15 -0
  176. package/src/query-builder/utils/index.ts +12 -0
  177. package/src/query-builder/utils/metadata-field-extractor.ts +460 -0
  178. package/src/query-builder/utils/popover-position.ts +63 -0
  179. package/src/query-builder/utils/schema-factory.ts +75 -0
  180. package/src/query-builder/utils/structural-equal.ts +5 -0
  181. package/src/query-builder/utils/value-input.ts +105 -0
  182. package/src/structural-equal.ts +49 -0
@@ -0,0 +1,316 @@
1
+ import type { EditContext, IEditor, RectProps } from '@visactor/vtable-editors';
2
+ import type { RelatedEntityItem } from '../../entity-form/interfaces.js';
3
+ import { DAISY_COLORS, positionOverlayPanel, refocusTable } from './global-overlay-editor.js';
4
+
5
+ /** 关联实体选项项 */
6
+ // Re-export for convenience
7
+ export { type RelatedEntityItem };
8
+
9
+ /**
10
+ * 关系字段搜索下拉编辑器(oneToOne / manyToOne)
11
+ *
12
+ * - 顶部搜索框:实时过滤可选实体列表
13
+ * - 键盘导航:↑ ↓ 移动焦点,Enter / 点击确认,Escape 取消
14
+ * - 空值支持:nullable=true 时列表首项为"(空)"
15
+ * - 存储外键 ID,显示关联实体的 displayName
16
+ */
17
+ export class RelationEditor implements IEditor<string> {
18
+ #currentValue = '';
19
+ #originalValue = '';
20
+ readonly #itemsSource: RelatedEntityItem[] | (() => RelatedEntityItem[]);
21
+ /** 每次 onStart 时从 source 解析出的本轮候选列表 */
22
+ #resolvedItems: RelatedEntityItem[] = [];
23
+ #filteredItems: RelatedEntityItem[] = [];
24
+ readonly #nullable: boolean;
25
+ #focusedIndex = -1;
26
+
27
+ #panel: HTMLDivElement | null = null;
28
+ #listContainer: HTMLDivElement | null = null;
29
+ #searchInput: HTMLInputElement | null = null;
30
+ #endEdit: (() => void) | null = null;
31
+ #container: HTMLElement | null = null;
32
+
33
+ readonly #handleOutsideMouseDown: (e: MouseEvent) => void;
34
+ readonly #handleKeydown: (e: KeyboardEvent) => void;
35
+ /** VTable 编辑器类型标识(注册到 VTable 的编辑器名) */
36
+ readonly editorType = 'relation-editor';
37
+
38
+ /**
39
+ * 创建关系下拉编辑器
40
+ *
41
+ * @param items - 候选实体列表,或每次打开时重新求值的工厂函数
42
+ * @param nullable - 是否允许空值,为 true 时列表首项为“(空)”
43
+ */
44
+ constructor(items: RelatedEntityItem[] | (() => RelatedEntityItem[]) = [], nullable = false) {
45
+ this.#itemsSource = items;
46
+ this.#nullable = nullable;
47
+
48
+ this.#handleOutsideMouseDown = (e: MouseEvent) => {
49
+ if (!this.#panel?.contains(e.target as HTMLElement)) {
50
+ this.#cancelEdit();
51
+ }
52
+ };
53
+
54
+ this.#handleKeydown = (e: KeyboardEvent) => {
55
+ const inSearch = document.activeElement === this.#searchInput;
56
+ switch (e.key) {
57
+ case 'Escape':
58
+ e.preventDefault();
59
+ e.stopPropagation();
60
+ this.#cancelEdit();
61
+ break;
62
+ case 'ArrowDown':
63
+ e.preventDefault();
64
+ e.stopPropagation();
65
+ this.#moveFocus(1);
66
+ break;
67
+ case 'ArrowUp':
68
+ e.preventDefault();
69
+ e.stopPropagation();
70
+ this.#moveFocus(-1);
71
+ break;
72
+ case 'Enter':
73
+ if (this.#focusedIndex >= 0) {
74
+ e.preventDefault();
75
+ e.stopPropagation();
76
+ this.#selectAt(this.#focusedIndex);
77
+ }
78
+ break;
79
+ case 'Delete':
80
+ case 'Backspace':
81
+ if (!inSearch && this.#nullable) {
82
+ e.preventDefault();
83
+ e.stopPropagation();
84
+ this.#currentValue = '';
85
+ this.#commitEdit();
86
+ }
87
+ break;
88
+ }
89
+ };
90
+ }
91
+
92
+ /** 弹出搜索浮层,解析候选列表并绑定键盘与点击事件 */
93
+ onStart(ctx: EditContext<string>): void {
94
+ const table = ctx.table as { getCellOriginValue(col: number, row: number): unknown } | null;
95
+ const origin = table?.getCellOriginValue(ctx.col, ctx.row) ?? ctx.value;
96
+ this.#currentValue = origin != null ? String(origin) : '';
97
+ this.#originalValue = this.#currentValue;
98
+ this.#container = ctx.container;
99
+ this.#endEdit = ctx.endEdit;
100
+
101
+ // 每次打开编辑器时重新解析候选列表(支持动态 provider)
102
+ this.#resolvedItems = typeof this.#itemsSource === 'function' ? this.#itemsSource() : this.#itemsSource;
103
+
104
+ this.#buildFilteredItems('');
105
+
106
+ const panel = document.createElement('div');
107
+ panel.style.cssText = [
108
+ 'position:fixed',
109
+ 'z-index:9999',
110
+ `background:${DAISY_COLORS.bg}`,
111
+ `border:1px solid ${DAISY_COLORS.border}`,
112
+ 'border-radius:6px',
113
+ 'padding:0',
114
+ 'box-shadow:0 4px 16px rgba(0,0,0,.15)',
115
+ 'min-width:200px',
116
+ 'max-width:340px',
117
+ 'display:flex',
118
+ 'flex-direction:column',
119
+ 'overflow:hidden'
120
+ ].join(';');
121
+
122
+ // 搜索框
123
+ const searchWrap = document.createElement('div');
124
+ searchWrap.style.cssText = `padding:6px 8px;border-bottom:1px solid ${DAISY_COLORS.border};flex-shrink:0`;
125
+
126
+ const input = document.createElement('input');
127
+ input.type = 'text';
128
+ input.placeholder = '搜索…';
129
+ input.style.cssText = [
130
+ 'width:100%',
131
+ 'box-sizing:border-box',
132
+ 'padding:4px 8px',
133
+ 'font-size:12px',
134
+ `background:${DAISY_COLORS.hover}`,
135
+ `color:${DAISY_COLORS.text}`,
136
+ `border:1px solid ${DAISY_COLORS.border}`,
137
+ 'border-radius:4px',
138
+ 'outline:none'
139
+ ].join(';');
140
+ input.addEventListener('input', () => {
141
+ this.#buildFilteredItems(input.value);
142
+ this.#renderItems();
143
+ });
144
+ searchWrap.appendChild(input);
145
+ panel.appendChild(searchWrap);
146
+
147
+ // 列表容器
148
+ const listContainer = document.createElement('div');
149
+ listContainer.style.cssText = 'max-height:200px;overflow-y:auto;overscroll-behavior:contain;padding:4px 0';
150
+ panel.appendChild(listContainer);
151
+
152
+ this.#panel = panel;
153
+ this.#listContainer = listContainer;
154
+ this.#searchInput = input;
155
+
156
+ this.#renderItems();
157
+ document.body.appendChild(panel);
158
+
159
+ positionOverlayPanel(panel, ctx.referencePosition.rect, ctx.container, 280, { minWidth: 200 });
160
+
161
+ document.addEventListener('mousedown', this.#handleOutsideMouseDown, { capture: true });
162
+ document.addEventListener('keydown', this.#handleKeydown, { capture: true });
163
+
164
+ setTimeout(() => {
165
+ input.focus();
166
+ this.#focusedIndex = this.#filteredItems.findIndex(i => i.id === this.#currentValue);
167
+ if (this.#focusedIndex >= 0) {
168
+ this.#listContainer?.querySelector<HTMLElement>(`[data-idx="${this.#focusedIndex}"]`)?.scrollIntoView({
169
+ block: 'nearest'
170
+ });
171
+ }
172
+ }, 10);
173
+ }
174
+
175
+ /** 移除浮层并解绑全局事件 */
176
+ onEnd(): void {
177
+ this.#panel?.remove();
178
+ this.#panel = null;
179
+ this.#listContainer = null;
180
+ this.#searchInput = null;
181
+ document.removeEventListener('mousedown', this.#handleOutsideMouseDown, { capture: true });
182
+ document.removeEventListener('keydown', this.#handleKeydown, { capture: true });
183
+ this.#endEdit = null;
184
+ this.#container = null;
185
+ }
186
+
187
+ /** 返回当前选中的关联实体 ID(未选中为空字符串) */
188
+ getValue(): string {
189
+ return this.#currentValue;
190
+ }
191
+
192
+ /** 判断目标元素是否位于本编辑器浮层内 */
193
+ isEditorElement(target: HTMLElement): boolean {
194
+ return this.#panel?.contains(target) ?? false;
195
+ }
196
+
197
+ /** 按新的单元格矩形重新定位浮层 */
198
+ adjustPosition(rect: RectProps): void {
199
+ if (this.#panel) positionOverlayPanel(this.#panel, rect, this.#container, 280, { minWidth: 200 });
200
+ }
201
+
202
+ #buildFilteredItems(query: string): void {
203
+ const base: RelatedEntityItem[] =
204
+ this.#nullable ? [{ id: '', displayName: '(空)' }, ...this.#resolvedItems] : [...this.#resolvedItems];
205
+ if (!query.trim()) {
206
+ this.#filteredItems = base;
207
+ return;
208
+ }
209
+ const q = query.toLowerCase();
210
+ this.#filteredItems = base.filter(i => i.displayName.toLowerCase().includes(q) || i.id.toLowerCase().includes(q));
211
+ this.#focusedIndex = -1;
212
+ }
213
+
214
+ #renderItems(): void {
215
+ const list = this.#listContainer;
216
+ if (!list) return;
217
+ list.textContent = '';
218
+
219
+ if (this.#filteredItems.length === 0) {
220
+ const empty = document.createElement('div');
221
+ empty.style.cssText = `padding:8px 12px;font-size:12px;opacity:0.45;color:${DAISY_COLORS.text}`;
222
+ empty.textContent = '无匹配结果';
223
+ list.appendChild(empty);
224
+ return;
225
+ }
226
+
227
+ this.#filteredItems.forEach((item, idx) => {
228
+ const isNullItem = item.id === '';
229
+ const isSelected = item.id === this.#currentValue;
230
+ const isFocused = idx === this.#focusedIndex;
231
+
232
+ const row = document.createElement('div');
233
+ row.dataset['idx'] = String(idx);
234
+ row.style.cssText = [
235
+ 'display:flex',
236
+ 'align-items:center',
237
+ 'padding:5px 12px',
238
+ 'cursor:pointer',
239
+ 'font-size:13px',
240
+ `color:${DAISY_COLORS.text}`,
241
+ `opacity:${isNullItem ? '0.55' : '1'}`,
242
+ `font-style:${isNullItem ? 'italic' : 'normal'}`,
243
+ `background:${
244
+ isFocused ? DAISY_COLORS.focusBg
245
+ : isSelected ? DAISY_COLORS.selectedBg
246
+ : 'transparent'
247
+ }`
248
+ ].join(';');
249
+ row.textContent = item.displayName;
250
+
251
+ row.addEventListener('mouseenter', () => {
252
+ const prev = this.#focusedIndex;
253
+ this.#focusedIndex = idx;
254
+ this.#updateRowStyle(prev);
255
+ this.#updateRowStyle(idx);
256
+ });
257
+ row.addEventListener('mousedown', e => {
258
+ e.preventDefault();
259
+ e.stopPropagation();
260
+ this.#selectAt(idx);
261
+ });
262
+
263
+ list.appendChild(row);
264
+ });
265
+ }
266
+
267
+ #updateRowStyle(idx: number): void {
268
+ if (idx < 0) return;
269
+ const row = this.#listContainer?.querySelector<HTMLElement>(`[data-idx="${idx}"]`);
270
+ if (!row) return;
271
+ const item = this.#filteredItems[idx];
272
+ const isNullItem = item?.id === '';
273
+ const isSelected = item?.id === this.#currentValue;
274
+ const isFocused = idx === this.#focusedIndex;
275
+ row.style.background =
276
+ isFocused ? DAISY_COLORS.focusBg
277
+ : isSelected ? DAISY_COLORS.selectedBg
278
+ : 'transparent';
279
+ row.style.opacity = isNullItem ? '0.55' : '1';
280
+ }
281
+
282
+ #moveFocus(delta: number): void {
283
+ const count = this.#filteredItems.length;
284
+ if (count === 0) return;
285
+ const prev = this.#focusedIndex;
286
+ this.#focusedIndex =
287
+ prev < 0 ?
288
+ delta > 0 ?
289
+ 0
290
+ : count - 1
291
+ : Math.max(0, Math.min(count - 1, prev + delta));
292
+ this.#updateRowStyle(prev);
293
+ this.#updateRowStyle(this.#focusedIndex);
294
+ this.#listContainer
295
+ ?.querySelector<HTMLElement>(`[data-idx="${this.#focusedIndex}"]`)
296
+ ?.scrollIntoView({ block: 'nearest' });
297
+ }
298
+
299
+ #selectAt(idx: number): void {
300
+ const item = this.#filteredItems[idx];
301
+ if (!item) return;
302
+ this.#currentValue = item.id;
303
+ this.#commitEdit();
304
+ }
305
+
306
+ #commitEdit(): void {
307
+ refocusTable(this.#container);
308
+ this.#endEdit?.();
309
+ }
310
+
311
+ #cancelEdit(): void {
312
+ this.#currentValue = this.#originalValue;
313
+ refocusTable(this.#container);
314
+ this.#endEdit?.();
315
+ }
316
+ }
@@ -0,0 +1,17 @@
1
+ import { ListEditor } from '@visactor/vtable-editors';
2
+
3
+ /**
4
+ * Workaround for VTable `ListEditor` bug:
5
+ * `onEnd` throws TypeError when `element.parentNode` is null
6
+ * during keyboard-triggered cell navigation.
7
+ */
8
+ export class SafeListEditor extends ListEditor {
9
+ /** 调用父类 onEnd,吞掉键盘导航场景下 parentNode 为 null 引发的 TypeError */
10
+ override onEnd(): void {
11
+ try {
12
+ super.onEnd();
13
+ } catch (e) {
14
+ if (!(e instanceof TypeError)) throw e;
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,239 @@
1
+ import type { EditContext, IEditor } from '@visactor/vtable-editors';
2
+ import { DAISY_COLORS, positionOverlayPanel, refocusTable, scheduleEditorSetup } from './global-overlay-editor.js';
3
+
4
+ type TableLike = { getCellOriginValue(col: number, row: number): unknown } | null;
5
+
6
+ /** 标签编辑器模式:'string' 字符串标签 / 'number' 数字标签 */
7
+ export type TagsMode = 'string' | 'number';
8
+
9
+ /**
10
+ * 标签(数组)输入编辑器
11
+ *
12
+ * 浮层显示标签芯片 + 输入框,适合编辑 stringArray / numberArray 类型。
13
+ * - Enter 或逗号:添加当前输入为新标签
14
+ * - Backspace(输入框为空时):移除最后一个标签
15
+ * - 点击 × :移除对应标签
16
+ * - Escape:取消(恢复原始标签)
17
+ * - 点击外部 / Tab:提交当前输入(若有内容)并关闭
18
+ *
19
+ * 数字模式下(TagsMode='number')仅接受有效数字。
20
+ */
21
+ export class TagsEditor implements IEditor<unknown> {
22
+ readonly #mode: TagsMode;
23
+ #tags: string[] = [];
24
+ #originalTags: string[] = [];
25
+ #panel: HTMLDivElement | null = null;
26
+ #chipsEl: HTMLDivElement | null = null;
27
+ #input: HTMLInputElement | null = null;
28
+ #endEdit: (() => void) | null = null;
29
+ #outsideHandler: ((e: MouseEvent) => void) | null = null;
30
+
31
+ /** VTable 编辑器类型标识(注册到 VTable 的编辑器名) */
32
+ readonly editorType: string;
33
+
34
+ /**
35
+ * 创建标签编辑器
36
+ *
37
+ * @param mode - 标签模式,'string'(默认)或 'number'
38
+ */
39
+ constructor(mode: TagsMode = 'string') {
40
+ this.#mode = mode;
41
+ this.editorType = mode === 'number' ? 'number-tags-editor' : 'tags-editor';
42
+ }
43
+
44
+ /** 弹出标签编辑浮层,从单元格值解析初始标签并绑定交互事件 */
45
+ onStart(ctx: EditContext<unknown>): void {
46
+ const container = ctx.container as HTMLElement;
47
+ this.#endEdit = () => {
48
+ refocusTable(container);
49
+ ctx.endEdit();
50
+ };
51
+ const origin = (ctx.table as TableLike)?.getCellOriginValue(ctx.col, ctx.row) ?? ctx.value;
52
+
53
+ if (Array.isArray(origin)) {
54
+ this.#tags = (origin as unknown[]).map(String).filter(Boolean);
55
+ } else if (origin != null && origin !== '') {
56
+ this.#tags = String(origin)
57
+ .split(',')
58
+ .map(x => x.trim())
59
+ .filter(Boolean);
60
+ } else {
61
+ this.#tags = [];
62
+ }
63
+ this.#originalTags = [...this.#tags];
64
+
65
+ const panel = document.createElement('div');
66
+ panel.style.cssText = [
67
+ 'position:fixed',
68
+ 'z-index:9999',
69
+ 'padding:8px',
70
+ 'min-width:220px',
71
+ 'max-width:360px',
72
+ `background:${DAISY_COLORS.bg}`,
73
+ `border:1px solid ${DAISY_COLORS.border}`,
74
+ 'border-radius:8px',
75
+ 'box-shadow:0 4px 16px rgba(0,0,0,.18)',
76
+ 'display:flex',
77
+ 'flex-direction:column',
78
+ 'gap:6px'
79
+ ].join(';');
80
+
81
+ const chipsEl = document.createElement('div');
82
+ chipsEl.style.cssText = ['display:flex', 'flex-wrap:wrap', 'gap:4px', 'min-height:20px'].join(';');
83
+ this.#chipsEl = chipsEl;
84
+
85
+ const input = document.createElement('input');
86
+ input.type = 'text';
87
+ if (this.#mode === 'number') input.inputMode = 'decimal';
88
+ input.placeholder = this.#mode === 'number' ? '输入数字 Enter 添加' : '输入后 Enter 添加';
89
+ input.style.cssText = [
90
+ 'width:100%',
91
+ `background:${DAISY_COLORS.bg}`,
92
+ `color:${DAISY_COLORS.text}`,
93
+ `border:1px solid ${DAISY_COLORS.border}`,
94
+ 'border-radius:4px',
95
+ 'padding:3px 6px',
96
+ 'font-size:12px',
97
+ 'outline:none',
98
+ `placeholder-color:${DAISY_COLORS.text}`
99
+ ].join(';');
100
+ this.#input = input;
101
+
102
+ const hint = document.createElement('div');
103
+ hint.style.cssText = `font-size:12px;opacity:0.4;color:${DAISY_COLORS.text}`;
104
+ hint.textContent = 'Enter /逗号 添加 · Backspace 删除 · Esc 取消';
105
+
106
+ input.addEventListener('keydown', e => {
107
+ if (e.key === 'Enter' || e.key === ',') {
108
+ e.preventDefault();
109
+ e.stopPropagation();
110
+ this.#addTag(input.value.trim());
111
+ input.value = '';
112
+ } else if (e.key === 'Backspace' && input.value === '' && this.#tags.length > 0) {
113
+ this.#tags.pop();
114
+ this.#renderChips();
115
+ } else if (e.key === 'Escape') {
116
+ e.preventDefault();
117
+ e.stopPropagation();
118
+ this.#tags = [...this.#originalTags]; // 恢复原始值
119
+ this.#endEdit?.();
120
+ } else if (e.key === 'Tab') {
121
+ const trimmed = input.value.trim();
122
+ if (trimmed) {
123
+ e.preventDefault();
124
+ this.#addTag(trimmed);
125
+ input.value = '';
126
+ }
127
+ }
128
+ });
129
+
130
+ panel.appendChild(chipsEl);
131
+ panel.appendChild(input);
132
+ panel.appendChild(hint);
133
+ document.body.appendChild(panel);
134
+ positionOverlayPanel(panel, ctx.referencePosition.rect, ctx.container as HTMLElement, 120, { maxRight: 380 });
135
+ this.#panel = panel;
136
+ this.#renderChips();
137
+
138
+ this.#outsideHandler = (e: MouseEvent) => {
139
+ if (!panel.contains(e.target as Node)) {
140
+ const trimmed = (this.#input?.value ?? '').trim();
141
+ if (trimmed) this.#addTag(trimmed);
142
+ this.#cleanup();
143
+ this.#endEdit?.();
144
+ }
145
+ };
146
+ scheduleEditorSetup(() => {
147
+ if (!this.#outsideHandler) return;
148
+ document.addEventListener('mousedown', this.#outsideHandler, { capture: true });
149
+ input.focus();
150
+ });
151
+ }
152
+
153
+ /** 返回标签数组;number 模式下返回数字数组 */
154
+ getValue(): string[] | number[] {
155
+ if (this.#mode === 'number') {
156
+ return this.#tags.map(Number).filter(n => !isNaN(n));
157
+ }
158
+ return [...this.#tags];
159
+ }
160
+
161
+ /** 移除浮层并解绑全局点击事件 */
162
+ onEnd(): void {
163
+ this.#cleanup();
164
+ }
165
+
166
+ /** 判断目标元素是否位于本编辑器浮层内 */
167
+ isEditorElement(el: HTMLElement | EventTarget): boolean {
168
+ return !!this.#panel?.contains(el as Node);
169
+ }
170
+
171
+ #addTag(value: string): void {
172
+ if (!value) return;
173
+ if (this.#mode === 'number') {
174
+ const n = Number(value);
175
+ if (isNaN(n)) return;
176
+ this.#tags.push(String(n)); // 允许重复
177
+ } else {
178
+ this.#tags.push(value); // 允许重复
179
+ }
180
+ this.#renderChips();
181
+ }
182
+
183
+ #renderChips(): void {
184
+ const el = this.#chipsEl;
185
+ if (!el) return;
186
+ el.textContent = '';
187
+ this.#tags.forEach((tag, idx) => {
188
+ const chip = document.createElement('span');
189
+ chip.style.cssText = [
190
+ 'display:inline-flex',
191
+ 'align-items:center',
192
+ 'gap:2px',
193
+ 'padding:1px 6px 1px 8px',
194
+ 'border-radius:9999px',
195
+ 'font-size:12px',
196
+ `background:${DAISY_COLORS.hover}`,
197
+ `color:${DAISY_COLORS.text}`,
198
+ `border:1px solid ${DAISY_COLORS.border}`,
199
+ 'user-select:none'
200
+ ].join(';');
201
+
202
+ const label = document.createTextNode(tag);
203
+ const removeBtn = document.createElement('button');
204
+ removeBtn.type = 'button';
205
+ removeBtn.textContent = '×';
206
+ removeBtn.style.cssText = [
207
+ 'background:none',
208
+ 'border:none',
209
+ 'cursor:pointer',
210
+ 'font-size:13px',
211
+ 'line-height:1',
212
+ 'padding:0 1px',
213
+ `color:${DAISY_COLORS.text}`,
214
+ 'opacity:0.5'
215
+ ].join(';');
216
+ removeBtn.addEventListener('mousedown', e => {
217
+ e.preventDefault();
218
+ e.stopPropagation();
219
+ this.#tags.splice(idx, 1);
220
+ this.#renderChips();
221
+ });
222
+
223
+ chip.appendChild(label);
224
+ chip.appendChild(removeBtn);
225
+ el.appendChild(chip);
226
+ });
227
+ }
228
+
229
+ #cleanup(): void {
230
+ if (this.#outsideHandler) {
231
+ document.removeEventListener('mousedown', this.#outsideHandler, { capture: true });
232
+ this.#outsideHandler = null;
233
+ }
234
+ this.#panel?.remove();
235
+ this.#panel = null;
236
+ this.#chipsEl = null;
237
+ this.#input = null;
238
+ }
239
+ }