draw-table-vue 0.2.0 → 0.3.0
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/README.md +125 -40
- package/dist/assets/index-BoSk-1mt.css +1 -0
- package/dist/assets/index-C5vPZIAB.js +1 -0
- package/{index.html → dist/index.html} +2 -1
- package/dist/package.json +24 -0
- package/package.json +15 -1
- package/.vscode/extensions.json +0 -3
- package/docs/DESIGN.md +0 -38
- package/examples/BasicUsage.vue +0 -116
- package/pnpm-lock.yaml +0 -952
- package/src/App.vue +0 -26
- package/src/main.ts +0 -5
- package/src/style.css +0 -79
- package/src/table/components/CanvasTable.vue +0 -659
- package/src/table/core/renderer.ts +0 -948
- package/src/table/index.ts +0 -3
- package/src/table/types/index.ts +0 -77
- package/tsconfig.app.json +0 -16
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -7
- /package/{public → dist}/vite.svg +0 -0
|
@@ -1,659 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="canvas-table-container" ref="containerRef">
|
|
3
|
-
<div
|
|
4
|
-
class="canvas-wrapper"
|
|
5
|
-
:style="wrapperStyle"
|
|
6
|
-
@wheel.prevent="handleWheel"
|
|
7
|
-
>
|
|
8
|
-
<canvas ref="canvasRef"></canvas>
|
|
9
|
-
|
|
10
|
-
<!-- Horizontal Scrollbar -->
|
|
11
|
-
<div
|
|
12
|
-
class="scrollbar horizontal"
|
|
13
|
-
ref="hScrollRef"
|
|
14
|
-
@scroll="handleHScroll"
|
|
15
|
-
>
|
|
16
|
-
<div :style="{ width: totalWidth + 'px', height: '1px' }"></div>
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<!-- Vertical Scrollbar -->
|
|
20
|
-
<div
|
|
21
|
-
class="scrollbar vertical"
|
|
22
|
-
ref="vScrollRef"
|
|
23
|
-
@scroll="handleVScroll"
|
|
24
|
-
>
|
|
25
|
-
<div :style="{ height: totalHeight + 'px', width: '1px' }"></div>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<!-- Overlays (Edit buttons, Dialogs, etc.) -->
|
|
29
|
-
<div class="table-overlays" :style="overlayStyle">
|
|
30
|
-
<div
|
|
31
|
-
v-if="hoverCell && hoverCell.column.renderEdit"
|
|
32
|
-
class="edit-btn"
|
|
33
|
-
:style="editBtnStyle"
|
|
34
|
-
@click.stop="openEdit(hoverCell)"
|
|
35
|
-
>
|
|
36
|
-
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<!-- Header Menu -->
|
|
40
|
-
<div
|
|
41
|
-
v-if="headerMenu"
|
|
42
|
-
class="header-menu-overlay"
|
|
43
|
-
@click.self="headerMenu = null"
|
|
44
|
-
>
|
|
45
|
-
<div class="header-menu" :style="headerMenuStyle">
|
|
46
|
-
<div
|
|
47
|
-
v-for="item in headerMenuItems"
|
|
48
|
-
:key="item.label"
|
|
49
|
-
class="menu-item"
|
|
50
|
-
@click="handleMenuCommand(item)"
|
|
51
|
-
>
|
|
52
|
-
<span class="icon">{{ item.icon || '•' }}</span>
|
|
53
|
-
<span class="label">{{ item.label }}</span>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
|
|
58
|
-
<!-- Expand Rows Overlay -->
|
|
59
|
-
<div
|
|
60
|
-
v-for="rowId in expandedRows"
|
|
61
|
-
:key="rowId"
|
|
62
|
-
class="expand-row-overlay"
|
|
63
|
-
:style="getExpandStyle(rowId)"
|
|
64
|
-
>
|
|
65
|
-
<component :is="renderExpandContent(rowId)" />
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<!-- Edit Dialog -->
|
|
69
|
-
<Teleport to="body">
|
|
70
|
-
<div v-if="editingCell" class="edit-dialog-popover" :style="editDialogStyle">
|
|
71
|
-
<div class="edit-dialog-content">
|
|
72
|
-
<v-node-renderer :vnode="renderCellEdit(editingCell)" />
|
|
73
|
-
</div>
|
|
74
|
-
<div class="edit-dialog-footer">
|
|
75
|
-
<button class="small" @click="closeEdit">Cancel</button>
|
|
76
|
-
<button class="small primary" @click="saveEdit">Save</button>
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
<!-- Click outside listener for popover -->
|
|
80
|
-
<div v-if="editingCell" class="popover-click-mask" @click="closeEdit"></div>
|
|
81
|
-
</Teleport>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
</template>
|
|
86
|
-
|
|
87
|
-
<script setup lang="ts">
|
|
88
|
-
import { ref, onMounted, onUnmounted, watch, computed, shallowRef, h, defineComponent } from 'vue';
|
|
89
|
-
import { CanvasRenderer } from '../core/renderer';
|
|
90
|
-
import type { ColumnConfig, TableRow, TableOptions, CellInfo, MenuItem } from '../types';
|
|
91
|
-
|
|
92
|
-
// Helper component to render VNodes
|
|
93
|
-
const VNodeRenderer = defineComponent({
|
|
94
|
-
props: ['vnode'],
|
|
95
|
-
setup(props) {
|
|
96
|
-
return () => props.vnode;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const props = defineProps<{
|
|
101
|
-
columns: ColumnConfig[];
|
|
102
|
-
data: TableRow[];
|
|
103
|
-
options?: Partial<TableOptions>;
|
|
104
|
-
}>();
|
|
105
|
-
|
|
106
|
-
const emit = defineEmits(['update:data', 'select-change', 'header-command']);
|
|
107
|
-
|
|
108
|
-
const containerRef = ref<HTMLDivElement | null>(null);
|
|
109
|
-
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
|
110
|
-
const hScrollRef = ref<HTMLDivElement | null>(null);
|
|
111
|
-
const vScrollRef = ref<HTMLDivElement | null>(null);
|
|
112
|
-
|
|
113
|
-
const renderer = shallowRef<CanvasRenderer | null>(null);
|
|
114
|
-
const totalWidth = ref(0);
|
|
115
|
-
const totalHeight = ref(0);
|
|
116
|
-
const viewWidth = ref(0);
|
|
117
|
-
const viewHeight = ref(0);
|
|
118
|
-
const scrollX = ref(0);
|
|
119
|
-
const scrollY = ref(0);
|
|
120
|
-
|
|
121
|
-
// Interaction state
|
|
122
|
-
const hoverCell = ref<CellInfo | null>(null);
|
|
123
|
-
const editingCell = ref<CellInfo | null>(null);
|
|
124
|
-
const editingData = ref<TableRow | null>(null);
|
|
125
|
-
const selectedRows = ref<Set<string | number>>(new Set());
|
|
126
|
-
const expandedRows = ref<Set<string | number>>(new Set());
|
|
127
|
-
const headerMenu = ref<{ column: ColumnConfig, rect: any } | null>(null);
|
|
128
|
-
|
|
129
|
-
const wrapperStyle = computed(() => ({
|
|
130
|
-
width: '100%',
|
|
131
|
-
height: '100%',
|
|
132
|
-
position: 'relative' as const,
|
|
133
|
-
overflow: 'hidden'
|
|
134
|
-
}));
|
|
135
|
-
|
|
136
|
-
const overlayStyle = computed(() => ({
|
|
137
|
-
position: 'absolute' as const,
|
|
138
|
-
top: 0,
|
|
139
|
-
left: 0,
|
|
140
|
-
pointerEvents: 'none' as const,
|
|
141
|
-
width: '100%',
|
|
142
|
-
height: '100%'
|
|
143
|
-
}));
|
|
144
|
-
|
|
145
|
-
const editBtnStyle = computed(() => {
|
|
146
|
-
if (!hoverCell.value) return {};
|
|
147
|
-
const { rect } = hoverCell.value;
|
|
148
|
-
return {
|
|
149
|
-
position: 'absolute' as const,
|
|
150
|
-
left: `${rect.x + rect.width - 24}px`,
|
|
151
|
-
top: `${rect.y + (rect.height - 20) / 2}px`,
|
|
152
|
-
pointerEvents: 'auto' as const
|
|
153
|
-
};
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const editDialogStyle = computed(() => {
|
|
157
|
-
if (!editingCell.value || !canvasRef.value) return {};
|
|
158
|
-
|
|
159
|
-
const canvasRect = canvasRef.value.getBoundingClientRect();
|
|
160
|
-
const { rect } = editingCell.value;
|
|
161
|
-
|
|
162
|
-
// Position popover relative to the cell
|
|
163
|
-
// We place it below the cell by default
|
|
164
|
-
let top = canvasRect.top + rect.y + rect.height + 5;
|
|
165
|
-
let left = canvasRect.left + rect.x;
|
|
166
|
-
|
|
167
|
-
// Simple viewport boundary check
|
|
168
|
-
const popoverWidth = 300; // Estimated or fixed
|
|
169
|
-
if (left + popoverWidth > window.innerWidth) {
|
|
170
|
-
left = window.innerWidth - popoverWidth - 20;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
position: 'fixed' as const,
|
|
175
|
-
top: `${top}px`,
|
|
176
|
-
left: `${left}px`,
|
|
177
|
-
zIndex: 2000,
|
|
178
|
-
pointerEvents: 'auto' as const
|
|
179
|
-
};
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const headerMenuStyle = computed(() => {
|
|
183
|
-
if (!headerMenu.value) return {};
|
|
184
|
-
const { rect } = headerMenu.value;
|
|
185
|
-
return {
|
|
186
|
-
position: 'absolute' as const,
|
|
187
|
-
left: `${rect.x + rect.width - 120}px`,
|
|
188
|
-
top: `${rect.y + rect.height}px`,
|
|
189
|
-
pointerEvents: 'auto' as const
|
|
190
|
-
};
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
const headerMenuItems = computed((): MenuItem[] => {
|
|
194
|
-
if (!headerMenu.value) return [];
|
|
195
|
-
const { column } = headerMenu.value;
|
|
196
|
-
const defaultMenu: MenuItem[] = [
|
|
197
|
-
{
|
|
198
|
-
label: 'Fix to Left',
|
|
199
|
-
onCommand: (col) => {
|
|
200
|
-
// Find index of this column and fix all previous ones
|
|
201
|
-
const idx = props.columns.findIndex(c => c.key === col.key);
|
|
202
|
-
props.columns.forEach((c, i) => {
|
|
203
|
-
if (i <= idx) c.fixed = 'left';
|
|
204
|
-
});
|
|
205
|
-
renderer.value?.setColumns(props.columns);
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
label: 'Unfix',
|
|
210
|
-
onCommand: (col) => {
|
|
211
|
-
col.fixed = false;
|
|
212
|
-
renderer.value?.setColumns(props.columns);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
];
|
|
216
|
-
if (column.renderHeaderMenu) {
|
|
217
|
-
return column.renderHeaderMenu(column, defaultMenu);
|
|
218
|
-
}
|
|
219
|
-
return defaultMenu;
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const handleMenuCommand = (item: MenuItem) => {
|
|
223
|
-
if (headerMenu.value) {
|
|
224
|
-
item.onCommand(headerMenu.value.column);
|
|
225
|
-
headerMenu.value = null;
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
230
|
-
if (!canvasRef.value || !renderer.value) return;
|
|
231
|
-
const rect = canvasRef.value.getBoundingClientRect();
|
|
232
|
-
const x = e.clientX - rect.left;
|
|
233
|
-
const y = e.clientY - rect.top;
|
|
234
|
-
|
|
235
|
-
// Handle header hover
|
|
236
|
-
const header = renderer.value.getHeaderAt(x, y);
|
|
237
|
-
if (header) {
|
|
238
|
-
const idx = props.columns.findIndex(c => c.key === header.column.key);
|
|
239
|
-
renderer.value.setHoverHeader(idx);
|
|
240
|
-
|
|
241
|
-
// Set cursor style
|
|
242
|
-
if (header.isMenu || header.column.type === 'selection') {
|
|
243
|
-
canvasRef.value.style.cursor = 'pointer';
|
|
244
|
-
} else {
|
|
245
|
-
canvasRef.value.style.cursor = 'default';
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
renderer.value.setHoverHeader(-1);
|
|
249
|
-
canvasRef.value.style.cursor = 'default';
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
hoverCell.value = renderer.value.getCellAt(x, y);
|
|
253
|
-
|
|
254
|
-
// Update cursor if hovering over expand button or selection
|
|
255
|
-
if (hoverCell.value) {
|
|
256
|
-
if (hoverCell.value.isExpandBtn || hoverCell.value.isSelection) {
|
|
257
|
-
canvasRef.value.style.cursor = 'pointer';
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const handleMouseDown = (e: MouseEvent) => {
|
|
263
|
-
if (!canvasRef.value || !renderer.value) return;
|
|
264
|
-
const rect = canvasRef.value.getBoundingClientRect();
|
|
265
|
-
const x = e.clientX - rect.left;
|
|
266
|
-
const y = e.clientY - rect.top;
|
|
267
|
-
|
|
268
|
-
// Check header click
|
|
269
|
-
const header = renderer.value.getHeaderAt(x, y);
|
|
270
|
-
if (header) {
|
|
271
|
-
if (header.isMenu) {
|
|
272
|
-
headerMenu.value = { column: header.column, rect: header.rect };
|
|
273
|
-
} else if (header.column.type === 'selection') {
|
|
274
|
-
// Toggle all selection
|
|
275
|
-
const allSelected = selectedRows.value.size === props.data.length;
|
|
276
|
-
if (allSelected) {
|
|
277
|
-
selectedRows.value.clear();
|
|
278
|
-
} else {
|
|
279
|
-
props.data.forEach(r => selectedRows.value.add(r.id));
|
|
280
|
-
}
|
|
281
|
-
renderer.value.setSelectedRows(Array.from(selectedRows.value));
|
|
282
|
-
emit('select-change', Array.from(selectedRows.value));
|
|
283
|
-
}
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const cell = renderer.value.getCellAt(x, y);
|
|
288
|
-
if (cell) {
|
|
289
|
-
if (cell.isExpandBtn) {
|
|
290
|
-
toggleExpand(cell.data.id);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const rowId = cell.data.id;
|
|
295
|
-
if (cell.isSelection) {
|
|
296
|
-
if (selectedRows.value.has(rowId)) {
|
|
297
|
-
selectedRows.value.delete(rowId);
|
|
298
|
-
} else {
|
|
299
|
-
selectedRows.value.add(rowId);
|
|
300
|
-
}
|
|
301
|
-
renderer.value.setSelectedRows(Array.from(selectedRows.value));
|
|
302
|
-
emit('select-change', Array.from(selectedRows.value));
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (props.options?.multiSelect) {
|
|
307
|
-
if (selectedRows.value.has(rowId)) {
|
|
308
|
-
selectedRows.value.delete(rowId);
|
|
309
|
-
} else {
|
|
310
|
-
selectedRows.value.add(rowId);
|
|
311
|
-
}
|
|
312
|
-
} else {
|
|
313
|
-
selectedRows.value.clear();
|
|
314
|
-
selectedRows.value.add(rowId);
|
|
315
|
-
}
|
|
316
|
-
emit('select-change', Array.from(selectedRows.value));
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
const handleDoubleClick = (e: MouseEvent) => {
|
|
321
|
-
if (!canvasRef.value || !renderer.value) return;
|
|
322
|
-
const rect = canvasRef.value.getBoundingClientRect();
|
|
323
|
-
const x = e.clientX - rect.left;
|
|
324
|
-
const y = e.clientY - rect.top;
|
|
325
|
-
const cell = renderer.value.getCellAt(x, y);
|
|
326
|
-
if (cell && cell.column.renderEdit) {
|
|
327
|
-
openEdit(cell);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
const openEdit = (cell: CellInfo) => {
|
|
332
|
-
editingCell.value = cell;
|
|
333
|
-
editingData.value = JSON.parse(JSON.stringify(cell.data));
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
const toggleExpand = (rowId: string | number) => {
|
|
337
|
-
if (expandedRows.value.has(rowId)) {
|
|
338
|
-
expandedRows.value.delete(rowId);
|
|
339
|
-
} else {
|
|
340
|
-
expandedRows.value.add(rowId);
|
|
341
|
-
}
|
|
342
|
-
renderer.value?.setExpandedRows(Array.from(expandedRows.value));
|
|
343
|
-
updateTotalSize();
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
const closeEdit = () => {
|
|
347
|
-
editingCell.value = null;
|
|
348
|
-
editingData.value = null;
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const saveEdit = () => {
|
|
352
|
-
if (editingCell.value && editingData.value) {
|
|
353
|
-
const rowId = editingCell.value.data.id;
|
|
354
|
-
const rowIndex = props.data.findIndex(r => r.id === rowId);
|
|
355
|
-
if (rowIndex !== -1) {
|
|
356
|
-
// In a real application, you'd likely want to avoid direct prop mutation
|
|
357
|
-
// We emit the change so the parent can update the data
|
|
358
|
-
const newData = [...props.data];
|
|
359
|
-
newData[rowIndex] = editingData.value;
|
|
360
|
-
emit('update:data', newData);
|
|
361
|
-
|
|
362
|
-
// Since renderer takes data as prop, we need to manually update it if it doesn't watch props
|
|
363
|
-
renderer.value?.setData(newData);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
closeEdit();
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
const renderCellEdit = (cell: CellInfo) => {
|
|
370
|
-
if (!cell.column.renderEdit || !editingData.value) return null;
|
|
371
|
-
return cell.column.renderEdit(editingData.value, (comp: any, propsOrChildren?: any, children?: any) => {
|
|
372
|
-
if (propsOrChildren && (typeof propsOrChildren === 'string' || Array.isArray(propsOrChildren) || propsOrChildren.__v_isVNode)) {
|
|
373
|
-
return h(comp, null, propsOrChildren);
|
|
374
|
-
}
|
|
375
|
-
return h(comp, propsOrChildren, children);
|
|
376
|
-
});
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
const getExpandStyle = (rowId: string | number) => {
|
|
380
|
-
if (!renderer.value) return {};
|
|
381
|
-
const rowIndex = props.data.findIndex(r => r.id === rowId);
|
|
382
|
-
if (rowIndex === -1) return {};
|
|
383
|
-
|
|
384
|
-
const rowOffsets = renderer.value.getRowOffsets();
|
|
385
|
-
const y = (rowOffsets[rowIndex] ?? 0) - scrollY.value + (props.options?.headerHeight || 48);
|
|
386
|
-
const h = (rowOffsets[rowIndex+1] ?? 0) - (rowOffsets[rowIndex] ?? 0) - (props.options?.rowHeight || 40);
|
|
387
|
-
const fixedLeftWidth = renderer.value.getFixedLeftWidth();
|
|
388
|
-
|
|
389
|
-
return {
|
|
390
|
-
position: 'absolute' as const,
|
|
391
|
-
top: `${y + (props.options?.rowHeight || 40)}px`,
|
|
392
|
-
left: `${fixedLeftWidth}px`,
|
|
393
|
-
width: `${viewWidth.value - fixedLeftWidth}px`,
|
|
394
|
-
height: `${h}px`,
|
|
395
|
-
pointerEvents: 'auto' as const,
|
|
396
|
-
overflow: 'hidden',
|
|
397
|
-
backgroundColor: '#fff',
|
|
398
|
-
borderBottom: '1px solid #ebeef5',
|
|
399
|
-
// Sync inner scrolling content
|
|
400
|
-
display: 'flex'
|
|
401
|
-
};
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
const getExpandContentStyle = () => {
|
|
405
|
-
return {
|
|
406
|
-
marginLeft: `-${scrollX.value}px`,
|
|
407
|
-
minWidth: `${totalWidth.value - renderer.value?.getFixedLeftWidth()!}px`
|
|
408
|
-
};
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
const renderExpandContent = (rowId: string | number) => {
|
|
412
|
-
const row = props.data.find(r => r.id === rowId);
|
|
413
|
-
if (!row || !props.options?.renderExpand) return null;
|
|
414
|
-
return h('div', { style: getExpandContentStyle() }, [
|
|
415
|
-
props.options.renderExpand(row, (comp: any, propsOrChildren?: any, children?: any) => {
|
|
416
|
-
if (propsOrChildren && (typeof propsOrChildren === 'string' || Array.isArray(propsOrChildren) || propsOrChildren.__v_isVNode)) {
|
|
417
|
-
return h(comp, null, propsOrChildren);
|
|
418
|
-
}
|
|
419
|
-
return h(comp, propsOrChildren, children);
|
|
420
|
-
})
|
|
421
|
-
]);
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
const handleHScroll = (e: Event) => {
|
|
425
|
-
const target = e.target as HTMLDivElement;
|
|
426
|
-
scrollX.value = target.scrollLeft;
|
|
427
|
-
renderer.value?.scrollTo(scrollX.value, scrollY.value);
|
|
428
|
-
hoverCell.value = null;
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
const handleVScroll = (e: Event) => {
|
|
432
|
-
const target = e.target as HTMLDivElement;
|
|
433
|
-
scrollY.value = target.scrollTop;
|
|
434
|
-
renderer.value?.scrollTo(scrollX.value, scrollY.value);
|
|
435
|
-
hoverCell.value = null;
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const handleWheel = (e: WheelEvent) => {
|
|
439
|
-
if (!vScrollRef.value || !hScrollRef.value) return;
|
|
440
|
-
|
|
441
|
-
// Use shift for horizontal or just deltaX
|
|
442
|
-
const deltaY = e.deltaY;
|
|
443
|
-
const deltaX = e.shiftKey ? e.deltaY : e.deltaX;
|
|
444
|
-
|
|
445
|
-
vScrollRef.value.scrollTop += deltaY;
|
|
446
|
-
hScrollRef.value.scrollLeft += deltaX;
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
const handleResize = (entries: ResizeObserverEntry[]) => {
|
|
450
|
-
const entry = entries[0];
|
|
451
|
-
if (!entry || !renderer.value) return;
|
|
452
|
-
|
|
453
|
-
const { width, height } = entry.contentRect;
|
|
454
|
-
viewWidth.value = width;
|
|
455
|
-
viewHeight.value = height;
|
|
456
|
-
|
|
457
|
-
renderer.value.resize(width, height);
|
|
458
|
-
updateTotalSize();
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const updateTotalSize = () => {
|
|
462
|
-
if (!renderer.value) return;
|
|
463
|
-
totalWidth.value = renderer.value.getTotalWidth();
|
|
464
|
-
totalHeight.value = renderer.value.getTotalHeight();
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
let resizeObserver: ResizeObserver | null = null;
|
|
468
|
-
|
|
469
|
-
onMounted(() => {
|
|
470
|
-
if (canvasRef.value) {
|
|
471
|
-
renderer.value = new CanvasRenderer(canvasRef.value);
|
|
472
|
-
renderer.value.setColumns(props.columns);
|
|
473
|
-
renderer.value.setData(props.data);
|
|
474
|
-
renderer.value.setOptions(props.options || {});
|
|
475
|
-
|
|
476
|
-
updateTotalSize();
|
|
477
|
-
|
|
478
|
-
resizeObserver = new ResizeObserver(handleResize);
|
|
479
|
-
if (containerRef.value) {
|
|
480
|
-
resizeObserver.observe(containerRef.value);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
canvasRef.value.addEventListener('mousemove', handleMouseMove);
|
|
484
|
-
canvasRef.value.addEventListener('mousedown', handleMouseDown);
|
|
485
|
-
canvasRef.value.addEventListener('dblclick', handleDoubleClick);
|
|
486
|
-
}
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
onUnmounted(() => {
|
|
490
|
-
resizeObserver?.disconnect();
|
|
491
|
-
canvasRef.value?.removeEventListener('mousemove', handleMouseMove);
|
|
492
|
-
canvasRef.value?.removeEventListener('mousedown', handleMouseDown);
|
|
493
|
-
canvasRef.value?.removeEventListener('dblclick', handleDoubleClick);
|
|
494
|
-
|
|
495
|
-
renderer.value?.destroy();
|
|
496
|
-
renderer.value = null;
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
watch(() => props.columns, (cols) => {
|
|
500
|
-
renderer.value?.setColumns(cols);
|
|
501
|
-
updateTotalSize();
|
|
502
|
-
}, { deep: true });
|
|
503
|
-
|
|
504
|
-
watch(() => props.data, (data) => {
|
|
505
|
-
renderer.value?.setData(data);
|
|
506
|
-
updateTotalSize();
|
|
507
|
-
}, { deep: true });
|
|
508
|
-
|
|
509
|
-
watch(() => props.options, (opts) => {
|
|
510
|
-
if (opts) renderer.value?.setOptions(opts);
|
|
511
|
-
}, { deep: true });
|
|
512
|
-
|
|
513
|
-
</script>
|
|
514
|
-
|
|
515
|
-
<style scoped>
|
|
516
|
-
.canvas-table-container {
|
|
517
|
-
width: 100%;
|
|
518
|
-
height: 100%;
|
|
519
|
-
border: 1px solid #ebeef5;
|
|
520
|
-
box-sizing: border-box;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
.canvas-wrapper {
|
|
524
|
-
background: #fff;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
.scrollbar {
|
|
528
|
-
position: absolute;
|
|
529
|
-
overflow: auto;
|
|
530
|
-
z-index: 10;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
.scrollbar.horizontal {
|
|
534
|
-
bottom: 0;
|
|
535
|
-
left: 0;
|
|
536
|
-
right: 0;
|
|
537
|
-
height: 12px;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
.scrollbar.vertical {
|
|
541
|
-
top: 0;
|
|
542
|
-
right: 0;
|
|
543
|
-
bottom: 0;
|
|
544
|
-
width: 12px;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/* Hide default scrollbars for the wrapper, use custom ones */
|
|
548
|
-
.scrollbar::-webkit-scrollbar {
|
|
549
|
-
width: 8px;
|
|
550
|
-
height: 8px;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
.scrollbar::-webkit-scrollbar-thumb {
|
|
554
|
-
background: #c1c1c1;
|
|
555
|
-
border-radius: 4px;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
.scrollbar::-webkit-scrollbar-thumb:hover {
|
|
559
|
-
background: #a8a8a8;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
.table-overlays {
|
|
563
|
-
z-index: 5;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
.edit-btn {
|
|
567
|
-
width: 20px;
|
|
568
|
-
height: 20px;
|
|
569
|
-
background: #fff;
|
|
570
|
-
border: 1px solid #dcdfe6;
|
|
571
|
-
border-radius: 4px;
|
|
572
|
-
display: flex;
|
|
573
|
-
align-items: center;
|
|
574
|
-
justify-content: center;
|
|
575
|
-
cursor: pointer;
|
|
576
|
-
color: #409eff;
|
|
577
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
.edit-btn:hover {
|
|
581
|
-
background: #f5f7fa;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
.header-menu-overlay {
|
|
585
|
-
position: absolute;
|
|
586
|
-
top: 0;
|
|
587
|
-
left: 0;
|
|
588
|
-
right: 0;
|
|
589
|
-
bottom: 0;
|
|
590
|
-
pointer-events: auto;
|
|
591
|
-
z-index: 20;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
.header-menu {
|
|
595
|
-
background: #fff;
|
|
596
|
-
border: 1px solid #ebeef5;
|
|
597
|
-
border-radius: 4px;
|
|
598
|
-
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
|
599
|
-
padding: 5px 0;
|
|
600
|
-
min-width: 120px;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
.menu-item {
|
|
604
|
-
padding: 8px 12px;
|
|
605
|
-
cursor: pointer;
|
|
606
|
-
display: flex;
|
|
607
|
-
align-items: center;
|
|
608
|
-
gap: 8px;
|
|
609
|
-
font-size: 14px;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
.menu-item:hover {
|
|
613
|
-
background: #f5f7fa;
|
|
614
|
-
color: #409eff;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
.edit-dialog-popover {
|
|
618
|
-
background: #fff;
|
|
619
|
-
border-radius: 4px;
|
|
620
|
-
width: 300px;
|
|
621
|
-
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
|
622
|
-
border: 1px solid #ebeef5;
|
|
623
|
-
display: flex;
|
|
624
|
-
flex-direction: column;
|
|
625
|
-
z-index: 2000;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
.popover-click-mask {
|
|
629
|
-
position: fixed;
|
|
630
|
-
top: 0;
|
|
631
|
-
left: 0;
|
|
632
|
-
right: 0;
|
|
633
|
-
bottom: 0;
|
|
634
|
-
z-index: 1999;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
.edit-dialog-content {
|
|
638
|
-
padding: 12px;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
.edit-dialog-footer {
|
|
642
|
-
padding: 8px 12px;
|
|
643
|
-
border-top: 1px solid #ebeef5;
|
|
644
|
-
display: flex;
|
|
645
|
-
justify-content: flex-end;
|
|
646
|
-
gap: 8px;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
button.small {
|
|
650
|
-
padding: 4px 12px;
|
|
651
|
-
font-size: 12px;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
button.primary {
|
|
655
|
-
background: #409eff;
|
|
656
|
-
color: #fff;
|
|
657
|
-
border-color: #409eff;
|
|
658
|
-
}
|
|
659
|
-
</style>
|