@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,277 @@
1
+ import type { EditContext, IEditor, RectProps } from '@visactor/vtable-editors';
2
+
3
+ /** daisyUI v5 主题色引用(CSS 变量,主题自适应) */
4
+ export const DAISY_COLORS = {
5
+ bg: 'var(--color-base-100)',
6
+ hover: 'var(--color-base-200)',
7
+ border: 'var(--color-base-300)',
8
+ text: 'var(--color-base-content)',
9
+ primary: 'var(--color-primary)',
10
+ primaryContent: 'var(--color-primary-content)',
11
+ error: 'var(--color-error)',
12
+ errorContent: 'var(--color-error-content)',
13
+ selectedBg: 'color-mix(in srgb, var(--color-primary) 15%, transparent)',
14
+ focusBg: 'color-mix(in srgb, var(--color-primary) 30%, var(--color-base-100))'
15
+ } as const;
16
+
17
+ /** 编辑器初始化延迟,避免 VTable 点击事件立即关闭编辑器 */
18
+ const EDITOR_SETUP_DELAY = 10;
19
+
20
+ /**
21
+ * 延迟执行编辑器初始化(注册外部点击监听 + 聚焦),避免 VTable 事件干扰
22
+ *
23
+ * @param fn 延迟执行的初始化回调
24
+ */
25
+ export function scheduleEditorSetup(fn: () => void): void {
26
+ setTimeout(fn, EDITOR_SETUP_DELAY);
27
+ }
28
+
29
+ /**
30
+ * 编辑结束后将焦点还给 VTable root element(tabindex=0 的 .vtable div = ctx.container = table.getElement()),使方向键导航继续工作
31
+ *
32
+ * @param container VTable 容器元素
33
+ */
34
+ export function refocusTable(container: HTMLElement | null | undefined): void {
35
+ container?.focus();
36
+ }
37
+
38
+ /**
39
+ * 将浮层面板定位到单元格下方(空间不足时翻转到上方)
40
+ *
41
+ * @param panel 浮层面板元素
42
+ * @param rect 单元格相对 VTable 容器的位置矩形
43
+ * @param container VTable 容器(用于换算页面绝对坐标)
44
+ * @param estimatedHeight 面板估算高度(用于判断是否翻转到上方)
45
+ * @param options 可选布局选项(maxRight 面板最大右边界、minWidth 面板最小宽度)
46
+ */
47
+ export function positionOverlayPanel(
48
+ panel: HTMLElement,
49
+ rect: { left: number; top: number; width?: number; height?: number },
50
+ container: HTMLElement | null | undefined,
51
+ estimatedHeight: number,
52
+ options?: { maxRight?: number; minWidth?: number }
53
+ ): void {
54
+ const cRect = container?.getBoundingClientRect();
55
+ const absLeft = (cRect?.left ?? 0) + rect.left;
56
+ const absTop = (cRect?.top ?? 0) + rect.top;
57
+ const absBottom = absTop + (rect.height ?? 0);
58
+ const showAbove = window.innerHeight - absBottom < estimatedHeight && absTop > estimatedHeight;
59
+
60
+ const maxRight = options?.maxRight;
61
+ panel.style.left =
62
+ maxRight != null ?
63
+ `${Math.max(8, Math.min(absLeft, window.innerWidth - maxRight))}px`
64
+ : `${Math.max(8, absLeft)}px`;
65
+ panel.style.top = showAbove ? `${absTop - estimatedHeight - 4}px` : `${absBottom + 2}px`;
66
+
67
+ const minWidth = options?.minWidth;
68
+ if (minWidth != null) {
69
+ panel.style.minWidth = `${Math.max(minWidth, rect.width ?? 0)}px`;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 全局浮层列表编辑器抽象基类
75
+ *
76
+ * 提供通用基础设施,子类只需关注内容渲染与选择逻辑:
77
+ * - `document.body` 固定浮层 + 智能上下翻转定位
78
+ * - 点击外部关闭、Escape 关闭
79
+ * - 键盘导航(↑ ↓ Enter)
80
+ * - 鼠标悬停高亮(通过 `data-idx` 自动委托)
81
+ * - `refocusTable` 焦点还原
82
+ *
83
+ * ### 子类实现要求
84
+ * 1. 声明 `readonly editorType: string`
85
+ * 2. 实现 `buildRows(panel)` — 将行渲染到面板,行必须设置 `data-idx`
86
+ * 3. 实现 `get itemCount()` — 总行数
87
+ * 4. 实现 `onRowClick(idx)` — 点击/Enter 触发
88
+ * 5. 实现 `onFocusChange(idx, prev)` — 更新行视觉高亮
89
+ * 6. 实现 `getValue()` — 返回最终值
90
+ */
91
+ export abstract class GlobalOverlayEditor implements IEditor<string> {
92
+ #panel: HTMLDivElement | null = null;
93
+ #endEdit: (() => void) | null = null;
94
+ #focusedIndex = -1;
95
+
96
+ readonly #handleOutsideMouseDown: (e: MouseEvent) => void;
97
+ readonly #handleKeydown: (e: KeyboardEvent) => void;
98
+
99
+ protected container: HTMLElement | null = null;
100
+ protected table: { getCellOriginValue(col: number, row: number): unknown } | null = null;
101
+ protected editCol = 0;
102
+ protected editRow = 0;
103
+
104
+ protected abstract get initialFocusIndex(): number;
105
+ protected abstract get itemCount(): number;
106
+
107
+ protected get focusedIndex(): number {
108
+ return this.#focusedIndex;
109
+ }
110
+
111
+ /** VTable 编辑器类型标识(注册到 VTable 的编辑器名,子类赋值) */
112
+ abstract readonly editorType: string;
113
+
114
+ /** 构造浮层编辑器基类(子类在 onStart 中构建面板) */
115
+ constructor() {
116
+ this.#handleOutsideMouseDown = (e: MouseEvent) => {
117
+ if (!this.isEditorElement(e.target as HTMLElement)) this.close();
118
+ };
119
+ this.#handleKeydown = (e: KeyboardEvent) => {
120
+ switch (e.key) {
121
+ case 'Escape':
122
+ e.preventDefault();
123
+ e.stopPropagation();
124
+ this.close();
125
+ break;
126
+ case 'ArrowDown':
127
+ e.preventDefault();
128
+ e.stopPropagation();
129
+ this.#moveFocus(1);
130
+ break;
131
+ case 'ArrowUp':
132
+ e.preventDefault();
133
+ e.stopPropagation();
134
+ this.#moveFocus(-1);
135
+ break;
136
+ case 'Enter':
137
+ case ' ':
138
+ e.preventDefault();
139
+ e.stopPropagation();
140
+ if (this.#focusedIndex >= 0) this.onRowClick(this.#focusedIndex);
141
+ break;
142
+ case 'Delete':
143
+ case 'Backspace':
144
+ e.preventDefault();
145
+ e.stopPropagation();
146
+ this.onDeleteKey();
147
+ break;
148
+ }
149
+ };
150
+ }
151
+
152
+ /** 打开浮层:渲染行、定位面板并注册全局鼠标/键盘监听 */
153
+ onStart(ctx: EditContext<string>): void {
154
+ this.#endEdit = ctx.endEdit;
155
+ this.container = ctx.container;
156
+ this.table = ctx.table as { getCellOriginValue(col: number, row: number): unknown } | null;
157
+ this.editCol = ctx.col;
158
+ this.editRow = ctx.row;
159
+ this.#focusedIndex = this.initialFocusIndex;
160
+
161
+ const panel = document.createElement('div');
162
+ panel.style.cssText = [
163
+ 'position:fixed',
164
+ 'z-index:9999',
165
+ `background:${DAISY_COLORS.bg}`,
166
+ `border:1px solid ${DAISY_COLORS.border}`,
167
+ 'border-radius:6px',
168
+ 'padding:4px 0',
169
+ 'box-shadow:0 4px 16px rgba(0,0,0,.15)',
170
+ 'min-width:140px',
171
+ 'max-height:220px',
172
+ 'overflow-y:auto',
173
+ 'overscroll-behavior:contain'
174
+ ].join(';');
175
+
176
+ this.buildRows(panel);
177
+ document.body.appendChild(panel);
178
+ this.#panel = panel;
179
+ this.#positionPanel(ctx.referencePosition.rect);
180
+
181
+ document.addEventListener('mousedown', this.#handleOutsideMouseDown, { capture: true });
182
+ document.addEventListener('keydown', this.#handleKeydown, { capture: true });
183
+
184
+ requestAnimationFrame(() => this.#scrollToFocused());
185
+ }
186
+
187
+ /** 关闭浮层:移除面板并注销全局鼠标/键盘监听 */
188
+ onEnd(): void {
189
+ this.#panel?.remove();
190
+ this.#panel = null;
191
+ document.removeEventListener('mousedown', this.#handleOutsideMouseDown, { capture: true });
192
+ document.removeEventListener('keydown', this.#handleKeydown, { capture: true });
193
+ this.#endEdit = null;
194
+ this.container = null;
195
+ this.table = null;
196
+ }
197
+
198
+ /** 返回编辑结果值 */
199
+ abstract getValue(): string;
200
+
201
+ /** 判断目标元素是否属于浮层面板 */
202
+ isEditorElement(target: HTMLElement): boolean {
203
+ return this.#panel?.contains(target) ?? false;
204
+ }
205
+
206
+ /** 根据新的单元格矩形重新定位浮层面板 */
207
+ adjustPosition(rect: RectProps): void {
208
+ this.#positionPanel(rect);
209
+ }
210
+
211
+ protected abstract buildRows(panel: HTMLDivElement): void;
212
+ protected abstract onRowClick(idx: number): void;
213
+ protected abstract onFocusChange(idx: number, prev: number): void;
214
+
215
+ protected rowAt(idx: number): HTMLElement | null {
216
+ return this.#panel?.querySelector<HTMLElement>(`[data-idx="${idx}"]`) ?? null;
217
+ }
218
+
219
+ protected setFocused(idx: number): void {
220
+ const prev = this.#focusedIndex;
221
+ this.#focusedIndex = idx;
222
+ if (prev !== idx) this.onFocusChange(idx, prev);
223
+ this.#scrollToFocused();
224
+ }
225
+
226
+ protected onDeleteKey(): void {
227
+ this.close();
228
+ }
229
+
230
+ protected close(): void {
231
+ this.refocusTable();
232
+ this.#endEdit?.();
233
+ }
234
+
235
+ protected refocusTable(): void {
236
+ refocusTable(this.container);
237
+ }
238
+
239
+ #moveFocus(delta: 1 | -1): void {
240
+ const len = this.itemCount;
241
+ if (len === 0) return;
242
+ const next =
243
+ this.#focusedIndex < 0 ?
244
+ delta === 1 ?
245
+ 0
246
+ : len - 1
247
+ : (this.#focusedIndex + delta + len) % len;
248
+ this.setFocused(next);
249
+ }
250
+
251
+ #positionPanel(rect: RectProps): void {
252
+ const panel = this.#panel;
253
+ if (!panel || !this.container) return;
254
+
255
+ const { left, top, width, height } = rect;
256
+ const cRect = this.container.getBoundingClientRect();
257
+ const vLeft = cRect.left + left;
258
+ const vTop = cRect.top + top;
259
+
260
+ panel.style.left = `${vLeft}px`;
261
+ panel.style.top = `${vTop + height}px`;
262
+ panel.style.minWidth = `${width}px`;
263
+
264
+ requestAnimationFrame(() => {
265
+ if (!panel.isConnected) return;
266
+ const panelH = panel.offsetHeight;
267
+ if (vTop + height + panelH > window.innerHeight - 8) {
268
+ panel.style.top = `${vTop - panelH}px`;
269
+ }
270
+ });
271
+ }
272
+
273
+ #scrollToFocused(): void {
274
+ if (this.#focusedIndex < 0) return;
275
+ this.rowAt(this.#focusedIndex)?.scrollIntoView({ block: 'nearest' });
276
+ }
277
+ }
@@ -0,0 +1,130 @@
1
+ import { DAISY_COLORS, GlobalOverlayEditor } from './global-overlay-editor.js';
2
+ import { lucideToSvgElement, type IconData } from './lucide-svg.js';
3
+
4
+ /**
5
+ * 带图标的列表条目
6
+ * @deprecated 请改用 `EnumItem`,`EnumEditor` 已原生支持 icon 和 color 字段。
7
+ */
8
+ export interface IconLabeledItem {
9
+ /** 显示文本 */
10
+ text: string;
11
+ /** 存储值 */
12
+ value: string;
13
+ /** Lucide 图标数据(可选,配合 color 使用) */
14
+ icon?: IconData;
15
+ /** 图标描边颜色(可选,配合 icon 使用) */
16
+ color?: string;
17
+ }
18
+
19
+ /**
20
+ * 支持图标 + 文字的单选列表编辑器
21
+ *
22
+ * @deprecated 请改用 `EnumEditor`,传入包含 `icon` / `color` 字段的 `EnumItem` 数组。
23
+ * `IconListEditor` 将在未来版本移除。
24
+ *
25
+ * 每项包含 lucide SVG 图标和本地化标签。
26
+ * ↑ ↓ 移动焦点,Enter / 点击选中并关闭,Esc 取消。
27
+ */
28
+ export class IconListEditor extends GlobalOverlayEditor {
29
+ readonly #items: IconLabeledItem[];
30
+ #currentValue = '';
31
+
32
+ protected get initialFocusIndex(): number {
33
+ return this.#items.findIndex(i => i.value === this.#currentValue);
34
+ }
35
+
36
+ protected get itemCount(): number {
37
+ return this.#items.length;
38
+ }
39
+
40
+ /** VTable 编辑器类型标识(注册到 VTable 的编辑器名) */
41
+ readonly editorType = 'icon-list';
42
+
43
+ /**
44
+ * 创建图标列表编辑器
45
+ *
46
+ * @param config 编辑器配置(values 为条目列表)
47
+ */
48
+ constructor(config: { values: IconLabeledItem[] }) {
49
+ super();
50
+ this.#items = config.values;
51
+ }
52
+
53
+ /** 初始化编辑:解析当前值;不在候选列表中时回退到单元格原始值 */
54
+ override onStart(ctx: Parameters<GlobalOverlayEditor['onStart']>[0]): void {
55
+ const raw = ctx.value ?? '';
56
+ const isValid = this.#items.some(i => i.value === raw);
57
+ this.#currentValue =
58
+ isValid ? raw : (
59
+ (((ctx.table as { getCellOriginValue(c: number, r: number): unknown } | null)?.getCellOriginValue(
60
+ ctx.col,
61
+ ctx.row
62
+ ) as string | undefined) ?? '')
63
+ );
64
+ super.onStart(ctx);
65
+ }
66
+
67
+ /** 返回当前选中的条目值 */
68
+ override getValue(): string {
69
+ return this.#currentValue;
70
+ }
71
+
72
+ protected override onDeleteKey(): void {
73
+ this.#currentValue = '';
74
+ this.close();
75
+ }
76
+
77
+ protected buildRows(panel: HTMLDivElement): void {
78
+ this.#items.forEach((item, idx) => {
79
+ const isSelected = item.value === this.#currentValue;
80
+ const row = document.createElement('div');
81
+ row.dataset['value'] = item.value;
82
+ row.dataset['idx'] = String(idx);
83
+ row.style.cssText = [
84
+ 'display:flex',
85
+ 'align-items:center',
86
+ 'gap:6px',
87
+ 'padding:5px 10px',
88
+ 'cursor:pointer',
89
+ 'font-size:12px',
90
+ `color:${DAISY_COLORS.text}`,
91
+ `background:${isSelected ? DAISY_COLORS.selectedBg : 'transparent'}`
92
+ ].join(';');
93
+
94
+ if (item.icon && item.color) {
95
+ row.appendChild(lucideToSvgElement(item.icon, item.color));
96
+ const span = document.createElement('span');
97
+ span.textContent = item.text;
98
+ row.appendChild(span);
99
+ } else {
100
+ row.textContent = item.text;
101
+ }
102
+
103
+ row.addEventListener('mouseenter', () => this.setFocused(idx));
104
+ row.addEventListener('mousedown', e => {
105
+ e.preventDefault();
106
+ e.stopPropagation();
107
+ this.onRowClick(idx);
108
+ });
109
+
110
+ panel.appendChild(row);
111
+ });
112
+ }
113
+
114
+ protected onRowClick(idx: number): void {
115
+ const item = this.#items[idx];
116
+ if (!item) return;
117
+ this.#currentValue = item.value;
118
+ this.close();
119
+ }
120
+
121
+ protected onFocusChange(idx: number, prev: number): void {
122
+ const prevRow = this.rowAt(prev);
123
+ if (prevRow) {
124
+ const isSelected = prevRow.dataset['value'] === this.#currentValue;
125
+ prevRow.style.background = isSelected ? DAISY_COLORS.selectedBg : 'transparent';
126
+ }
127
+ const nextRow = this.rowAt(idx);
128
+ if (nextRow) nextRow.style.background = DAISY_COLORS.focusBg;
129
+ }
130
+ }
@@ -0,0 +1,212 @@
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
+ /**
7
+ * JSON / KeyValue 编辑器
8
+ *
9
+ * 浮层显示带语法提示的 textarea,适合编辑 json 和 keyValue 类型。
10
+ * - Ctrl+Enter:验证并提交
11
+ * - Escape:取消(恢复原始值)
12
+ * - 点击外部:best-effort 提交(JSON 合法则提交,否则保持原始值)
13
+ * - 实时显示 JSON 格式错误
14
+ */
15
+ export class JsonEditor implements IEditor<unknown> {
16
+ #value: unknown = null;
17
+ #originalValue: unknown = null;
18
+ #panel: HTMLDivElement | null = null;
19
+ #textarea: HTMLTextAreaElement | null = null;
20
+ #errorEl: HTMLDivElement | null = null;
21
+ #endEdit: (() => void) | null = null;
22
+ #outsideHandler: ((e: MouseEvent) => void) | null = null;
23
+
24
+ /** VTable 编辑器类型标识(注册到 VTable 的编辑器名) */
25
+ readonly editorType = 'json-editor';
26
+
27
+ /** 打开编辑器:构建浮层 textarea 面板并初始化原始值 */
28
+ onStart(ctx: EditContext<unknown>): void {
29
+ const container = ctx.container as HTMLElement;
30
+ this.#endEdit = () => {
31
+ refocusTable(container);
32
+ ctx.endEdit();
33
+ };
34
+ const origin = (ctx.table as TableLike)?.getCellOriginValue(ctx.col, ctx.row) ?? ctx.value;
35
+ this.#value = origin ?? null;
36
+ this.#originalValue = this.#value;
37
+
38
+ const panel = document.createElement('div');
39
+ panel.style.cssText = [
40
+ 'position:fixed',
41
+ 'z-index:9999',
42
+ 'padding:8px',
43
+ 'min-width:280px',
44
+ `background:${DAISY_COLORS.bg}`,
45
+ `border:1px solid ${DAISY_COLORS.border}`,
46
+ 'border-radius:8px',
47
+ 'box-shadow:0 4px 16px rgba(0,0,0,.18)',
48
+ 'display:flex',
49
+ 'flex-direction:column',
50
+ 'gap:4px'
51
+ ].join(';');
52
+
53
+ const topRow = document.createElement('div');
54
+ topRow.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:4px';
55
+
56
+ const hint = document.createElement('div');
57
+ hint.style.cssText = `font-size:12px;opacity:0.45;color:${DAISY_COLORS.text}`;
58
+ hint.textContent = 'Ctrl+Enter 确认 · Esc 取消';
59
+
60
+ const copyBtn = document.createElement('button');
61
+ copyBtn.type = 'button';
62
+ copyBtn.textContent = '复制';
63
+ copyBtn.title = '复制 JSON';
64
+ copyBtn.style.cssText = [
65
+ 'font-size:12px',
66
+ `color:${DAISY_COLORS.text}`,
67
+ 'opacity:0.6',
68
+ 'background:none',
69
+ `border:1px solid ${DAISY_COLORS.border}`,
70
+ 'border-radius:4px',
71
+ 'cursor:pointer',
72
+ 'padding:1px 6px',
73
+ 'white-space:nowrap',
74
+ 'flex-shrink:0'
75
+ ].join(';');
76
+ copyBtn.addEventListener('mousedown', e => {
77
+ e.preventDefault();
78
+ e.stopPropagation();
79
+ const text = this.#textarea?.value ?? '';
80
+ navigator.clipboard?.writeText(text).catch(() => {
81
+ /* ignore */
82
+ });
83
+ copyBtn.textContent = '✓';
84
+ setTimeout(() => {
85
+ copyBtn.textContent = '复制';
86
+ }, 1200);
87
+ });
88
+
89
+ topRow.appendChild(hint);
90
+ topRow.appendChild(copyBtn);
91
+
92
+ const textarea = document.createElement('textarea');
93
+ textarea.style.cssText = [
94
+ 'width:280px',
95
+ 'height:140px',
96
+ 'resize:both',
97
+ `background:${DAISY_COLORS.bg}`,
98
+ `color:${DAISY_COLORS.text}`,
99
+ `border:1px solid ${DAISY_COLORS.border}`,
100
+ 'border-radius:4px',
101
+ 'padding:6px',
102
+ 'font-size:12px',
103
+ 'font-family:monospace',
104
+ 'outline:none',
105
+ 'line-height:1.5'
106
+ ].join(';');
107
+ try {
108
+ textarea.value = this.#value != null ? JSON.stringify(this.#value, null, 2) : '';
109
+ } catch {
110
+ textarea.value = '';
111
+ }
112
+ this.#textarea = textarea;
113
+
114
+ const errorEl = document.createElement('div');
115
+ errorEl.style.cssText = `font-size:12px;color:${DAISY_COLORS.error};min-height:14px;word-break:break-all`;
116
+ this.#errorEl = errorEl;
117
+
118
+ textarea.addEventListener('input', () => this.#validate());
119
+ textarea.addEventListener('keydown', e => {
120
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
121
+ e.preventDefault();
122
+ e.stopPropagation();
123
+ if (this.#tryCommit()) this.#endEdit?.();
124
+ } else if (e.key === 'Escape') {
125
+ e.preventDefault();
126
+ e.stopPropagation();
127
+ this.#value = this.#originalValue; // 恢复原始值
128
+ this.#endEdit?.();
129
+ }
130
+ // Enter 在 textarea 里正常换行,不拦截
131
+ });
132
+
133
+ panel.appendChild(topRow);
134
+ panel.appendChild(textarea);
135
+ panel.appendChild(errorEl);
136
+ document.body.appendChild(panel);
137
+ positionOverlayPanel(panel, ctx.referencePosition.rect, ctx.container as HTMLElement, 200, { maxRight: 320 });
138
+ this.#panel = panel;
139
+
140
+ this.#outsideHandler = (e: MouseEvent) => {
141
+ if (!panel.contains(e.target as Node)) {
142
+ // 点击外部:best-effort 提交
143
+ this.#tryCommit();
144
+ this.#cleanup();
145
+ this.#endEdit?.();
146
+ }
147
+ };
148
+ scheduleEditorSetup(() => {
149
+ if (!this.#outsideHandler) return;
150
+ document.addEventListener('mousedown', this.#outsideHandler, { capture: true });
151
+ textarea.focus();
152
+ // 光标移到末尾
153
+ textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
154
+ });
155
+ }
156
+
157
+ /** 返回当前编辑值 */
158
+ getValue(): unknown {
159
+ return this.#value;
160
+ }
161
+
162
+ /** 结束编辑:清理浮层面板与事件监听 */
163
+ onEnd(): void {
164
+ this.#cleanup();
165
+ }
166
+
167
+ /** 判断目标元素是否属于编辑器浮层 */
168
+ isEditorElement(el: HTMLElement | EventTarget): boolean {
169
+ return !!this.#panel?.contains(el as Node);
170
+ }
171
+
172
+ #validate(): boolean {
173
+ const text = (this.#textarea?.value ?? '').trim();
174
+ if (!text) {
175
+ if (this.#errorEl) this.#errorEl.textContent = '';
176
+ return true;
177
+ }
178
+ try {
179
+ JSON.parse(text);
180
+ if (this.#errorEl) this.#errorEl.textContent = '';
181
+ return true;
182
+ } catch (e) {
183
+ if (this.#errorEl) this.#errorEl.textContent = `⚠ ${(e as Error).message}`;
184
+ return false;
185
+ }
186
+ }
187
+
188
+ #tryCommit(): boolean {
189
+ const text = (this.#textarea?.value ?? '').trim();
190
+ if (!text) {
191
+ this.#value = null;
192
+ return true;
193
+ }
194
+ try {
195
+ this.#value = JSON.parse(text);
196
+ return true;
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ #cleanup(): void {
203
+ if (this.#outsideHandler) {
204
+ document.removeEventListener('mousedown', this.#outsideHandler, { capture: true });
205
+ this.#outsideHandler = null;
206
+ }
207
+ this.#panel?.remove();
208
+ this.#panel = null;
209
+ this.#textarea = null;
210
+ this.#errorEl = null;
211
+ }
212
+ }