@aiao/rxdb-model 0.0.18 → 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.
- package/LICENSE +1 -1
- package/README.md +43 -6
- package/dist/cn.d.ts +19 -0
- package/dist/cn.d.ts.map +1 -0
- package/dist/entity-capabilities.d.ts +31 -0
- package/dist/entity-capabilities.d.ts.map +1 -0
- package/dist/entity-detail/detail-tabs.d.ts +13 -2
- package/dist/entity-detail/detail-tabs.d.ts.map +1 -1
- package/dist/entity-detail/index.d.ts +5 -0
- package/dist/entity-detail/index.d.ts.map +1 -1
- package/dist/entity-detail/interfaces.d.ts +29 -1
- package/dist/entity-detail/interfaces.d.ts.map +1 -1
- package/dist/entity-field.utils.d.ts +60 -2
- package/dist/entity-field.utils.d.ts.map +1 -1
- package/dist/entity-form/form-data.d.ts +22 -1
- package/dist/entity-form/form-data.d.ts.map +1 -1
- package/dist/entity-form/form-fields.d.ts +25 -2
- package/dist/entity-form/form-fields.d.ts.map +1 -1
- package/dist/entity-form/form-validation.d.ts +20 -2
- package/dist/entity-form/form-validation.d.ts.map +1 -1
- package/dist/entity-form/index.d.ts +5 -0
- package/dist/entity-form/index.d.ts.map +1 -1
- package/dist/entity-form/interfaces.d.ts +65 -1
- package/dist/entity-form/interfaces.d.ts.map +1 -1
- package/dist/entity-list/manual-order-list.d.ts +99 -0
- package/dist/entity-list/manual-order-list.d.ts.map +1 -0
- package/dist/entity-table/columns/build-editable-columns.d.ts +26 -4
- package/dist/entity-table/columns/build-editable-columns.d.ts.map +1 -1
- package/dist/entity-table/columns/column-utils.d.ts +78 -12
- package/dist/entity-table/columns/column-utils.d.ts.map +1 -1
- package/dist/entity-table/editors/color-editor.d.ts +23 -0
- package/dist/entity-table/editors/color-editor.d.ts.map +1 -0
- package/dist/entity-table/editors/date-editor.d.ts +6 -1
- package/dist/entity-table/editors/date-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/enum-editor.d.ts +11 -1
- package/dist/entity-table/editors/enum-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/global-overlay-editor.d.ts +27 -4
- package/dist/entity-table/editors/global-overlay-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/icon-list-editor.d.ts +13 -1
- package/dist/entity-table/editors/icon-list-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/json-editor.d.ts +6 -1
- package/dist/entity-table/editors/json-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/key-value-editor.d.ts +12 -2
- package/dist/entity-table/editors/key-value-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/lucide-svg.d.ts +3 -2
- package/dist/entity-table/editors/lucide-svg.d.ts.map +1 -1
- package/dist/entity-table/editors/multiselect-editor.d.ts +24 -6
- package/dist/entity-table/editors/multiselect-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/number-editor.d.ts +33 -3
- package/dist/entity-table/editors/number-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/relation-editor.d.ts +14 -2
- package/dist/entity-table/editors/relation-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/safe-list-editor.d.ts +1 -0
- package/dist/entity-table/editors/safe-list-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/tags-editor.d.ts +12 -1
- package/dist/entity-table/editors/tags-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/text-format-editor.d.ts +32 -0
- package/dist/entity-table/editors/text-format-editor.d.ts.map +1 -0
- package/dist/entity-table/editors/uuid-editor.d.ts +14 -1
- package/dist/entity-table/editors/uuid-editor.d.ts.map +1 -1
- package/dist/entity-table/index.d.ts +13 -3
- package/dist/entity-table/index.d.ts.map +1 -1
- package/dist/entity-table/interfaces.d.ts +13 -0
- package/dist/entity-table/interfaces.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-clipboard.d.ts +31 -2
- package/dist/entity-table/vtable/table-clipboard.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-factory.d.ts +25 -5
- package/dist/entity-table/vtable/table-factory.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-keyboard.d.ts +18 -1
- package/dist/entity-table/vtable/table-keyboard.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-operations.d.ts +44 -3
- package/dist/entity-table/vtable/table-operations.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-row-move.d.ts +88 -0
- package/dist/entity-table/vtable/table-row-move.d.ts.map +1 -0
- package/dist/entity-table/vtable/table-theme.d.ts +47 -2
- package/dist/entity-table/vtable/table-theme.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-tooltip.d.ts +7 -1
- package/dist/entity-table/vtable/table-tooltip.d.ts.map +1 -1
- package/dist/entity-table/vtable/vtable-compat.d.ts +33 -5
- package/dist/entity-table/vtable/vtable-compat.d.ts.map +1 -1
- package/dist/entity-value.utils.d.ts +50 -2
- package/dist/entity-value.utils.d.ts.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4392 -64471
- package/dist/query-builder/models/config.interface.d.ts +2 -2
- package/dist/query-builder/models/operator.interface.d.ts +6 -2
- package/dist/query-builder/models/operator.interface.d.ts.map +1 -1
- package/dist/query-builder/models/query-builder-state.d.ts +31 -2
- package/dist/query-builder/models/query-builder-state.d.ts.map +1 -1
- package/dist/query-builder/models/validation.interface.d.ts +2 -2
- package/dist/query-builder/services/operator-registry.service.d.ts +13 -3
- package/dist/query-builder/services/operator-registry.service.d.ts.map +1 -1
- package/dist/query-builder/services/query-builder.service.d.ts +16 -6
- package/dist/query-builder/services/query-builder.service.d.ts.map +1 -1
- package/dist/query-builder/services/query-converter.service.d.ts +10 -2
- package/dist/query-builder/services/query-converter.service.d.ts.map +1 -1
- package/dist/query-builder/services/schema-parser.service.d.ts +10 -2
- package/dist/query-builder/services/schema-parser.service.d.ts.map +1 -1
- package/dist/query-builder/services/validation.service.d.ts +34 -4
- package/dist/query-builder/services/validation.service.d.ts.map +1 -1
- package/dist/query-builder/utils/date.d.ts +12 -0
- package/dist/query-builder/utils/date.d.ts.map +1 -1
- package/dist/query-builder/utils/field-tree.d.ts +7 -1
- package/dist/query-builder/utils/field-tree.d.ts.map +1 -1
- package/dist/query-builder/utils/id.d.ts +2 -0
- package/dist/query-builder/utils/id.d.ts.map +1 -1
- package/dist/query-builder/utils/metadata-field-extractor.d.ts +15 -3
- package/dist/query-builder/utils/metadata-field-extractor.d.ts.map +1 -1
- package/dist/query-builder/utils/popover-position.d.ts +11 -0
- package/dist/query-builder/utils/popover-position.d.ts.map +1 -1
- package/dist/query-builder/utils/schema-factory.d.ts +7 -3
- package/dist/query-builder/utils/structural-equal.d.ts +4 -0
- package/dist/query-builder/utils/structural-equal.d.ts.map +1 -1
- package/dist/query-builder/utils/value-input.d.ts +21 -1
- package/dist/query-builder/utils/value-input.d.ts.map +1 -1
- package/dist/structural-equal.d.ts +5 -1
- package/dist/structural-equal.d.ts.map +1 -1
- package/package.json +34 -10
- package/src/cn.ts +21 -0
- package/src/entity-capabilities.ts +37 -0
- package/src/entity-detail/detail-tabs.ts +48 -0
- package/src/entity-detail/index.ts +7 -0
- package/src/entity-detail/interfaces.ts +55 -0
- package/src/entity-field.utils.ts +205 -0
- package/src/entity-form/form-data.ts +83 -0
- package/src/entity-form/form-fields.ts +48 -0
- package/src/entity-form/form-validation.ts +34 -0
- package/src/entity-form/index.ts +9 -0
- package/src/entity-form/interfaces.ts +116 -0
- package/src/entity-list/manual-order-list.ts +159 -0
- package/src/entity-table/columns/build-editable-columns.ts +184 -0
- package/src/entity-table/columns/column-utils.ts +657 -0
- package/src/entity-table/editors/color-editor.ts +239 -0
- package/src/entity-table/editors/date-editor.ts +169 -0
- package/src/entity-table/editors/enum-editor.ts +167 -0
- package/src/entity-table/editors/global-overlay-editor.ts +277 -0
- package/src/entity-table/editors/icon-list-editor.ts +130 -0
- package/src/entity-table/editors/json-editor.ts +212 -0
- package/src/entity-table/editors/key-value-editor.ts +595 -0
- package/src/entity-table/editors/lucide-svg.ts +78 -0
- package/src/entity-table/editors/multiselect-editor.ts +171 -0
- package/src/entity-table/editors/number-editor.ts +322 -0
- package/src/entity-table/editors/relation-editor.ts +316 -0
- package/src/entity-table/editors/safe-list-editor.ts +17 -0
- package/src/entity-table/editors/tags-editor.ts +239 -0
- package/src/entity-table/editors/text-format-editor.ts +220 -0
- package/src/entity-table/editors/uuid-editor.ts +198 -0
- package/src/entity-table/index.ts +96 -0
- package/src/entity-table/interfaces.ts +72 -0
- package/src/entity-table/vtable/table-clipboard.ts +419 -0
- package/src/entity-table/vtable/table-factory.ts +117 -0
- package/src/entity-table/vtable/table-keyboard.ts +89 -0
- package/src/entity-table/vtable/table-operations.ts +123 -0
- package/src/entity-table/vtable/table-row-move.ts +122 -0
- package/src/entity-table/vtable/table-theme.ts +181 -0
- package/src/entity-table/vtable/table-tooltip.ts +70 -0
- package/src/entity-table/vtable/vtable-compat.ts +84 -0
- package/src/entity-value.utils.ts +456 -0
- package/src/index.ts +57 -0
- package/src/query-builder/models/config.interface.ts +31 -0
- package/src/query-builder/models/index.ts +8 -0
- package/src/query-builder/models/operator.interface.ts +191 -0
- package/src/query-builder/models/query-builder-state.ts +222 -0
- package/src/query-builder/models/validation.interface.ts +184 -0
- package/src/query-builder/services/index.ts +9 -0
- package/src/query-builder/services/operator-registry.service.ts +243 -0
- package/src/query-builder/services/query-builder.service.ts +568 -0
- package/src/query-builder/services/query-converter.service.ts +153 -0
- package/src/query-builder/services/schema-parser.service.ts +309 -0
- package/src/query-builder/services/validation.service.ts +390 -0
- package/src/query-builder/utils/date.ts +35 -0
- package/src/query-builder/utils/field-tree.ts +400 -0
- package/src/query-builder/utils/id.ts +15 -0
- package/src/query-builder/utils/index.ts +12 -0
- package/src/query-builder/utils/metadata-field-extractor.ts +460 -0
- package/src/query-builder/utils/popover-position.ts +59 -0
- package/src/query-builder/utils/schema-factory.ts +75 -0
- package/src/query-builder/utils/structural-equal.ts +5 -0
- package/src/query-builder/utils/value-input.ts +105 -0
- package/src/structural-equal.ts +49 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import type * as VTable from '@visactor/vtable';
|
|
2
|
+
import { UUID_RE } from '../../entity-value.utils.js';
|
|
3
|
+
import type { EntityTableRecord, PendingWrite } from '../interfaces.js';
|
|
4
|
+
|
|
5
|
+
/** 布尔类型列 */
|
|
6
|
+
const BOOL_CELL_TYPES = new Set(['switch', 'checkbox', 'radio']);
|
|
7
|
+
|
|
8
|
+
/** 验证失败哨值 — 区分「无效值应跳过」与「合法的 null/空 」 */
|
|
9
|
+
const INVALID = Symbol('invalid');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 针对 propertyType 验证并转换来自系统剪贴板的字符串值。
|
|
13
|
+
* - 返回 INVALID 表示验证不通过,应跳过该单元格。
|
|
14
|
+
* - 返回其他值(包括 null)表示写入该值(空字符串/null 表示清空)。
|
|
15
|
+
*/
|
|
16
|
+
function parseSystemValue(value: string, propertyType: string | undefined): unknown {
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
switch (propertyType) {
|
|
19
|
+
case 'uuid':
|
|
20
|
+
if (trimmed === '') return '';
|
|
21
|
+
return UUID_RE.test(trimmed) ? trimmed : INVALID;
|
|
22
|
+
case 'number': {
|
|
23
|
+
if (trimmed === '') return null;
|
|
24
|
+
const n = Number(trimmed);
|
|
25
|
+
return isNaN(n) ? INVALID : n;
|
|
26
|
+
}
|
|
27
|
+
case 'integer': {
|
|
28
|
+
if (trimmed === '') return null;
|
|
29
|
+
if (!/^-?\d+$/.test(trimmed)) return INVALID;
|
|
30
|
+
return parseInt(trimmed, 10);
|
|
31
|
+
}
|
|
32
|
+
case 'bigint': {
|
|
33
|
+
if (trimmed === '') return null;
|
|
34
|
+
if (!/^-?\d+$/.test(trimmed)) return INVALID;
|
|
35
|
+
return BigInt(trimmed);
|
|
36
|
+
}
|
|
37
|
+
case 'binary': {
|
|
38
|
+
if (trimmed === '') return null;
|
|
39
|
+
if (trimmed.length % 2 !== 0 || !/^(?:[0-9a-fA-F]{2})+$/.test(trimmed)) return INVALID;
|
|
40
|
+
const bytes = new Uint8Array(trimmed.length / 2);
|
|
41
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
42
|
+
bytes[i] = parseInt(trimmed.slice(i * 2, i * 2 + 2), 16);
|
|
43
|
+
}
|
|
44
|
+
return bytes;
|
|
45
|
+
}
|
|
46
|
+
case 'date': {
|
|
47
|
+
if (trimmed === '') return null;
|
|
48
|
+
const d = new Date(trimmed);
|
|
49
|
+
return isNaN(d.getTime()) ? INVALID : value;
|
|
50
|
+
}
|
|
51
|
+
case 'json': {
|
|
52
|
+
if (trimmed === '') return null;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(trimmed);
|
|
55
|
+
} catch {
|
|
56
|
+
return INVALID;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
case 'keyValue': {
|
|
60
|
+
if (trimmed === '') return null;
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(trimmed);
|
|
63
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) return INVALID;
|
|
64
|
+
return parsed;
|
|
65
|
+
} catch {
|
|
66
|
+
return INVALID;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
case 'enum':
|
|
70
|
+
// isAllowedByMenuList 已在外层过滤不合法枚举值,此处仅确保返回字符串
|
|
71
|
+
return trimmed;
|
|
72
|
+
case 'stringArray': {
|
|
73
|
+
if (trimmed === '') return null;
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(trimmed);
|
|
76
|
+
if (!Array.isArray(parsed) || parsed.some(el => typeof el !== 'string')) return INVALID;
|
|
77
|
+
return parsed;
|
|
78
|
+
} catch {
|
|
79
|
+
return INVALID;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
case 'numberArray': {
|
|
83
|
+
if (trimmed === '') return null;
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(trimmed);
|
|
86
|
+
if (!Array.isArray(parsed) || parsed.some(el => typeof el !== 'number' || isNaN(el))) return INVALID;
|
|
87
|
+
return parsed;
|
|
88
|
+
} catch {
|
|
89
|
+
return INVALID;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 当列定义包含 menuList 时,检查值是否在允许的枚举选项中。
|
|
99
|
+
* - strict=false(默认):无 menuList 时返回 true(不限制)。
|
|
100
|
+
* - strict=true(enum 列):无 menuList 时返回 false(必须有 menuList 才能验证)。
|
|
101
|
+
*/
|
|
102
|
+
function isAllowedByMenuList(colDef: Record<string, unknown> | undefined, value: string, strict = false): boolean {
|
|
103
|
+
const menuList = colDef?.['menuList'] as Array<{ value: unknown } | string> | undefined;
|
|
104
|
+
if (!menuList?.length) return !strict;
|
|
105
|
+
return menuList.some(m => String(typeof m === 'object' ? (m.value ?? '') : m) === value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 文本 → 布尔值转换(仅已知值,未知则返回 undefined 表示跳过) */
|
|
109
|
+
const TRUTHY_TEXT = new Set(['true', '1', 'yes', '是']);
|
|
110
|
+
const FALSY_TEXT = new Set(['false', '0', 'no', '否', '']);
|
|
111
|
+
function textToBoolean(text: string): boolean | undefined {
|
|
112
|
+
const t = text.trim().toLowerCase();
|
|
113
|
+
if (TRUTHY_TEXT.has(t)) return true;
|
|
114
|
+
if (FALSY_TEXT.has(t)) return false;
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 单个单元格的剪贴板快照 */
|
|
119
|
+
export interface ClipboardCell {
|
|
120
|
+
/** 列索引 */
|
|
121
|
+
col: number;
|
|
122
|
+
/** 字段名 */
|
|
123
|
+
field: string;
|
|
124
|
+
/** 单元格值 */
|
|
125
|
+
value: unknown;
|
|
126
|
+
/** 单元格类型 */
|
|
127
|
+
cellType: string;
|
|
128
|
+
/** 属性类型 */
|
|
129
|
+
propertyType?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 剪贴板内容:行优先二维数组 `[rowOffset][colOffset]` */
|
|
133
|
+
export type ClipboardContent = ClipboardCell[][];
|
|
134
|
+
|
|
135
|
+
/** 从 VTable 选区提取去重排序后的目标行/列,排除表头 */
|
|
136
|
+
function getDestRowsCols(table: VTable.ListTable): { destRows: number[]; destCols: number[] } | null {
|
|
137
|
+
const cellInfos = table.getSelectedCellInfos() as VTable.TYPES.CellInfo[][] | null;
|
|
138
|
+
if (!cellInfos?.length) return null;
|
|
139
|
+
const flat = cellInfos.flat().filter(c => c.row > 0);
|
|
140
|
+
if (!flat.length) return null;
|
|
141
|
+
return {
|
|
142
|
+
destRows: [...new Set(flat.map(c => c.row))].sort((a, b) => a - b),
|
|
143
|
+
destCols: [...new Set(flat.map(c => c.col))].sort((a, b) => a - b)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* PropertyType 兼容分组 — 同组之间允许粘贴,跨组则拒绝。
|
|
149
|
+
* 未定义的类型视为 'text'。
|
|
150
|
+
*/
|
|
151
|
+
const PROPERTY_TYPE_GROUP: Record<string, string> = {
|
|
152
|
+
string: 'text',
|
|
153
|
+
uuid: 'text',
|
|
154
|
+
enum: 'enum',
|
|
155
|
+
number: 'number',
|
|
156
|
+
integer: 'number',
|
|
157
|
+
bigint: 'number',
|
|
158
|
+
boolean: 'boolean',
|
|
159
|
+
date: 'date',
|
|
160
|
+
binary: 'binary',
|
|
161
|
+
stringArray: 'stringArray',
|
|
162
|
+
numberArray: 'numberArray',
|
|
163
|
+
json: 'object',
|
|
164
|
+
keyValue: 'object'
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** 取 propertyType 的兼容分组,未定义类型返回空字符串 */
|
|
168
|
+
function getPropertyTypeGroup(type: string | undefined): string {
|
|
169
|
+
return type ? (PROPERTY_TYPE_GROUP[type] ?? 'text') : '';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 将当前 VTable 选区快照为剪贴板内容。
|
|
174
|
+
*
|
|
175
|
+
* 跳过:表头行、只读行、新增行、元字段列(`_` 前缀)。
|
|
176
|
+
*
|
|
177
|
+
* @param table 目标表格实例
|
|
178
|
+
* @param columns 列定义列表(可选),用于读取列属性类型
|
|
179
|
+
* @returns 剪贴板快照;无可复制单元格时返回 null
|
|
180
|
+
*/
|
|
181
|
+
export function snapshotSelection(
|
|
182
|
+
table: VTable.ListTable,
|
|
183
|
+
columns?: readonly Record<string, unknown>[]
|
|
184
|
+
): ClipboardContent | null {
|
|
185
|
+
const cellInfos = table.getSelectedCellInfos() as VTable.TYPES.CellInfo[][] | null;
|
|
186
|
+
if (!cellInfos?.length) return null;
|
|
187
|
+
|
|
188
|
+
const result: ClipboardContent = [];
|
|
189
|
+
|
|
190
|
+
for (const rowCells of cellInfos) {
|
|
191
|
+
const clipRow: ClipboardCell[] = [];
|
|
192
|
+
|
|
193
|
+
for (const cell of rowCells) {
|
|
194
|
+
if (cell.row === 0) continue;
|
|
195
|
+
const field = cell.field == null ? '' : String(cell.field);
|
|
196
|
+
if (!field || field.startsWith('_')) continue;
|
|
197
|
+
|
|
198
|
+
const record = table.getRecordByCell(cell.col, cell.row) as EntityTableRecord | undefined;
|
|
199
|
+
if (!record || record._readonly || record._isAddRow) continue;
|
|
200
|
+
|
|
201
|
+
clipRow.push({
|
|
202
|
+
col: cell.col,
|
|
203
|
+
field,
|
|
204
|
+
value: (record as Record<string, unknown>)[field],
|
|
205
|
+
cellType: String(cell.cellType ?? 'text'),
|
|
206
|
+
propertyType: (columns?.[cell.col - 1]?.['_propertyType'] as string) ?? undefined
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (clipRow.length) result.push(clipRow);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result.length ? result : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 将 ClipboardContent 序列化为 TSV 字符串
|
|
218
|
+
*
|
|
219
|
+
* @param clipboard 内部剪贴板内容
|
|
220
|
+
* @returns TSV 格式文本,null/undefined 值序列化为空字符串
|
|
221
|
+
*/
|
|
222
|
+
export function formatAsTsv(clipboard: ClipboardContent): string {
|
|
223
|
+
return clipboard.map(row => row.map(cell => String(cell.value ?? '')).join('\t')).join('\n');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 从内部剪贴板收集待写入操作(按列位置匹配,类 Excel 行为)。
|
|
228
|
+
* cellType 必须一致;行/列数不足时循环复用。
|
|
229
|
+
* 若目标列定义了 menuList,则跳过不在枚举范围内的值。
|
|
230
|
+
*
|
|
231
|
+
* @param table 目标表格实例
|
|
232
|
+
* @param clipboard 内部剪贴板内容
|
|
233
|
+
* @param columns 列定义列表(可选),用于读取列属性类型与枚举选项
|
|
234
|
+
* @returns 待写入操作列表
|
|
235
|
+
*/
|
|
236
|
+
export function applyClipboard(
|
|
237
|
+
table: VTable.ListTable,
|
|
238
|
+
clipboard: ClipboardContent,
|
|
239
|
+
columns?: readonly Record<string, unknown>[]
|
|
240
|
+
): PendingWrite[] {
|
|
241
|
+
const sel = getDestRowsCols(table);
|
|
242
|
+
if (!sel) return [];
|
|
243
|
+
const { destRows, destCols } = sel;
|
|
244
|
+
|
|
245
|
+
const clipRowCount = clipboard.length;
|
|
246
|
+
const clipColCount = clipboard[0]?.length ?? 0;
|
|
247
|
+
if (!clipColCount) return [];
|
|
248
|
+
|
|
249
|
+
const pending: PendingWrite[] = [];
|
|
250
|
+
for (let rowI = 0; rowI < destRows.length; rowI++) {
|
|
251
|
+
const targetRow = destRows[rowI];
|
|
252
|
+
const clipRow = clipboard[rowI % clipRowCount];
|
|
253
|
+
|
|
254
|
+
const targetRecord = table.getRecordByCell(destCols[0], targetRow) as EntityTableRecord | undefined;
|
|
255
|
+
if (!targetRecord || targetRecord._readonly || targetRecord._isAddRow) continue;
|
|
256
|
+
|
|
257
|
+
for (let colI = 0; colI < destCols.length; colI++) {
|
|
258
|
+
const targetCol = destCols[colI];
|
|
259
|
+
const clipCell = clipRow[colI % clipColCount];
|
|
260
|
+
|
|
261
|
+
const targetCellInfo = table.getCellInfo(targetCol, targetRow) as { cellType?: unknown; field?: unknown };
|
|
262
|
+
if (String(targetCellInfo.cellType ?? 'text') !== clipCell.cellType) continue;
|
|
263
|
+
|
|
264
|
+
const colDef = columns?.[targetCol - 1];
|
|
265
|
+
const srcGroup = getPropertyTypeGroup(clipCell.propertyType);
|
|
266
|
+
const dstGroup = getPropertyTypeGroup(colDef?.['_propertyType'] as string | undefined);
|
|
267
|
+
if (srcGroup && dstGroup && srcGroup !== dstGroup) continue;
|
|
268
|
+
|
|
269
|
+
if (!isAllowedByMenuList(colDef, String(clipCell.value ?? ''))) continue;
|
|
270
|
+
|
|
271
|
+
const field = String(targetCellInfo.field ?? '');
|
|
272
|
+
pending.push({ col: targetCol, row: targetRow, value: clipCell.value, field, record: targetRecord });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return pending;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 从系统剪贴板文本(TSV)收集待写入操作。
|
|
280
|
+
* 布尔列自动将文本转为布尔值(true/1/yes/是 → true,false/0/no/否/空 → false),
|
|
281
|
+
* 无法识别的值跳过;枚举列跳过不在 menuList 中的值;行/列数不足时循环复用。
|
|
282
|
+
*
|
|
283
|
+
* @param table 目标表格实例
|
|
284
|
+
* @param text 系统剪贴板文本(TSV 格式,按 \n 分行、\t 分列)
|
|
285
|
+
* @param columns 列定义列表(可选),用于读取列属性类型与枚举选项
|
|
286
|
+
* @returns 待写入操作列表
|
|
287
|
+
*/
|
|
288
|
+
export function applySystemText(
|
|
289
|
+
table: VTable.ListTable,
|
|
290
|
+
text: string,
|
|
291
|
+
columns?: readonly Record<string, unknown>[]
|
|
292
|
+
): PendingWrite[] {
|
|
293
|
+
const rows = text.split('\n').map(r => r.split('\t').map(v => v.trimEnd()));
|
|
294
|
+
const sel = getDestRowsCols(table);
|
|
295
|
+
if (!sel) return [];
|
|
296
|
+
const { destRows, destCols } = sel;
|
|
297
|
+
|
|
298
|
+
const clipRowCount = rows.length;
|
|
299
|
+
const clipColCount = rows[0]?.length ?? 0;
|
|
300
|
+
if (!clipColCount) return [];
|
|
301
|
+
|
|
302
|
+
const pending: PendingWrite[] = [];
|
|
303
|
+
for (let rowI = 0; rowI < destRows.length; rowI++) {
|
|
304
|
+
const targetRow = destRows[rowI];
|
|
305
|
+
const clipRow = rows[rowI % clipRowCount];
|
|
306
|
+
|
|
307
|
+
const targetRecord = table.getRecordByCell(destCols[0], targetRow) as EntityTableRecord | undefined;
|
|
308
|
+
if (!targetRecord || targetRecord._readonly || targetRecord._isAddRow) continue;
|
|
309
|
+
|
|
310
|
+
for (let colI = 0; colI < destCols.length; colI++) {
|
|
311
|
+
const targetCol = destCols[colI];
|
|
312
|
+
const value = clipRow[colI % clipColCount] ?? '';
|
|
313
|
+
|
|
314
|
+
const targetCellInfo = table.getCellInfo(targetCol, targetRow) as { cellType?: unknown; field?: unknown };
|
|
315
|
+
const targetCellType = String(targetCellInfo.cellType ?? 'text');
|
|
316
|
+
const field = String(targetCellInfo.field ?? '');
|
|
317
|
+
|
|
318
|
+
if (BOOL_CELL_TYPES.has(targetCellType)) {
|
|
319
|
+
const boolValue = textToBoolean(value);
|
|
320
|
+
if (boolValue === undefined) continue;
|
|
321
|
+
pending.push({ col: targetCol, row: targetRow, value: boolValue, field, record: targetRecord });
|
|
322
|
+
} else {
|
|
323
|
+
const colDef = columns?.[targetCol - 1];
|
|
324
|
+
const propertyType = colDef?.['_propertyType'] as string | undefined;
|
|
325
|
+
// enum 列必须有 menuList 且值在其中才允许写入(strict=true)
|
|
326
|
+
if (!isAllowedByMenuList(colDef, value, propertyType === 'enum')) continue;
|
|
327
|
+
const converted = parseSystemValue(value, propertyType);
|
|
328
|
+
if (converted === INVALID) continue;
|
|
329
|
+
pending.push({ col: targetCol, row: targetRow, value: converted, field, record: targetRecord });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return pending;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 从选区收集 Delete/Backspace 需要清空的单元格写入列表。
|
|
338
|
+
*
|
|
339
|
+
* @param table 目标表格实例
|
|
340
|
+
* @param columns 列定义列表
|
|
341
|
+
* @param nonClearableFields 禁止清空的字段名集合
|
|
342
|
+
* @param cellClearable 业务层回调判断单元格是否可清空,返回 false 跳过该单元格
|
|
343
|
+
* @returns writes — 需要写入的清空操作;boolCells — 需要刷新视觉状态的布尔列坐标
|
|
344
|
+
*/
|
|
345
|
+
export function collectDeleteWrites(
|
|
346
|
+
table: VTable.ListTable,
|
|
347
|
+
columns: readonly { field?: string; cellType?: string | ((...args: unknown[]) => string) }[],
|
|
348
|
+
nonClearableFields: ReadonlySet<string>,
|
|
349
|
+
cellClearable?: (record: Record<string, unknown>, field: string) => boolean
|
|
350
|
+
): { writes: PendingWrite[]; boolCells: { col: number; row: number }[] } {
|
|
351
|
+
const cellInfos = table.getSelectedCellInfos() as { col: number; row: number; field?: unknown }[][] | null;
|
|
352
|
+
if (!cellInfos?.length) return { writes: [], boolCells: [] };
|
|
353
|
+
|
|
354
|
+
const writes: PendingWrite[] = [];
|
|
355
|
+
const boolCells: { col: number; row: number }[] = [];
|
|
356
|
+
|
|
357
|
+
for (const rowCells of cellInfos) {
|
|
358
|
+
for (const cell of rowCells) {
|
|
359
|
+
if (cell.row === 0) continue;
|
|
360
|
+
const record = table.getRecordByCell(cell.col, cell.row) as Record<string, unknown> | undefined;
|
|
361
|
+
if (!record || record['_readonly'] || record['_isAddRow']) continue;
|
|
362
|
+
const colDef = columns[cell.col - 1];
|
|
363
|
+
const field = colDef?.field;
|
|
364
|
+
if (!field || field.startsWith('_')) continue;
|
|
365
|
+
if (nonClearableFields.has(field)) continue;
|
|
366
|
+
if (cellClearable && !cellClearable(record, field)) continue;
|
|
367
|
+
|
|
368
|
+
const isBool = BOOL_CELL_TYPES.has(typeof colDef.cellType === 'string' ? colDef.cellType : '');
|
|
369
|
+
const clearValue = isBool ? false : '';
|
|
370
|
+
const currentValue = record[field];
|
|
371
|
+
const alreadyClear = isBool ? !currentValue : currentValue === '' || currentValue == null;
|
|
372
|
+
if (alreadyClear) {
|
|
373
|
+
if (isBool) boolCells.push({ col: cell.col, row: cell.row });
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
writes.push({ col: cell.col, row: cell.row, value: clearValue, field, record: record as EntityTableRecord });
|
|
377
|
+
if (isBool) boolCells.push({ col: cell.col, row: cell.row });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { writes, boolCells };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** 内部剪贴板管理器 — 封装系统剪贴板 API 交互 + 内部备份 */
|
|
385
|
+
export class TableClipboardManager {
|
|
386
|
+
#content: ClipboardContent | null = null;
|
|
387
|
+
#lastWrittenTsv = '';
|
|
388
|
+
|
|
389
|
+
/** 复制当前选区到系统剪贴板,并写入内部备份 */
|
|
390
|
+
async copy(table: VTable.ListTable, columns?: readonly Record<string, unknown>[]): Promise<void> {
|
|
391
|
+
const content = snapshotSelection(table, columns);
|
|
392
|
+
if (!content) return;
|
|
393
|
+
this.#content = content;
|
|
394
|
+
const tsv = formatAsTsv(content);
|
|
395
|
+
this.#lastWrittenTsv = tsv;
|
|
396
|
+
try {
|
|
397
|
+
await navigator.clipboard.writeText(tsv);
|
|
398
|
+
} catch {
|
|
399
|
+
// fallback: internal clipboard still works
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** 粘贴:系统剪贴板文本与上次写入不同时按文本解析,否则退回内部备份按列匹配写入 */
|
|
404
|
+
async paste(table: VTable.ListTable, columns?: readonly Record<string, unknown>[]): Promise<PendingWrite[]> {
|
|
405
|
+
let systemText = '';
|
|
406
|
+
try {
|
|
407
|
+
systemText = await navigator.clipboard.readText();
|
|
408
|
+
} catch {
|
|
409
|
+
// fallback
|
|
410
|
+
}
|
|
411
|
+
if (systemText && systemText !== this.#lastWrittenTsv) {
|
|
412
|
+
return applySystemText(table, systemText, columns);
|
|
413
|
+
}
|
|
414
|
+
if (this.#content) {
|
|
415
|
+
return applyClipboard(table, this.#content, columns);
|
|
416
|
+
}
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ListTableConstructorOptions } from '@visactor/vtable';
|
|
2
|
+
import * as VTable from '@visactor/vtable';
|
|
3
|
+
import { InputEditor, TextAreaEditor } from '@visactor/vtable-editors';
|
|
4
|
+
import { ColorEditor } from '../editors/color-editor.js';
|
|
5
|
+
import { DateEditor } from '../editors/date-editor.js';
|
|
6
|
+
import { EnumEditor } from '../editors/enum-editor.js';
|
|
7
|
+
import { JsonEditor } from '../editors/json-editor.js';
|
|
8
|
+
import { KeyValueEditor } from '../editors/key-value-editor.js';
|
|
9
|
+
import { MultiSelectEditor } from '../editors/multiselect-editor.js';
|
|
10
|
+
import { NumberEditor } from '../editors/number-editor.js';
|
|
11
|
+
import { RelationEditor } from '../editors/relation-editor.js';
|
|
12
|
+
import { SafeListEditor } from '../editors/safe-list-editor.js';
|
|
13
|
+
import { TagsEditor } from '../editors/tags-editor.js';
|
|
14
|
+
import { TextFormatEditor } from '../editors/text-format-editor.js';
|
|
15
|
+
import { UuidEditor } from '../editors/uuid-editor.js';
|
|
16
|
+
import type { EntityTableRecord } from '../interfaces.js';
|
|
17
|
+
import { createTheme, getCSSVariables } from './table-theme.js';
|
|
18
|
+
|
|
19
|
+
/** 基础编辑器注册标记(全局仅注册一次) */
|
|
20
|
+
let baseEditorRegistered = false;
|
|
21
|
+
|
|
22
|
+
/** 注册 VTable 基础编辑器(全局单例,重复调用仅注册一次) */
|
|
23
|
+
export function ensureBaseEditorRegistered(): void {
|
|
24
|
+
if (baseEditorRegistered) return;
|
|
25
|
+
baseEditorRegistered = true;
|
|
26
|
+
VTable.register.editor('input-editor', new InputEditor());
|
|
27
|
+
VTable.register.editor('list-editor', new SafeListEditor({ values: [] }));
|
|
28
|
+
VTable.register.editor('enum-editor', new EnumEditor());
|
|
29
|
+
VTable.register.editor('date-editor', new DateEditor());
|
|
30
|
+
VTable.register.editor('tags-editor', new TagsEditor('string'));
|
|
31
|
+
VTable.register.editor('number-tags-editor', new TagsEditor('number'));
|
|
32
|
+
VTable.register.editor('json-editor', new JsonEditor());
|
|
33
|
+
VTable.register.editor('key-value-editor', new KeyValueEditor());
|
|
34
|
+
VTable.register.editor('number-editor', new NumberEditor(false));
|
|
35
|
+
VTable.register.editor('integer-editor', new NumberEditor(true));
|
|
36
|
+
VTable.register.editor('bigint-editor', new NumberEditor(false, { bigint: true }));
|
|
37
|
+
VTable.register.editor('uuid-editor', new UuidEditor());
|
|
38
|
+
VTable.register.editor('relation-editor', new RelationEditor());
|
|
39
|
+
VTable.register.editor('color-editor', new ColorEditor());
|
|
40
|
+
VTable.register.editor('text-format-editor', new TextFormatEditor('url'));
|
|
41
|
+
VTable.register.editor('multiselect-editor', new MultiSelectEditor([]));
|
|
42
|
+
VTable.register.editor('vtable-textarea-editor', new TextAreaEditor());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 注册自定义编辑器
|
|
47
|
+
*
|
|
48
|
+
* 业务层调用此函数注册领域相关编辑器(如属性类型选择器)。
|
|
49
|
+
*
|
|
50
|
+
* @param name 编辑器名称
|
|
51
|
+
* @param editor 编辑器实例
|
|
52
|
+
*/
|
|
53
|
+
export function registerEditor(name: string, editor: unknown): void {
|
|
54
|
+
VTable.register.editor(name, editor as never);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 构建 ListTable 初始化选项
|
|
59
|
+
*
|
|
60
|
+
* @param records 表格行数据
|
|
61
|
+
* @param columns 列定义
|
|
62
|
+
* @param isDarkMode 是否暗色主题
|
|
63
|
+
* @param options 额外选项(可选),覆盖默认配置
|
|
64
|
+
* @returns ListTable 构造选项
|
|
65
|
+
*/
|
|
66
|
+
export function buildTableOptions(
|
|
67
|
+
records: EntityTableRecord[],
|
|
68
|
+
columns: ListTableConstructorOptions['columns'],
|
|
69
|
+
isDarkMode: boolean,
|
|
70
|
+
options?: Partial<ListTableConstructorOptions>
|
|
71
|
+
): ListTableConstructorOptions {
|
|
72
|
+
return {
|
|
73
|
+
records,
|
|
74
|
+
columns,
|
|
75
|
+
widthMode: 'autoWidth',
|
|
76
|
+
autoFillWidth: true,
|
|
77
|
+
rightFrozenColCount: 1,
|
|
78
|
+
hierarchyIndent: 20,
|
|
79
|
+
theme: createTheme(isDarkMode, getCSSVariables()),
|
|
80
|
+
editCellTrigger: ['keydown', 'doubleclick'],
|
|
81
|
+
tooltip: { renderMode: 'html' },
|
|
82
|
+
menu: { contextMenuItems: ['copy', 'paste'] },
|
|
83
|
+
rowSeriesNumber: {
|
|
84
|
+
title: '',
|
|
85
|
+
width: 40,
|
|
86
|
+
dragOrder: true
|
|
87
|
+
},
|
|
88
|
+
...options
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 创建 VTable ListTable 实例
|
|
94
|
+
*
|
|
95
|
+
* @param container 挂载容器元素
|
|
96
|
+
* @param records 表格行数据
|
|
97
|
+
* @param columns 列定义
|
|
98
|
+
* @param isDarkMode 是否暗色主题
|
|
99
|
+
* @param options 额外选项(可选),覆盖默认配置
|
|
100
|
+
* @returns 已注册基础编辑器的 ListTable 实例
|
|
101
|
+
*/
|
|
102
|
+
export function createListTable(
|
|
103
|
+
container: HTMLElement,
|
|
104
|
+
records: EntityTableRecord[],
|
|
105
|
+
columns: ListTableConstructorOptions['columns'],
|
|
106
|
+
isDarkMode: boolean,
|
|
107
|
+
options?: Partial<ListTableConstructorOptions>
|
|
108
|
+
): VTable.ListTable {
|
|
109
|
+
ensureBaseEditorRegistered();
|
|
110
|
+
const option = buildTableOptions(records, columns, isDarkMode, options);
|
|
111
|
+
return new VTable.ListTable(container, option);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 重置编辑器注册状态(仅供测试使用) */
|
|
115
|
+
export function _resetEditorRegistry(): void {
|
|
116
|
+
baseEditorRegistered = false;
|
|
117
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type * as VTable from '@visactor/vtable';
|
|
2
|
+
import type { EntityTableRecord } from '../interfaces.js';
|
|
3
|
+
import { isTableEditing, setCellSwitchState } from './vtable-compat.js';
|
|
4
|
+
|
|
5
|
+
/** 键盘处理上下文 — 由组件提供回调 */
|
|
6
|
+
export interface KeyboardHandlerContext {
|
|
7
|
+
/** 获取当前表格实例 */
|
|
8
|
+
getTable(): VTable.ListTable | null;
|
|
9
|
+
/** 获取当前选中单元格坐标 */
|
|
10
|
+
getSelectedCell(): {
|
|
11
|
+
/** 列索引 */
|
|
12
|
+
col: number;
|
|
13
|
+
/** 行索引 */
|
|
14
|
+
row: number;
|
|
15
|
+
} | null;
|
|
16
|
+
/** 按列索引获取列定义 */
|
|
17
|
+
getColDef(col: number):
|
|
18
|
+
| {
|
|
19
|
+
/** 单元格类型 */
|
|
20
|
+
cellType?: string;
|
|
21
|
+
/** 字段名 */
|
|
22
|
+
field?: string;
|
|
23
|
+
}
|
|
24
|
+
| undefined;
|
|
25
|
+
/** 复制回调 */
|
|
26
|
+
onCopy(): void;
|
|
27
|
+
/** 粘贴回调 */
|
|
28
|
+
onPaste(): void;
|
|
29
|
+
/** 删除回调 */
|
|
30
|
+
onDelete(): void;
|
|
31
|
+
/** 单元格值变更回调 */
|
|
32
|
+
onCellChange(col: number, row: number, value: unknown): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 可通过 Enter / Space 切换的布尔类型列 */
|
|
36
|
+
const TOGGLE_CELL_TYPES = new Set(['switch', 'checkbox', 'radio']);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 表格容器 keydown 事件处理 — 剪贴板快捷键 + 布尔切换 + 删除。
|
|
40
|
+
* 编辑状态下不拦截剪贴板快捷键与删除键。
|
|
41
|
+
*
|
|
42
|
+
* @param ctx 键盘处理上下文
|
|
43
|
+
* @param e 键盘事件
|
|
44
|
+
*/
|
|
45
|
+
export function handleTableKeydown(ctx: KeyboardHandlerContext, e: KeyboardEvent): void {
|
|
46
|
+
const table = ctx.getTable();
|
|
47
|
+
if (!table) return;
|
|
48
|
+
const editing = isTableEditing(table);
|
|
49
|
+
const isCtrl = e.ctrlKey || e.metaKey;
|
|
50
|
+
|
|
51
|
+
if (isCtrl && e.key === 'c') {
|
|
52
|
+
if (editing) return;
|
|
53
|
+
ctx.onCopy();
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (isCtrl && e.key === 'v') {
|
|
58
|
+
if (editing) return;
|
|
59
|
+
ctx.onPaste();
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
64
|
+
if (editing) return;
|
|
65
|
+
e.stopPropagation();
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
ctx.onDelete();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
71
|
+
toggleCellValue(ctx, e);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Enter / Space 切换 switch / checkbox / radio 单元格值 */
|
|
75
|
+
function toggleCellValue(ctx: KeyboardHandlerContext, e: KeyboardEvent): void {
|
|
76
|
+
const sel = ctx.getSelectedCell();
|
|
77
|
+
if (!sel) return;
|
|
78
|
+
const colDef = ctx.getColDef(sel.col);
|
|
79
|
+
if (!colDef?.cellType || !TOGGLE_CELL_TYPES.has(colDef.cellType)) return;
|
|
80
|
+
const table = ctx.getTable()!;
|
|
81
|
+
const record = table.getRecordByCell(sel.col, sel.row) as EntityTableRecord | undefined;
|
|
82
|
+
if (record?._readonly) return;
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
if (!colDef.field) return;
|
|
85
|
+
const currentValue = (record as Record<string, unknown> | undefined)?.[colDef.field];
|
|
86
|
+
const newValue = !currentValue;
|
|
87
|
+
setCellSwitchState(table, sel.col, sel.row, newValue);
|
|
88
|
+
ctx.onCellChange(sel.col, sel.row, newValue);
|
|
89
|
+
}
|