@aiao/rxdb-model 0.0.19 → 0.0.26

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 (181) 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 +31 -0
  6. package/dist/entity-capabilities.d.ts.map +1 -0
  7. package/dist/entity-detail/detail-tabs.d.ts +13 -2
  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 +29 -1
  12. package/dist/entity-detail/interfaces.d.ts.map +1 -1
  13. package/dist/entity-field.utils.d.ts +60 -2
  14. package/dist/entity-field.utils.d.ts.map +1 -1
  15. package/dist/entity-form/form-data.d.ts +22 -1
  16. package/dist/entity-form/form-data.d.ts.map +1 -1
  17. package/dist/entity-form/form-fields.d.ts +25 -2
  18. package/dist/entity-form/form-fields.d.ts.map +1 -1
  19. package/dist/entity-form/form-validation.d.ts +20 -2
  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 +65 -1
  24. package/dist/entity-form/interfaces.d.ts.map +1 -1
  25. package/dist/entity-list/manual-order-list.d.ts +99 -0
  26. package/dist/entity-list/manual-order-list.d.ts.map +1 -0
  27. package/dist/entity-table/columns/build-editable-columns.d.ts +26 -4
  28. package/dist/entity-table/columns/build-editable-columns.d.ts.map +1 -1
  29. package/dist/entity-table/columns/column-utils.d.ts +78 -12
  30. package/dist/entity-table/columns/column-utils.d.ts.map +1 -1
  31. package/dist/entity-table/editors/color-editor.d.ts +23 -0
  32. package/dist/entity-table/editors/color-editor.d.ts.map +1 -0
  33. package/dist/entity-table/editors/date-editor.d.ts +6 -1
  34. package/dist/entity-table/editors/date-editor.d.ts.map +1 -1
  35. package/dist/entity-table/editors/enum-editor.d.ts +11 -1
  36. package/dist/entity-table/editors/enum-editor.d.ts.map +1 -1
  37. package/dist/entity-table/editors/global-overlay-editor.d.ts +27 -4
  38. package/dist/entity-table/editors/global-overlay-editor.d.ts.map +1 -1
  39. package/dist/entity-table/editors/icon-list-editor.d.ts +13 -1
  40. package/dist/entity-table/editors/icon-list-editor.d.ts.map +1 -1
  41. package/dist/entity-table/editors/json-editor.d.ts +6 -1
  42. package/dist/entity-table/editors/json-editor.d.ts.map +1 -1
  43. package/dist/entity-table/editors/key-value-editor.d.ts +12 -2
  44. package/dist/entity-table/editors/key-value-editor.d.ts.map +1 -1
  45. package/dist/entity-table/editors/lucide-svg.d.ts +3 -2
  46. package/dist/entity-table/editors/lucide-svg.d.ts.map +1 -1
  47. package/dist/entity-table/editors/multiselect-editor.d.ts +24 -6
  48. package/dist/entity-table/editors/multiselect-editor.d.ts.map +1 -1
  49. package/dist/entity-table/editors/number-editor.d.ts +33 -3
  50. package/dist/entity-table/editors/number-editor.d.ts.map +1 -1
  51. package/dist/entity-table/editors/relation-editor.d.ts +14 -2
  52. package/dist/entity-table/editors/relation-editor.d.ts.map +1 -1
  53. package/dist/entity-table/editors/safe-list-editor.d.ts +1 -0
  54. package/dist/entity-table/editors/safe-list-editor.d.ts.map +1 -1
  55. package/dist/entity-table/editors/tags-editor.d.ts +12 -1
  56. package/dist/entity-table/editors/tags-editor.d.ts.map +1 -1
  57. package/dist/entity-table/editors/text-format-editor.d.ts +32 -0
  58. package/dist/entity-table/editors/text-format-editor.d.ts.map +1 -0
  59. package/dist/entity-table/editors/uuid-editor.d.ts +14 -1
  60. package/dist/entity-table/editors/uuid-editor.d.ts.map +1 -1
  61. package/dist/entity-table/index.d.ts +13 -3
  62. package/dist/entity-table/index.d.ts.map +1 -1
  63. package/dist/entity-table/interfaces.d.ts +13 -0
  64. package/dist/entity-table/interfaces.d.ts.map +1 -1
  65. package/dist/entity-table/vtable/table-clipboard.d.ts +31 -2
  66. package/dist/entity-table/vtable/table-clipboard.d.ts.map +1 -1
  67. package/dist/entity-table/vtable/table-factory.d.ts +25 -5
  68. package/dist/entity-table/vtable/table-factory.d.ts.map +1 -1
  69. package/dist/entity-table/vtable/table-keyboard.d.ts +18 -1
  70. package/dist/entity-table/vtable/table-keyboard.d.ts.map +1 -1
  71. package/dist/entity-table/vtable/table-operations.d.ts +44 -3
  72. package/dist/entity-table/vtable/table-operations.d.ts.map +1 -1
  73. package/dist/entity-table/vtable/table-row-move.d.ts +88 -0
  74. package/dist/entity-table/vtable/table-row-move.d.ts.map +1 -0
  75. package/dist/entity-table/vtable/table-theme.d.ts +47 -2
  76. package/dist/entity-table/vtable/table-theme.d.ts.map +1 -1
  77. package/dist/entity-table/vtable/table-tooltip.d.ts +7 -1
  78. package/dist/entity-table/vtable/table-tooltip.d.ts.map +1 -1
  79. package/dist/entity-table/vtable/vtable-compat.d.ts +33 -5
  80. package/dist/entity-table/vtable/vtable-compat.d.ts.map +1 -1
  81. package/dist/entity-value.utils.d.ts +50 -2
  82. package/dist/entity-value.utils.d.ts.map +1 -1
  83. package/dist/index.d.ts +9 -0
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +4392 -64471
  86. package/dist/query-builder/models/config.interface.d.ts +2 -2
  87. package/dist/query-builder/models/operator.interface.d.ts +6 -2
  88. package/dist/query-builder/models/operator.interface.d.ts.map +1 -1
  89. package/dist/query-builder/models/query-builder-state.d.ts +31 -2
  90. package/dist/query-builder/models/query-builder-state.d.ts.map +1 -1
  91. package/dist/query-builder/models/validation.interface.d.ts +2 -2
  92. package/dist/query-builder/services/operator-registry.service.d.ts +13 -3
  93. package/dist/query-builder/services/operator-registry.service.d.ts.map +1 -1
  94. package/dist/query-builder/services/query-builder.service.d.ts +16 -6
  95. package/dist/query-builder/services/query-builder.service.d.ts.map +1 -1
  96. package/dist/query-builder/services/query-converter.service.d.ts +10 -2
  97. package/dist/query-builder/services/query-converter.service.d.ts.map +1 -1
  98. package/dist/query-builder/services/schema-parser.service.d.ts +10 -2
  99. package/dist/query-builder/services/schema-parser.service.d.ts.map +1 -1
  100. package/dist/query-builder/services/validation.service.d.ts +34 -4
  101. package/dist/query-builder/services/validation.service.d.ts.map +1 -1
  102. package/dist/query-builder/utils/date.d.ts +12 -0
  103. package/dist/query-builder/utils/date.d.ts.map +1 -1
  104. package/dist/query-builder/utils/field-tree.d.ts +7 -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 +15 -3
  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 +7 -3
  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 +21 -1
  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-table/columns/build-editable-columns.ts +184 -0
  133. package/src/entity-table/columns/column-utils.ts +657 -0
  134. package/src/entity-table/editors/color-editor.ts +239 -0
  135. package/src/entity-table/editors/date-editor.ts +169 -0
  136. package/src/entity-table/editors/enum-editor.ts +167 -0
  137. package/src/entity-table/editors/global-overlay-editor.ts +277 -0
  138. package/src/entity-table/editors/icon-list-editor.ts +130 -0
  139. package/src/entity-table/editors/json-editor.ts +212 -0
  140. package/src/entity-table/editors/key-value-editor.ts +595 -0
  141. package/src/entity-table/editors/lucide-svg.ts +78 -0
  142. package/src/entity-table/editors/multiselect-editor.ts +171 -0
  143. package/src/entity-table/editors/number-editor.ts +322 -0
  144. package/src/entity-table/editors/relation-editor.ts +316 -0
  145. package/src/entity-table/editors/safe-list-editor.ts +17 -0
  146. package/src/entity-table/editors/tags-editor.ts +239 -0
  147. package/src/entity-table/editors/text-format-editor.ts +220 -0
  148. package/src/entity-table/editors/uuid-editor.ts +198 -0
  149. package/src/entity-table/index.ts +96 -0
  150. package/src/entity-table/interfaces.ts +72 -0
  151. package/src/entity-table/vtable/table-clipboard.ts +419 -0
  152. package/src/entity-table/vtable/table-factory.ts +117 -0
  153. package/src/entity-table/vtable/table-keyboard.ts +89 -0
  154. package/src/entity-table/vtable/table-operations.ts +123 -0
  155. package/src/entity-table/vtable/table-row-move.ts +122 -0
  156. package/src/entity-table/vtable/table-theme.ts +181 -0
  157. package/src/entity-table/vtable/table-tooltip.ts +70 -0
  158. package/src/entity-table/vtable/vtable-compat.ts +84 -0
  159. package/src/entity-value.utils.ts +456 -0
  160. package/src/index.ts +57 -0
  161. package/src/query-builder/models/config.interface.ts +31 -0
  162. package/src/query-builder/models/index.ts +8 -0
  163. package/src/query-builder/models/operator.interface.ts +191 -0
  164. package/src/query-builder/models/query-builder-state.ts +222 -0
  165. package/src/query-builder/models/validation.interface.ts +184 -0
  166. package/src/query-builder/services/index.ts +9 -0
  167. package/src/query-builder/services/operator-registry.service.ts +243 -0
  168. package/src/query-builder/services/query-builder.service.ts +568 -0
  169. package/src/query-builder/services/query-converter.service.ts +153 -0
  170. package/src/query-builder/services/schema-parser.service.ts +309 -0
  171. package/src/query-builder/services/validation.service.ts +390 -0
  172. package/src/query-builder/utils/date.ts +35 -0
  173. package/src/query-builder/utils/field-tree.ts +400 -0
  174. package/src/query-builder/utils/id.ts +15 -0
  175. package/src/query-builder/utils/index.ts +12 -0
  176. package/src/query-builder/utils/metadata-field-extractor.ts +460 -0
  177. package/src/query-builder/utils/popover-position.ts +59 -0
  178. package/src/query-builder/utils/schema-factory.ts +75 -0
  179. package/src/query-builder/utils/structural-equal.ts +5 -0
  180. package/src/query-builder/utils/value-input.ts +105 -0
  181. package/src/structural-equal.ts +49 -0
@@ -0,0 +1,171 @@
1
+ import { DAISY_COLORS, GlobalOverlayEditor } from './global-overlay-editor.js';
2
+
3
+ /** 多选选项:id + 显示名 + 可选色点 / 禁用 */
4
+ export interface MultiSelectOption {
5
+ /** 选项唯一标识(也是存储值) */
6
+ id: string;
7
+ /** 选项显示名 */
8
+ name: string;
9
+ /** 色点颜色(可选,显示在复选框左侧) */
10
+ color?: string;
11
+ /** 是否禁用(只影响展示与选择,不改变合法性) */
12
+ disabled?: boolean;
13
+ }
14
+
15
+ /** 从 origin 值解析出已选 id 列表:数组逐项转字符串,字符串按逗号拆分。 */
16
+ function parseSelectedIds(origin: unknown): string[] {
17
+ if (Array.isArray(origin)) return (origin as unknown[]).map(String);
18
+ if (typeof origin === 'string') {
19
+ const result: string[] = [];
20
+ for (const part of origin.split(',')) {
21
+ const trimmed = part.trim();
22
+ if (trimmed !== '') result.push(trimmed);
23
+ }
24
+ return result;
25
+ }
26
+ return [];
27
+ }
28
+
29
+ /**
30
+ * VTable 自定义多选编辑器
31
+ *
32
+ * 弹出浮层显示 checkbox 列表,支持 toggle 多选;禁用的选项不可勾选。
33
+ * 结束编辑时以选中 id 列表(逗号分隔)写回单元格,并通过 onCommit 回调给出 id 数组。
34
+ * 未显式传入初始选中时,从单元格 origin 值读取。
35
+ */
36
+ export class MultiSelectEditor extends GlobalOverlayEditor {
37
+ readonly #options: MultiSelectOption[];
38
+ #selectedIds: Set<string>;
39
+ readonly #readOrigin: boolean;
40
+ readonly #onCommit: ((ids: string[]) => void) | undefined;
41
+
42
+ protected get initialFocusIndex(): number {
43
+ return -1;
44
+ }
45
+
46
+ protected get itemCount(): number {
47
+ return this.#options.length;
48
+ }
49
+
50
+ /** VTable 编辑器类型标识(注册到 VTable 的编辑器名) */
51
+ readonly editorType = 'multiselect-editor';
52
+
53
+ /**
54
+ * 创建多选编辑器
55
+ *
56
+ * @param options - 全部可选项
57
+ * @param selectedIds - 初始选中的选项 ID;不传(null / undefined)时从单元格 origin 值读取
58
+ * @param onCommit - 结束编辑时的提交回调,收到当前选中的 ID 列表
59
+ */
60
+ constructor(options: MultiSelectOption[], selectedIds?: string[] | null, onCommit?: (ids: string[]) => void) {
61
+ super();
62
+ this.#options = options;
63
+ this.#selectedIds = new Set(selectedIds ?? []);
64
+ this.#readOrigin = selectedIds == null;
65
+ this.#onCommit = onCommit;
66
+ }
67
+
68
+ /** 打开浮层:无显式初始选中时从单元格 origin 值解析已选 id */
69
+ override onStart(ctx: Parameters<GlobalOverlayEditor['onStart']>[0]): void {
70
+ if (this.#readOrigin) {
71
+ // 基类的 table/editCol/editRow 在 super.onStart 里才赋值,这里直接用 ctx
72
+ const table = ctx.table as { getCellOriginValue(col: number, row: number): unknown } | null;
73
+ const origin = table?.getCellOriginValue(ctx.col, ctx.row) ?? ctx.value;
74
+ this.#selectedIds = new Set(parseSelectedIds(origin));
75
+ }
76
+ super.onStart(ctx);
77
+ }
78
+
79
+ /** 结束编辑时以当前选中的 ID 列表触发提交回调 */
80
+ override onEnd(): void {
81
+ this.#onCommit?.([...this.#selectedIds]);
82
+ super.onEnd();
83
+ }
84
+
85
+ /** 返回当前选中选项的 id,以逗号加空格拼接(存储值口径,非显示名) */
86
+ getValue(): string {
87
+ return [...this.#selectedIds].join(', ');
88
+ }
89
+
90
+ protected buildRows(panel: HTMLDivElement): void {
91
+ if (this.#options.length === 0) {
92
+ const empty = document.createElement('div');
93
+ empty.style.cssText = 'padding:8px 12px;font-size:12px;opacity:.5';
94
+ empty.textContent = '—';
95
+ panel.appendChild(empty);
96
+ return;
97
+ }
98
+
99
+ this.#options.forEach((opt, idx) => {
100
+ const isDisabled = opt.disabled === true;
101
+ const row = document.createElement('div');
102
+ row.dataset['idx'] = String(idx);
103
+ row.style.cssText = [
104
+ 'display:flex',
105
+ 'align-items:center',
106
+ 'gap:6px',
107
+ 'padding:5px 12px',
108
+ `cursor:${isDisabled ? 'default' : 'pointer'}`,
109
+ 'font-size:13px',
110
+ 'user-select:none',
111
+ `color:${DAISY_COLORS.text}`,
112
+ `opacity:${isDisabled ? '0.55' : '1'}`
113
+ ].join(';');
114
+
115
+ const checkbox = document.createElement('input');
116
+ checkbox.type = 'checkbox';
117
+ checkbox.checked = this.#selectedIds.has(opt.id);
118
+ checkbox.disabled = isDisabled;
119
+ checkbox.style.cssText = 'pointer-events:none;flex-shrink:0';
120
+
121
+ row.appendChild(checkbox);
122
+
123
+ if (opt.color) {
124
+ const dot = document.createElement('span');
125
+ dot.style.cssText = [
126
+ 'width:8px',
127
+ 'height:8px',
128
+ 'border-radius:9999px',
129
+ 'flex-shrink:0',
130
+ `background:${opt.color}`
131
+ ].join(';');
132
+ row.appendChild(dot);
133
+ }
134
+
135
+ const label = document.createElement('span');
136
+ label.textContent = opt.name;
137
+ row.appendChild(label);
138
+
139
+ row.addEventListener('mouseenter', () => {
140
+ if (!isDisabled) this.setFocused(idx);
141
+ });
142
+ row.addEventListener('mousedown', e => {
143
+ e.preventDefault();
144
+ e.stopPropagation();
145
+ if (!isDisabled) this.onRowClick(idx);
146
+ });
147
+
148
+ panel.appendChild(row);
149
+ });
150
+ }
151
+
152
+ protected onRowClick(idx: number): void {
153
+ const opt = this.#options[idx];
154
+ if (!opt || opt.disabled === true) return;
155
+ const checkbox = this.rowAt(idx)?.querySelector<HTMLInputElement>('input[type="checkbox"]');
156
+ if (this.#selectedIds.has(opt.id)) {
157
+ this.#selectedIds.delete(opt.id);
158
+ if (checkbox) checkbox.checked = false;
159
+ } else {
160
+ this.#selectedIds.add(opt.id);
161
+ if (checkbox) checkbox.checked = true;
162
+ }
163
+ }
164
+
165
+ protected onFocusChange(idx: number, prev: number): void {
166
+ const prevRow = this.rowAt(prev);
167
+ if (prevRow) prevRow.style.background = '';
168
+ const nextRow = this.rowAt(idx);
169
+ if (nextRow) nextRow.style.background = DAISY_COLORS.hover;
170
+ }
171
+ }
@@ -0,0 +1,322 @@
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
+ export interface NumberEditorOptions {
8
+ /** true 为 64 位大整数模式(值类型 bigint) */
9
+ bigint?: boolean;
10
+ /** 最小值(含) */
11
+ min?: number;
12
+ /** 最大值(含) */
13
+ max?: number;
14
+ /** 取值步长(相对 min 对齐,容差 1e-8) */
15
+ step?: number;
16
+ }
17
+
18
+ /** 步长对齐判定容差:浮点除法残差小于它即视为对齐。 */
19
+ const STEP_TOLERANCE = 1e-8;
20
+
21
+ /** 有符号 64 位整数字面量(十进制) */
22
+ const INTEGER_RE = /^-?\d+$/;
23
+
24
+ /**
25
+ * 数字输入编辑器
26
+ *
27
+ * 在单元格上方弹出固定定位输入框,输入时实时校验并显示红色错误提示。
28
+ * Enter / Tab 提交;Escape 恢复原值;点击外部提交,提交失败时恢复原值。
29
+ * 支持整数 / 小数 / bigint 三种模式,并可按 min / max / step 约束值域。
30
+ */
31
+ export class NumberEditor implements IEditor<unknown> {
32
+ #value: number | bigint | null = null;
33
+ #originalValue: number | bigint | null = null;
34
+ #input: HTMLInputElement | null = null;
35
+ #tooltip: HTMLDivElement | null = null;
36
+ #container: HTMLElement | null = null;
37
+ #endEdit: (() => void) | null = null;
38
+ #outsideHandler: ((e: MouseEvent) => void) | null = null;
39
+ readonly #isInteger: boolean;
40
+ readonly #isBigint: boolean;
41
+ readonly #min: number | undefined;
42
+ readonly #max: number | undefined;
43
+ readonly #step: number | undefined;
44
+
45
+ /** VTable 编辑器类型标识(注册到 VTable 的编辑器名) */
46
+ readonly editorType: string;
47
+
48
+ /**
49
+ * 创建数字编辑器
50
+ *
51
+ * @param isInteger - true 为整数模式,false(默认)为小数模式
52
+ * @param options - 可选行为配置(bigint 模式与值域约束)
53
+ */
54
+ constructor(isInteger = false, options: NumberEditorOptions = {}) {
55
+ this.#isInteger = isInteger;
56
+ this.#isBigint = options.bigint === true;
57
+ this.#min = options.min;
58
+ this.#max = options.max;
59
+ this.#step = options.step;
60
+ this.editorType =
61
+ this.#isBigint ? 'bigint-editor'
62
+ : this.#isInteger ? 'integer-editor'
63
+ : 'number-editor';
64
+ }
65
+
66
+ /** 创建并显示输入框与错误提示浮层,绑定输入校验与提交逻辑 */
67
+ onStart(ctx: EditContext<unknown>): void {
68
+ const container = ctx.container as HTMLElement;
69
+ this.#container = container;
70
+ this.#endEdit = () => {
71
+ refocusTable(container);
72
+ ctx.endEdit();
73
+ };
74
+ const origin = (ctx.table as TableLike)?.getCellOriginValue(ctx.col, ctx.row) ?? ctx.value;
75
+
76
+ if (origin != null && origin !== '') {
77
+ const text = String(origin);
78
+ if (this.#isBigint && INTEGER_RE.test(text.trim())) {
79
+ this.#value = BigInt(text.trim());
80
+ } else if (!this.#isBigint) {
81
+ const n = this.#isInteger ? parseInt(text, 10) : parseFloat(text);
82
+ this.#value = isNaN(n) ? null : n;
83
+ } else {
84
+ this.#value = null;
85
+ }
86
+ } else {
87
+ this.#value = null;
88
+ }
89
+ this.#originalValue = this.#value;
90
+
91
+ const rect = ctx.referencePosition.rect;
92
+ const cRect = container.getBoundingClientRect();
93
+ const absLeft = cRect.left + rect.left;
94
+ const absTop = cRect.top + rect.top;
95
+ const w = Math.max(60, rect.width ?? 0);
96
+ const h = Math.max(24, rect.height ?? 0);
97
+
98
+ const input = document.createElement('input');
99
+ input.type = 'text';
100
+ input.inputMode =
101
+ this.#isBigint ? 'numeric'
102
+ : this.#isInteger ? 'numeric'
103
+ : 'decimal';
104
+ input.value = this.#value != null ? String(this.#value) : '';
105
+ input.style.cssText = [
106
+ 'position:fixed',
107
+ 'z-index:9999',
108
+ 'padding:4px',
109
+ 'box-sizing:border-box',
110
+ `background:${DAISY_COLORS.bg}`,
111
+ `color:${DAISY_COLORS.text}`,
112
+ 'border-radius:0',
113
+ `border:2px solid ${DAISY_COLORS.primary}`,
114
+ 'outline:none',
115
+ 'font-size:13px',
116
+ `left:${absLeft}px`,
117
+ `top:${absTop}px`,
118
+ `width:${w}px`,
119
+ `height:${h}px`
120
+ ].join(';');
121
+
122
+ const tooltip = document.createElement('div');
123
+ tooltip.style.cssText = [
124
+ 'position:fixed',
125
+ 'z-index:10000',
126
+ 'padding:4px 8px',
127
+ 'font-size:11px',
128
+ `color:${DAISY_COLORS.errorContent}`,
129
+ `background:${DAISY_COLORS.error}`,
130
+ 'border-radius:4px',
131
+ 'box-shadow:0 2px 8px rgba(0,0,0,.15)',
132
+ 'white-space:nowrap',
133
+ 'display:none',
134
+ 'pointer-events:none',
135
+ `left:${absLeft}px`,
136
+ `top:${absTop + h + 2}px`
137
+ ].join(';');
138
+
139
+ const setError = (show: boolean, msg?: string) => {
140
+ if (show && msg) {
141
+ tooltip.textContent = msg;
142
+ tooltip.style.display = 'block';
143
+ input.style.borderColor = DAISY_COLORS.error;
144
+ } else {
145
+ tooltip.textContent = '';
146
+ tooltip.style.display = 'none';
147
+ input.style.borderColor = DAISY_COLORS.primary;
148
+ }
149
+ };
150
+
151
+ input.addEventListener('input', () => {
152
+ const v = input.value;
153
+ if (v !== '' && !this.#isValid(v)) {
154
+ setError(true, this.#typeErrorHint(false));
155
+ } else {
156
+ setError(false);
157
+ }
158
+ });
159
+
160
+ input.addEventListener('keydown', e => {
161
+ if (e.key === 'Enter' || e.key === 'Tab') {
162
+ e.preventDefault();
163
+ e.stopPropagation();
164
+ if (this.#tryCommit(input.value)) {
165
+ this.#endEdit?.();
166
+ }
167
+ } else if (e.key === 'Escape') {
168
+ e.preventDefault();
169
+ e.stopPropagation();
170
+ this.#value = this.#originalValue;
171
+ this.#endEdit?.();
172
+ }
173
+ });
174
+
175
+ document.body.appendChild(input);
176
+ document.body.appendChild(tooltip);
177
+ this.#input = input;
178
+ this.#tooltip = tooltip;
179
+
180
+ this.#outsideHandler = (e: MouseEvent) => {
181
+ const target = e.target as Node;
182
+ if (target !== input && !tooltip.contains(target)) {
183
+ if (!this.#tryCommit(input.value)) {
184
+ this.#value = this.#originalValue;
185
+ }
186
+ this.#cleanup();
187
+ this.#endEdit?.();
188
+ }
189
+ };
190
+ scheduleEditorSetup(() => {
191
+ if (!this.#outsideHandler) return;
192
+ document.addEventListener('mousedown', this.#outsideHandler, { capture: true });
193
+ input.focus();
194
+ input.select();
195
+ });
196
+ }
197
+
198
+ /** 返回当前数值(bigint 模式下为 bigint),空值为 null */
199
+ getValue(): number | bigint | null {
200
+ return this.#value;
201
+ }
202
+
203
+ /** 移除输入框与提示浮层,解绑全局点击事件 */
204
+ onEnd(): void {
205
+ this.#cleanup();
206
+ }
207
+
208
+ /** 判断目标元素是否属于本编辑器(输入框或错误提示浮层) */
209
+ isEditorElement(el: HTMLElement | EventTarget): boolean {
210
+ const node = el as Node;
211
+ return node === this.#input || this.#tooltip?.contains(node) === true;
212
+ }
213
+
214
+ /** 按新的单元格矩形重新定位输入框与错误提示浮层 */
215
+ adjustPosition(rect: RectProps): void {
216
+ const input = this.#input;
217
+ const container = this.#container;
218
+ if (!input || !container) return;
219
+ const cRect = container.getBoundingClientRect();
220
+ const absLeft = cRect.left + rect.left;
221
+ const absTop = cRect.top + rect.top;
222
+ const h = Math.max(24, rect.height ?? 0);
223
+ input.style.left = `${absLeft}px`;
224
+ input.style.top = `${absTop}px`;
225
+ if (this.#tooltip) {
226
+ this.#tooltip.style.left = `${absLeft}px`;
227
+ this.#tooltip.style.top = `${absTop + h + 2}px`;
228
+ }
229
+ }
230
+
231
+ #isValid(v: string): boolean {
232
+ if (v.trim() === '') return true;
233
+ if (this.#isBigint) return INTEGER_RE.test(v.trim());
234
+ if (this.#isInteger) return INTEGER_RE.test(v.trim());
235
+ return !isNaN(Number(v.trim()));
236
+ }
237
+
238
+ #typeErrorHint(detailed: boolean): string {
239
+ if (this.#isBigint) return detailed ? '请输入 64 位整数(如 42、-7)' : '请输入 64 位整数';
240
+ if (this.#isInteger) return detailed ? '请输入整数(如 42、-7)' : '请输入整数';
241
+ return detailed ? '请输入有效数字(如 3.14、-2)' : '请输入有效数字';
242
+ }
243
+
244
+ #boundsError(v: number | bigint): string | null {
245
+ // bigint 模式不携带 format,没有值域约束
246
+ if (this.#isBigint) return null;
247
+ const n = v as number;
248
+ if (this.#min !== undefined && n < this.#min) return `必须不小于 ${this.#min}`;
249
+ if (this.#max !== undefined && n > this.#max) return `必须不大于 ${this.#max}`;
250
+ if (this.#step !== undefined && this.#min !== undefined) {
251
+ const steps = (n - this.#min) / this.#step;
252
+ if (Math.abs(steps - Math.round(steps)) >= STEP_TOLERANCE) return `必须按步长 ${this.#step} 取值`;
253
+ }
254
+ return null;
255
+ }
256
+
257
+ #tryCommit(v: string): boolean {
258
+ if (v.trim() === '') {
259
+ this.#value = null;
260
+ return true;
261
+ }
262
+ if (this.#isBigint) {
263
+ if (!INTEGER_RE.test(v.trim())) {
264
+ this.#showError(this.#typeErrorHint(true));
265
+ return false;
266
+ }
267
+ const value = BigInt(v.trim());
268
+ const boundsError = this.#boundsError(value);
269
+ if (boundsError) {
270
+ this.#showError(boundsError);
271
+ return false;
272
+ }
273
+ this.#value = value;
274
+ return true;
275
+ }
276
+ if (this.#isInteger) {
277
+ if (!INTEGER_RE.test(v.trim())) {
278
+ this.#showError(this.#typeErrorHint(true));
279
+ return false;
280
+ }
281
+ this.#value = parseInt(v.trim(), 10);
282
+ return this.#commitNumber(this.#value);
283
+ }
284
+ const n = Number(v.trim());
285
+ if (isNaN(n)) {
286
+ this.#showError(this.#typeErrorHint(true));
287
+ return false;
288
+ }
289
+ this.#value = n;
290
+ return this.#commitNumber(n);
291
+ }
292
+
293
+ #commitNumber(n: number): boolean {
294
+ const boundsError = this.#boundsError(n);
295
+ if (boundsError) {
296
+ this.#showError(boundsError);
297
+ return false;
298
+ }
299
+ return true;
300
+ }
301
+
302
+ #showError(message: string): void {
303
+ const tooltip = this.#tooltip;
304
+ const input = this.#input;
305
+ if (!tooltip || !input) return;
306
+ tooltip.textContent = message;
307
+ tooltip.style.display = 'block';
308
+ input.style.borderColor = DAISY_COLORS.error;
309
+ }
310
+
311
+ #cleanup(): void {
312
+ if (this.#outsideHandler) {
313
+ document.removeEventListener('mousedown', this.#outsideHandler, { capture: true });
314
+ this.#outsideHandler = null;
315
+ }
316
+ this.#input?.remove();
317
+ this.#input = null;
318
+ this.#tooltip?.remove();
319
+ this.#tooltip = null;
320
+ this.#container = null;
321
+ }
322
+ }