@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,239 @@
1
+ import type { EditContext, IEditor, RectProps } from '@visactor/vtable-editors';
2
+ import { DAISY_COLORS, refocusTable, scheduleEditorSetup } from './global-overlay-editor.js';
3
+
4
+ type TableLike = { getCellOriginValue(col: number, row: number): unknown } | null;
5
+
6
+ /** 十六进制颜色(允许省略 `#`) */
7
+ const HEX_COLOR_RE = /^#?[0-9a-fA-F]{6}$/;
8
+
9
+ /** 把用户输入规范化为 `#rrggbb`;输入非法返回 null。 */
10
+ const normalizeHexColor = (text: string): string | null => {
11
+ const trimmed = text.trim();
12
+ if (!HEX_COLOR_RE.test(trimmed)) return null;
13
+ return trimmed.startsWith('#') ? trimmed.toLowerCase() : `#${trimmed.toLowerCase()}`;
14
+ };
15
+
16
+ /**
17
+ * 颜色编辑器
18
+ *
19
+ * 在单元格上方弹出原生取色器 + 十六进制文本输入,两者双向同步。
20
+ * Enter / Tab 提交;Escape 恢复原值;点击外部提交,提交失败时恢复原值。
21
+ */
22
+ export class ColorEditor implements IEditor<unknown> {
23
+ #value: string | null = null;
24
+ #originalValue: string | null = null;
25
+ #input: HTMLInputElement | null = null;
26
+ #picker: HTMLInputElement | null = null;
27
+ #tooltip: HTMLDivElement | null = null;
28
+ #container: HTMLElement | null = null;
29
+ #endEdit: (() => void) | null = null;
30
+ #outsideHandler: ((e: MouseEvent) => void) | null = null;
31
+
32
+ /** VTable 编辑器类型标识(注册到 VTable 的编辑器名) */
33
+ readonly editorType = 'color-editor';
34
+
35
+ /** 创建并显示取色器、文本输入与错误提示浮层 */
36
+ onStart(ctx: EditContext<unknown>): void {
37
+ const container = ctx.container as HTMLElement;
38
+ this.#container = container;
39
+ this.#endEdit = () => {
40
+ refocusTable(container);
41
+ ctx.endEdit();
42
+ };
43
+ const origin = (ctx.table as TableLike)?.getCellOriginValue(ctx.col, ctx.row) ?? ctx.value;
44
+ const normalized = origin != null && origin !== '' ? normalizeHexColor(String(origin)) : null;
45
+ this.#value = normalized;
46
+ this.#originalValue = normalized;
47
+
48
+ const rect = ctx.referencePosition.rect;
49
+ const cRect = container.getBoundingClientRect();
50
+ const absLeft = cRect.left + rect.left;
51
+ const absTop = cRect.top + rect.top;
52
+ const w = Math.max(150, rect.width ?? 0);
53
+ const h = Math.max(24, rect.height ?? 0);
54
+
55
+ const input = document.createElement('input');
56
+ input.type = 'text';
57
+ input.placeholder = '#rrggbb';
58
+ input.value = this.#value ?? '';
59
+ input.style.cssText = [
60
+ 'position:fixed',
61
+ 'z-index:9999',
62
+ 'padding:4px 6px',
63
+ 'box-sizing:border-box',
64
+ `background:${DAISY_COLORS.bg}`,
65
+ `color:${DAISY_COLORS.text}`,
66
+ 'border-radius:0',
67
+ `border:2px solid ${DAISY_COLORS.primary}`,
68
+ 'outline:none',
69
+ 'font-size:13px',
70
+ `left:${absLeft + 26}px`,
71
+ `top:${absTop}px`,
72
+ `width:${Math.max(60, w - 26)}px`,
73
+ `height:${h}px`
74
+ ].join(';');
75
+
76
+ const picker = document.createElement('input');
77
+ picker.type = 'color';
78
+ picker.value = this.#value ?? '#000000';
79
+ picker.title = '选择颜色';
80
+ picker.style.cssText = [
81
+ 'position:fixed',
82
+ 'z-index:9999',
83
+ 'box-sizing:border-box',
84
+ 'padding:0',
85
+ 'border:none',
86
+ 'background:transparent',
87
+ 'cursor:pointer',
88
+ `left:${absLeft + 4}px`,
89
+ `top:${absTop + 2}px`,
90
+ 'width:22px',
91
+ 'height:20px'
92
+ ].join(';');
93
+
94
+ const tooltip = document.createElement('div');
95
+ tooltip.style.cssText = [
96
+ 'position:fixed',
97
+ 'z-index:10000',
98
+ 'padding:4px 8px',
99
+ 'font-size:11px',
100
+ `color:${DAISY_COLORS.errorContent}`,
101
+ `background:${DAISY_COLORS.error}`,
102
+ 'border-radius:4px',
103
+ 'box-shadow:0 2px 8px rgba(0,0,0,.15)',
104
+ 'white-space:nowrap',
105
+ 'display:none',
106
+ 'pointer-events:none',
107
+ `left:${absLeft}px`,
108
+ `top:${absTop + h + 2}px`
109
+ ].join(';');
110
+
111
+ const setError = (show: boolean, msg?: string) => {
112
+ if (show && msg) {
113
+ tooltip.textContent = msg;
114
+ tooltip.style.display = 'block';
115
+ input.style.borderColor = DAISY_COLORS.error;
116
+ } else {
117
+ tooltip.textContent = '';
118
+ tooltip.style.display = 'none';
119
+ input.style.borderColor = DAISY_COLORS.primary;
120
+ }
121
+ };
122
+
123
+ picker.addEventListener('input', () => {
124
+ input.value = picker.value;
125
+ setError(false);
126
+ });
127
+
128
+ input.addEventListener('input', () => {
129
+ const normalizedInput = normalizeHexColor(input.value);
130
+ if (input.value !== '' && normalizedInput !== null) {
131
+ picker.value = normalizedInput;
132
+ setError(false);
133
+ }
134
+ });
135
+
136
+ const commit = (): boolean => {
137
+ if (input.value.trim() === '') {
138
+ this.#value = null;
139
+ return true;
140
+ }
141
+ const normalizedInput = normalizeHexColor(input.value);
142
+ if (normalizedInput === null) return false;
143
+ this.#value = normalizedInput;
144
+ return true;
145
+ };
146
+
147
+ input.addEventListener('keydown', e => {
148
+ if (e.key === 'Enter' || e.key === 'Tab') {
149
+ e.preventDefault();
150
+ e.stopPropagation();
151
+ if (commit()) {
152
+ this.#endEdit?.();
153
+ } else {
154
+ setError(true, '请输入 #rrggbb 格式的颜色');
155
+ }
156
+ } else if (e.key === 'Escape') {
157
+ e.preventDefault();
158
+ e.stopPropagation();
159
+ this.#value = this.#originalValue;
160
+ this.#endEdit?.();
161
+ }
162
+ });
163
+
164
+ document.body.appendChild(picker);
165
+ document.body.appendChild(input);
166
+ document.body.appendChild(tooltip);
167
+ this.#input = input;
168
+ this.#picker = picker;
169
+ this.#tooltip = tooltip;
170
+
171
+ this.#outsideHandler = (e: MouseEvent) => {
172
+ const target = e.target as Node;
173
+ if (target !== input && target !== picker && !tooltip.contains(target)) {
174
+ if (!commit()) {
175
+ this.#value = this.#originalValue;
176
+ }
177
+ this.#cleanup();
178
+ this.#endEdit?.();
179
+ }
180
+ };
181
+ scheduleEditorSetup(() => {
182
+ if (!this.#outsideHandler) return;
183
+ document.addEventListener('mousedown', this.#outsideHandler, { capture: true });
184
+ input.focus();
185
+ input.select();
186
+ });
187
+ }
188
+
189
+ /** 返回当前颜色值,空值为 null */
190
+ getValue(): string | null {
191
+ return this.#value;
192
+ }
193
+
194
+ /** 移除输入框与提示浮层,解绑全局点击事件 */
195
+ onEnd(): void {
196
+ this.#cleanup();
197
+ }
198
+
199
+ /** 判断目标元素是否属于本编辑器 */
200
+ isEditorElement(el: HTMLElement | EventTarget): boolean {
201
+ const node = el as Node;
202
+ return node === this.#input || node === this.#picker || this.#tooltip?.contains(node) === true;
203
+ }
204
+
205
+ /** 按新的单元格矩形重新定位取色器、输入框与提示浮层 */
206
+ adjustPosition(rect: RectProps): void {
207
+ const input = this.#input;
208
+ const container = this.#container;
209
+ if (!input || !container) return;
210
+ const cRect = container.getBoundingClientRect();
211
+ const absLeft = cRect.left + rect.left;
212
+ const absTop = cRect.top + rect.top;
213
+ const h = Math.max(24, rect.height ?? 0);
214
+ if (this.#picker) {
215
+ this.#picker.style.left = `${absLeft + 4}px`;
216
+ this.#picker.style.top = `${absTop + 2}px`;
217
+ }
218
+ input.style.left = `${absLeft + 26}px`;
219
+ input.style.top = `${absTop}px`;
220
+ if (this.#tooltip) {
221
+ this.#tooltip.style.left = `${absLeft}px`;
222
+ this.#tooltip.style.top = `${absTop + h + 2}px`;
223
+ }
224
+ }
225
+
226
+ #cleanup(): void {
227
+ if (this.#outsideHandler) {
228
+ document.removeEventListener('mousedown', this.#outsideHandler, { capture: true });
229
+ this.#outsideHandler = null;
230
+ }
231
+ this.#input?.remove();
232
+ this.#input = null;
233
+ this.#picker?.remove();
234
+ this.#picker = null;
235
+ this.#tooltip?.remove();
236
+ this.#tooltip = null;
237
+ this.#container = null;
238
+ }
239
+ }
@@ -0,0 +1,169 @@
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
+ * 日期时间选择编辑器
8
+ *
9
+ * 直接覆盖单元格显示原生 datetime-local 输入框。
10
+ * - Enter / Tab:确认
11
+ * - Escape:取消(恢复原始值)
12
+ * - 点击外部:确认并关闭
13
+ * - 「清除」按钮:将值设为 null
14
+ */
15
+ export class DateEditor implements IEditor<unknown> {
16
+ #value: Date | null = null;
17
+ #originalValue: Date | null = null;
18
+ #panel: HTMLDivElement | null = null;
19
+ #endEdit: (() => void) | null = null;
20
+ #outsideHandler: ((e: MouseEvent) => void) | null = null;
21
+
22
+ /** VTable 编辑器类型标识(注册到 VTable 的编辑器名) */
23
+ readonly editorType = 'date-editor';
24
+
25
+ /** 打开编辑器:构建浮层面板并初始化原生 datetime-local 输入 */
26
+ onStart(ctx: EditContext<unknown>): void {
27
+ const container = ctx.container as HTMLElement;
28
+ this.#endEdit = () => {
29
+ refocusTable(container);
30
+ ctx.endEdit();
31
+ };
32
+ const origin = (ctx.table as TableLike)?.getCellOriginValue(ctx.col, ctx.row) ?? ctx.value;
33
+
34
+ if (origin instanceof Date) {
35
+ this.#value = isNaN(origin.getTime()) ? null : new Date(origin.getTime());
36
+ } else if (origin != null && origin !== '') {
37
+ const d = new Date(origin as string | number);
38
+ this.#value = isNaN(d.getTime()) ? null : d;
39
+ } else {
40
+ this.#value = null;
41
+ }
42
+ this.#originalValue = this.#value ? new Date(this.#value.getTime()) : null;
43
+
44
+ const panel = document.createElement('div');
45
+ panel.style.cssText = [
46
+ 'position:fixed',
47
+ 'z-index:9999',
48
+ 'padding:4px 6px',
49
+ `background:${DAISY_COLORS.bg}`,
50
+ `border:1px solid ${DAISY_COLORS.border}`,
51
+ 'border-radius:6px',
52
+ 'box-shadow:0 2px 8px rgba(0,0,0,.15)',
53
+ 'display:flex',
54
+ 'flex-direction:row',
55
+ 'align-items:center',
56
+ 'gap:4px'
57
+ ].join(';');
58
+
59
+ const input = document.createElement('input');
60
+ input.type = 'datetime-local';
61
+ input.style.cssText = [
62
+ `background:${DAISY_COLORS.bg}`,
63
+ `color:${DAISY_COLORS.text}`,
64
+ 'border:none',
65
+ 'outline:none',
66
+ 'font-size:13px',
67
+ 'flex:1',
68
+ 'min-width:200px'
69
+ ].join(';');
70
+ if (this.#value) input.value = this.#formatForInput(this.#value);
71
+
72
+ input.addEventListener('change', () => {
73
+ this.#value = this.#resolveValue(input.value);
74
+ });
75
+ input.addEventListener('keydown', e => {
76
+ if (e.key === 'Enter' || e.key === 'Tab') {
77
+ e.preventDefault();
78
+ e.stopPropagation();
79
+ this.#value = this.#resolveValue(input.value);
80
+ this.#endEdit?.();
81
+ } else if (e.key === 'Escape') {
82
+ e.preventDefault();
83
+ e.stopPropagation();
84
+ this.#value = this.#originalValue; // 恢复原始值
85
+ this.#endEdit?.();
86
+ }
87
+ });
88
+
89
+ const clearBtn = document.createElement('button');
90
+ clearBtn.type = 'button';
91
+ clearBtn.textContent = '清除';
92
+ clearBtn.style.cssText = [
93
+ 'font-size:12px',
94
+ `color:${DAISY_COLORS.text}`,
95
+ 'opacity:0.6',
96
+ 'background:none',
97
+ 'border:none',
98
+ 'cursor:pointer',
99
+ 'white-space:nowrap',
100
+ 'padding:0 2px',
101
+ 'flex-shrink:0'
102
+ ].join(';');
103
+ clearBtn.addEventListener('mousedown', e => {
104
+ e.preventDefault();
105
+ this.#value = null;
106
+ this.#endEdit?.();
107
+ });
108
+
109
+ panel.appendChild(input);
110
+ panel.appendChild(clearBtn);
111
+ document.body.appendChild(panel);
112
+ positionOverlayPanel(panel, ctx.referencePosition.rect, ctx.container as HTMLElement, 80, { minWidth: 240 });
113
+ this.#panel = panel;
114
+
115
+ this.#outsideHandler = (e: MouseEvent) => {
116
+ if (!panel.contains(e.target as Node)) {
117
+ this.#value = this.#resolveValue(input.value);
118
+ this.#cleanup();
119
+ this.#endEdit?.();
120
+ }
121
+ };
122
+ scheduleEditorSetup(() => {
123
+ if (!this.#outsideHandler) return;
124
+ document.addEventListener('mousedown', this.#outsideHandler, { capture: true });
125
+ input.focus();
126
+ });
127
+ }
128
+
129
+ /** 返回当前选择的日期值 */
130
+ getValue(): Date | null {
131
+ return this.#value;
132
+ }
133
+
134
+ /** 结束编辑:清理浮层面板与事件监听 */
135
+ onEnd(): void {
136
+ this.#cleanup();
137
+ }
138
+
139
+ /** 判断目标元素是否属于编辑器浮层 */
140
+ isEditorElement(el: HTMLElement | EventTarget): boolean {
141
+ return !!this.#panel?.contains(el as Node);
142
+ }
143
+
144
+ /** 将 input.value 解析为 Date,若与原始值等分钟则返回原始值(避免更新毫秒)*/
145
+ #resolveValue(inputVal: string): Date | null {
146
+ if (!inputVal) return null;
147
+ const orig = this.#originalValue;
148
+ if (orig && inputVal === this.#formatForInput(orig)) {
149
+ return orig; // 分钟精度未变,返回原值(保留原始毫秒)
150
+ }
151
+ const d = new Date(inputVal);
152
+ d.setSeconds(0, 0); // 输入框不支持秒/毫秒,统一归零
153
+ return d;
154
+ }
155
+
156
+ #formatForInput(d: Date): string {
157
+ const pad = (n: number) => String(n).padStart(2, '0');
158
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
159
+ }
160
+
161
+ #cleanup(): void {
162
+ if (this.#outsideHandler) {
163
+ document.removeEventListener('mousedown', this.#outsideHandler, { capture: true });
164
+ this.#outsideHandler = null;
165
+ }
166
+ this.#panel?.remove();
167
+ this.#panel = null;
168
+ }
169
+ }
@@ -0,0 +1,167 @@
1
+ import { DAISY_COLORS, GlobalOverlayEditor } from './global-overlay-editor.js';
2
+ import { lucideToSvgElement, type IconData } from './lucide-svg.js';
3
+
4
+ /** 枚举选项配置 */
5
+ export interface EnumItem {
6
+ /** 存储值 */
7
+ value: string;
8
+ /** 显示文本(默认等于 value) */
9
+ text?: string;
10
+ /** Lucide 图标数据(可选,配合 color 使用) */
11
+ icon?: IconData;
12
+ /** 图标描边颜色(可选,配合 icon 使用) */
13
+ color?: string;
14
+ /** 是否禁用(只影响展示与选择,不改变枚举合法性) */
15
+ disabled?: boolean;
16
+ }
17
+
18
+ type VTableWithColumnDef = {
19
+ getCellOriginValue(col: number, row: number): unknown;
20
+ getColumnDefine?(col: number): Record<string, unknown> | undefined;
21
+ };
22
+
23
+ /**
24
+ * 枚举下拉选择编辑器
25
+ *
26
+ * 从列定义的 `menuList` 动态读取可选项,支持键盘导航。
27
+ * - ↑ ↓:移动选中项
28
+ * - Enter / 点击:确认选中并关闭
29
+ * - Delete / Backspace:清空值并关闭
30
+ * - Escape:取消,恢复原始值
31
+ *
32
+ * 未来可扩展 icon、i18n 等字段通过 menuList 传入。
33
+ */
34
+ export class EnumEditor extends GlobalOverlayEditor {
35
+ #currentValue = '';
36
+ #items: EnumItem[] = [];
37
+ #presetItems: EnumItem[] | null = null;
38
+
39
+ protected get initialFocusIndex(): number {
40
+ return this.#items.findIndex(i => i.value === this.#currentValue);
41
+ }
42
+
43
+ protected get itemCount(): number {
44
+ return this.#items.length;
45
+ }
46
+
47
+ /** VTable 编辑器类型标识(注册到 VTable 的编辑器名) */
48
+ readonly editorType = 'enum-editor';
49
+
50
+ /**
51
+ * 创建枚举编辑器
52
+ *
53
+ * @param presetItems 预设选项;不传时在 onStart 中从列定义 menuList 读取
54
+ */
55
+ constructor(presetItems?: EnumItem[]) {
56
+ super();
57
+ this.#presetItems = presetItems ?? null;
58
+ }
59
+
60
+ /** 初始化编辑:解析原始值并加载可选项(预设选项优先,其次列定义 menuList) */
61
+ override onStart(ctx: Parameters<GlobalOverlayEditor['onStart']>[0]): void {
62
+ const table = ctx.table as VTableWithColumnDef | null;
63
+ const origin = table?.getCellOriginValue(ctx.col, ctx.row) ?? ctx.value;
64
+ this.#currentValue = origin != null ? String(origin) : '';
65
+
66
+ // 优先使用构造时注入的预设选项,否则从列定义读取
67
+ const menuList =
68
+ this.#presetItems ?? (table?.getColumnDefine?.(ctx.col)?.['menuList'] as Array<EnumItem> | undefined);
69
+ this.#items = (menuList ?? []).map(item => ({
70
+ ...item,
71
+ text: item.text ?? item.value
72
+ }));
73
+
74
+ // 是否允许 null(菜单列表中有 value='' 的项)
75
+ const hasNullOption = this.#items.some(i => i.value === '');
76
+ const realItems = this.#items.filter(i => i.value !== '');
77
+
78
+ if (!hasNullOption && this.#currentValue === '') {
79
+ // 不允许 null 且当前值为空,自动选择第一项
80
+ this.#currentValue = realItems[0]?.value ?? '';
81
+ } else if (!hasNullOption && !this.#items.some(i => i.value === this.#currentValue)) {
82
+ // 不允许 null 且当前值不在列表中,自动选择第一项
83
+ this.#currentValue = realItems[0]?.value ?? '';
84
+ }
85
+
86
+ super.onStart(ctx);
87
+ }
88
+
89
+ /** 返回当前选中的枚举值 */
90
+ override getValue(): string {
91
+ return this.#currentValue;
92
+ }
93
+
94
+ protected override onDeleteKey(): void {
95
+ // 仅当列表包含 null 选项时才允许清除
96
+ if (this.#items.some(i => i.value === '')) {
97
+ this.#currentValue = '';
98
+ this.close();
99
+ }
100
+ }
101
+
102
+ protected buildRows(panel: HTMLDivElement): void {
103
+ if (this.#items.length === 0) {
104
+ const empty = document.createElement('div');
105
+ empty.style.cssText = `padding:8px 12px;font-size:12px;opacity:0.45;color:${DAISY_COLORS.text}`;
106
+ empty.textContent = '暂无可选项';
107
+ panel.appendChild(empty);
108
+ return;
109
+ }
110
+
111
+ this.#items.forEach((item, idx) => {
112
+ const isNullItem = item.value === '';
113
+ const isSelected = item.value === this.#currentValue;
114
+ const isDisabled = item.disabled === true;
115
+ const row = document.createElement('div');
116
+ row.dataset['value'] = item.value;
117
+ row.dataset['idx'] = String(idx);
118
+ row.style.cssText = [
119
+ 'display:flex',
120
+ 'align-items:center',
121
+ 'padding:5px 12px',
122
+ `cursor:${isDisabled ? 'default' : 'pointer'}`,
123
+ 'font-size:13px',
124
+ `color:${DAISY_COLORS.text}`,
125
+ `opacity:${isNullItem || isDisabled ? '0.55' : '1'}`,
126
+ `font-style:${isNullItem ? 'italic' : 'normal'}`,
127
+ `background:${isSelected ? DAISY_COLORS.selectedBg : 'transparent'}`
128
+ ].join(';');
129
+ if (item.icon && item.color) {
130
+ row.appendChild(lucideToSvgElement(item.icon, item.color));
131
+ const span = document.createElement('span');
132
+ span.textContent = item.text ?? item.value;
133
+ row.appendChild(span);
134
+ } else {
135
+ row.textContent = item.text ?? item.value;
136
+ }
137
+
138
+ row.addEventListener('mouseenter', () => {
139
+ if (!isDisabled) this.setFocused(idx);
140
+ });
141
+ row.addEventListener('mousedown', e => {
142
+ e.preventDefault();
143
+ e.stopPropagation();
144
+ if (!isDisabled) this.onRowClick(idx);
145
+ });
146
+
147
+ panel.appendChild(row);
148
+ });
149
+ }
150
+
151
+ protected onRowClick(idx: number): void {
152
+ const item = this.#items[idx];
153
+ if (!item || item.disabled === true) return;
154
+ this.#currentValue = item.value;
155
+ this.close();
156
+ }
157
+
158
+ protected onFocusChange(idx: number, prev: number): void {
159
+ const prevRow = this.rowAt(prev);
160
+ if (prevRow) {
161
+ prevRow.style.background =
162
+ prevRow.dataset['value'] === this.#currentValue ? DAISY_COLORS.selectedBg : 'transparent';
163
+ }
164
+ const nextRow = this.rowAt(idx);
165
+ if (nextRow) nextRow.style.background = DAISY_COLORS.focusBg;
166
+ }
167
+ }