@alaarab/ogrid-vue-vuetify 2.0.11 → 2.0.13
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/dist/esm/ColumnHeaderMenu/ColumnHeaderMenu.js +5 -5
- package/dist/esm/DataGridTable/DataGridTable.css +351 -4
- package/dist/esm/DataGridTable/DataGridTable.js +22 -603
- package/dist/esm/DataGridTable/EmptyState.js +22 -0
- package/dist/esm/OGrid/OGrid.js +4 -4
- package/dist/esm/PaginationControls/PaginationControls.js +4 -4
- package/dist/types/DataGridTable/DataGridTable.d.ts +3 -5
- package/dist/types/DataGridTable/EmptyState.d.ts +11 -0
- package/package.json +2 -2
|
@@ -60,16 +60,16 @@ export const ColumnHeaderMenu = defineComponent({
|
|
|
60
60
|
}, {
|
|
61
61
|
default: () => h(VList, { density: 'compact', 'aria-label': 'Column options' }, () => {
|
|
62
62
|
const children = [];
|
|
63
|
-
items.value.forEach((item
|
|
64
|
-
// Add divider before item if needed (but not at the start)
|
|
65
|
-
if (item.divider && index > 0) {
|
|
66
|
-
children.push(h(VDivider, { key: `divider-${item.id}` }));
|
|
67
|
-
}
|
|
63
|
+
items.value.forEach((item) => {
|
|
68
64
|
children.push(h(VListItem, {
|
|
69
65
|
key: item.id,
|
|
70
66
|
disabled: item.disabled,
|
|
71
67
|
onClick: () => { getHandler(item.id)(); },
|
|
72
68
|
}, () => item.label));
|
|
69
|
+
// Add divider after item to separate sections
|
|
70
|
+
if (item.divider) {
|
|
71
|
+
children.push(h(VDivider, { key: `divider-${item.id}` }));
|
|
72
|
+
}
|
|
73
73
|
});
|
|
74
74
|
return children;
|
|
75
75
|
}),
|
|
@@ -1,4 +1,55 @@
|
|
|
1
|
-
/* OGrid Vue Vuetify
|
|
1
|
+
/* OGrid Vue Vuetify - DataGridTable styles */
|
|
2
|
+
|
|
3
|
+
/* ─── OGrid Theme Variables ─── */
|
|
4
|
+
:root {
|
|
5
|
+
--ogrid-bg: #ffffff;
|
|
6
|
+
--ogrid-fg: rgba(0, 0, 0, 0.87);
|
|
7
|
+
--ogrid-fg-secondary: rgba(0, 0, 0, 0.6);
|
|
8
|
+
--ogrid-fg-muted: rgba(0, 0, 0, 0.5);
|
|
9
|
+
--ogrid-border: rgba(0, 0, 0, 0.12);
|
|
10
|
+
--ogrid-header-bg: rgba(0, 0, 0, 0.04);
|
|
11
|
+
--ogrid-hover-bg: rgba(0, 0, 0, 0.04);
|
|
12
|
+
--ogrid-selected-row-bg: #e6f0fb;
|
|
13
|
+
--ogrid-active-cell-bg: rgba(0, 0, 0, 0.02);
|
|
14
|
+
--ogrid-range-bg: rgba(33, 115, 70, 0.12);
|
|
15
|
+
--ogrid-accent: #0078d4;
|
|
16
|
+
--ogrid-selection-color: #217346;
|
|
17
|
+
--ogrid-loading-overlay: rgba(255, 255, 255, 0.7);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root:not([data-theme="light"]) {
|
|
22
|
+
--ogrid-bg: #1e1e1e;
|
|
23
|
+
--ogrid-fg: rgba(255, 255, 255, 0.87);
|
|
24
|
+
--ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
|
|
25
|
+
--ogrid-fg-muted: rgba(255, 255, 255, 0.5);
|
|
26
|
+
--ogrid-border: rgba(255, 255, 255, 0.12);
|
|
27
|
+
--ogrid-header-bg: rgba(255, 255, 255, 0.06);
|
|
28
|
+
--ogrid-hover-bg: rgba(255, 255, 255, 0.08);
|
|
29
|
+
--ogrid-selected-row-bg: #1a3a5c;
|
|
30
|
+
--ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
|
|
31
|
+
--ogrid-range-bg: rgba(46, 160, 67, 0.15);
|
|
32
|
+
--ogrid-accent: #4da6ff;
|
|
33
|
+
--ogrid-selection-color: #2ea043;
|
|
34
|
+
--ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[data-theme="dark"] {
|
|
39
|
+
--ogrid-bg: #1e1e1e;
|
|
40
|
+
--ogrid-fg: rgba(255, 255, 255, 0.87);
|
|
41
|
+
--ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
|
|
42
|
+
--ogrid-fg-muted: rgba(255, 255, 255, 0.5);
|
|
43
|
+
--ogrid-border: rgba(255, 255, 255, 0.12);
|
|
44
|
+
--ogrid-header-bg: rgba(255, 255, 255, 0.06);
|
|
45
|
+
--ogrid-hover-bg: rgba(255, 255, 255, 0.08);
|
|
46
|
+
--ogrid-selected-row-bg: #1a3a5c;
|
|
47
|
+
--ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
|
|
48
|
+
--ogrid-range-bg: rgba(46, 160, 67, 0.15);
|
|
49
|
+
--ogrid-accent: #4da6ff;
|
|
50
|
+
--ogrid-selection-color: #2ea043;
|
|
51
|
+
--ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
|
|
52
|
+
}
|
|
2
53
|
|
|
3
54
|
/* Remove focus outline from scrollable wrapper (keyboard nav is handled via cell outlines) */
|
|
4
55
|
[role="region"][tabindex="0"] {
|
|
@@ -12,18 +63,18 @@
|
|
|
12
63
|
|
|
13
64
|
/* Cut range highlighting */
|
|
14
65
|
.ogrid-cell-cut {
|
|
15
|
-
background: var(--ogrid-bg
|
|
66
|
+
background: var(--ogrid-hover-bg) !important;
|
|
16
67
|
opacity: 0.7;
|
|
17
68
|
}
|
|
18
69
|
|
|
19
70
|
/* Drag-range highlight applied via DOM attributes during drag (bypasses Vue for performance) */
|
|
20
71
|
[data-drag-range] {
|
|
21
|
-
background: var(--ogrid-bg
|
|
72
|
+
background: var(--ogrid-range-bg, rgba(33, 115, 70, 0.12)) !important;
|
|
22
73
|
}
|
|
23
74
|
|
|
24
75
|
/* Anchor cell during drag: white/transparent background (like Excel) */
|
|
25
76
|
[data-drag-anchor] {
|
|
26
|
-
background: var(--ogrid-bg
|
|
77
|
+
background: var(--ogrid-bg) !important;
|
|
27
78
|
}
|
|
28
79
|
|
|
29
80
|
/* Accessibility: Focus visible styles */
|
|
@@ -49,3 +100,299 @@
|
|
|
49
100
|
outline-offset: -2px;
|
|
50
101
|
z-index: 3;
|
|
51
102
|
}
|
|
103
|
+
|
|
104
|
+
/* === Layout === */
|
|
105
|
+
|
|
106
|
+
.ogrid-outer-container {
|
|
107
|
+
position: relative;
|
|
108
|
+
flex: 1;
|
|
109
|
+
min-height: 0;
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
background-color: var(--ogrid-bg);
|
|
113
|
+
color: var(--ogrid-fg);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.ogrid-scroll-wrapper {
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
min-height: 100%;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.ogrid-table-container {
|
|
123
|
+
position: relative;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.ogrid-table-container--loading {
|
|
127
|
+
opacity: 0.6;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.ogrid-table {
|
|
131
|
+
width: 100%;
|
|
132
|
+
border-collapse: collapse;
|
|
133
|
+
font-size: 0.875rem;
|
|
134
|
+
background-color: var(--ogrid-bg);
|
|
135
|
+
color: var(--ogrid-fg);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* === Header === */
|
|
139
|
+
|
|
140
|
+
.ogrid-thead {
|
|
141
|
+
z-index: 8;
|
|
142
|
+
background-color: var(--ogrid-header-bg);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ogrid-header-row {
|
|
146
|
+
background-color: var(--ogrid-header-bg);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.ogrid-header-cell {
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
position: sticky;
|
|
152
|
+
top: 0;
|
|
153
|
+
background-color: var(--ogrid-header-bg);
|
|
154
|
+
z-index: 8;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.ogrid-header-cell--pinned-left {
|
|
158
|
+
z-index: 10;
|
|
159
|
+
will-change: transform;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.ogrid-header-cell--pinned-right {
|
|
163
|
+
z-index: 10;
|
|
164
|
+
will-change: transform;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.ogrid-header-content {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
width: 100%;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.ogrid-column-group-header {
|
|
174
|
+
text-align: center;
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
border-bottom: 2px solid var(--ogrid-border);
|
|
177
|
+
padding: 6px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.ogrid-column-menu-btn {
|
|
181
|
+
background: none;
|
|
182
|
+
border: none;
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
padding: 4px 6px;
|
|
185
|
+
font-size: 16px;
|
|
186
|
+
color: var(--ogrid-fg-muted);
|
|
187
|
+
line-height: 1;
|
|
188
|
+
flex-shrink: 0;
|
|
189
|
+
border-radius: 4px;
|
|
190
|
+
display: inline-flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
justify-content: center;
|
|
193
|
+
min-width: 24px;
|
|
194
|
+
height: 24px;
|
|
195
|
+
transition: background-color 0.15s;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.ogrid-column-menu-btn:hover {
|
|
199
|
+
background: var(--ogrid-hover-bg, rgba(0, 0, 0, 0.04));
|
|
200
|
+
color: var(--ogrid-fg);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* === Checkbox column === */
|
|
204
|
+
|
|
205
|
+
.ogrid-checkbox-header,
|
|
206
|
+
.ogrid-checkbox-cell {
|
|
207
|
+
text-align: center;
|
|
208
|
+
padding: 4px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.ogrid-checkbox-cell {
|
|
212
|
+
padding: 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.ogrid-checkbox-wrapper {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
justify-content: center;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.ogrid-checkbox-spacer {
|
|
222
|
+
padding: 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* === Row numbers === */
|
|
226
|
+
|
|
227
|
+
.ogrid-row-number-header {
|
|
228
|
+
text-align: center;
|
|
229
|
+
font-weight: 600;
|
|
230
|
+
background-color: var(--ogrid-header-bg);
|
|
231
|
+
color: var(--ogrid-fg-secondary);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.ogrid-row-number-spacer {
|
|
235
|
+
background-color: var(--ogrid-header-bg);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.ogrid-row-number-cell {
|
|
239
|
+
text-align: center;
|
|
240
|
+
font-weight: 600;
|
|
241
|
+
font-variant-numeric: tabular-nums;
|
|
242
|
+
background-color: var(--ogrid-header-bg);
|
|
243
|
+
color: var(--ogrid-fg-secondary);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* === Data cells === */
|
|
247
|
+
|
|
248
|
+
.ogrid-data-cell {
|
|
249
|
+
position: relative;
|
|
250
|
+
padding: 0;
|
|
251
|
+
height: 1px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.ogrid-data-cell--pinned-left {
|
|
255
|
+
position: sticky;
|
|
256
|
+
z-index: 6;
|
|
257
|
+
background-color: var(--ogrid-bg);
|
|
258
|
+
will-change: transform;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.ogrid-data-cell--pinned-right {
|
|
262
|
+
position: sticky;
|
|
263
|
+
z-index: 6;
|
|
264
|
+
background-color: var(--ogrid-bg);
|
|
265
|
+
will-change: transform;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.ogrid-cell-content {
|
|
269
|
+
width: 100%;
|
|
270
|
+
height: 100%;
|
|
271
|
+
display: flex;
|
|
272
|
+
align-items: center;
|
|
273
|
+
min-width: 0;
|
|
274
|
+
padding: 6px 10px;
|
|
275
|
+
box-sizing: border-box;
|
|
276
|
+
overflow: hidden;
|
|
277
|
+
text-overflow: ellipsis;
|
|
278
|
+
white-space: nowrap;
|
|
279
|
+
user-select: none;
|
|
280
|
+
outline: none;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.ogrid-cell-content--numeric {
|
|
284
|
+
justify-content: flex-end;
|
|
285
|
+
text-align: right;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.ogrid-cell-content--boolean {
|
|
289
|
+
justify-content: center;
|
|
290
|
+
text-align: center;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.ogrid-cell-content--editable {
|
|
294
|
+
cursor: cell;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.ogrid-cell-content--active {
|
|
298
|
+
outline: 2px solid var(--ogrid-selection, #217346);
|
|
299
|
+
outline-offset: -1px;
|
|
300
|
+
z-index: 2;
|
|
301
|
+
position: relative;
|
|
302
|
+
overflow: visible;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* === Fill handle === */
|
|
306
|
+
|
|
307
|
+
.ogrid-fill-handle {
|
|
308
|
+
position: absolute;
|
|
309
|
+
right: -3px;
|
|
310
|
+
bottom: -3px;
|
|
311
|
+
width: 7px;
|
|
312
|
+
height: 7px;
|
|
313
|
+
background-color: var(--ogrid-selection, #217346);
|
|
314
|
+
border: 1px solid var(--ogrid-bg);
|
|
315
|
+
border-radius: 1px;
|
|
316
|
+
cursor: crosshair;
|
|
317
|
+
pointer-events: auto;
|
|
318
|
+
z-index: 3;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* === Resize handle === */
|
|
322
|
+
|
|
323
|
+
.ogrid-resize-handle {
|
|
324
|
+
position: absolute;
|
|
325
|
+
top: 0;
|
|
326
|
+
right: -3px;
|
|
327
|
+
bottom: 0;
|
|
328
|
+
width: 8px;
|
|
329
|
+
cursor: col-resize;
|
|
330
|
+
user-select: none;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* === Drop indicator === */
|
|
334
|
+
|
|
335
|
+
.ogrid-drop-indicator {
|
|
336
|
+
position: absolute;
|
|
337
|
+
top: 0;
|
|
338
|
+
bottom: 0;
|
|
339
|
+
width: 3px;
|
|
340
|
+
background: var(--ogrid-primary, #217346);
|
|
341
|
+
pointer-events: none;
|
|
342
|
+
z-index: 100;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* === Empty state === */
|
|
346
|
+
|
|
347
|
+
.ogrid-empty-state {
|
|
348
|
+
padding: 32px 16px;
|
|
349
|
+
text-align: center;
|
|
350
|
+
border-top: 1px solid var(--ogrid-border);
|
|
351
|
+
background-color: var(--ogrid-header-bg);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.ogrid-empty-state-title {
|
|
355
|
+
font-size: 1.25rem;
|
|
356
|
+
font-weight: 600;
|
|
357
|
+
margin-bottom: 8px;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.ogrid-empty-state-message {
|
|
361
|
+
font-size: 0.875rem;
|
|
362
|
+
color: var(--ogrid-fg-secondary);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* === Loading overlay === */
|
|
366
|
+
|
|
367
|
+
.ogrid-loading-overlay {
|
|
368
|
+
position: absolute;
|
|
369
|
+
inset: 0;
|
|
370
|
+
z-index: 2;
|
|
371
|
+
display: flex;
|
|
372
|
+
align-items: center;
|
|
373
|
+
justify-content: center;
|
|
374
|
+
background-color: var(--ogrid-loading-overlay);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.ogrid-loading-inner {
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-direction: column;
|
|
380
|
+
align-items: center;
|
|
381
|
+
gap: 8px;
|
|
382
|
+
padding: 16px;
|
|
383
|
+
background-color: var(--ogrid-bg);
|
|
384
|
+
border: 1px solid var(--ogrid-border);
|
|
385
|
+
border-radius: 4px;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.ogrid-loading-message {
|
|
389
|
+
font-size: 0.875rem;
|
|
390
|
+
color: var(--ogrid-fg-secondary);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* === Popover editor anchor === */
|
|
394
|
+
|
|
395
|
+
.ogrid-popover-anchor {
|
|
396
|
+
min-height: 100%;
|
|
397
|
+
min-width: 40px;
|
|
398
|
+
}
|
|
@@ -1,609 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { VCheckbox, VProgressCircular
|
|
3
|
-
import {
|
|
1
|
+
import { h } from 'vue';
|
|
2
|
+
import { VCheckbox, VProgressCircular } from 'vuetify/components';
|
|
3
|
+
import { createDataGridTable } from '@alaarab/ogrid-vue';
|
|
4
4
|
import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
|
|
5
5
|
import { ColumnHeaderMenu } from '../ColumnHeaderMenu/ColumnHeaderMenu';
|
|
6
6
|
import { InlineCellEditor } from './InlineCellEditor';
|
|
7
7
|
import { GridContextMenu } from './GridContextMenu';
|
|
8
|
+
import { renderEmptyState } from './EmptyState';
|
|
8
9
|
import './DataGridTable.css';
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* Why Vue doesn't need React-style memoization:
|
|
28
|
-
* - Vue tracks which specific reactive properties are accessed during render
|
|
29
|
-
* - When `activeCell` changes, Vue only re-runs code paths that access it
|
|
30
|
-
* - React re-runs the entire render function unless explicitly memoized
|
|
31
|
-
*
|
|
32
|
-
* The over-dereferencing pattern here is intentional for code clarity.
|
|
33
|
-
* Vue's reactivity system compensates for it automatically.
|
|
34
|
-
*
|
|
35
|
-
* Benchmark results (1000 rows): Initial: 19.07ms, Selection change: 4.74ms
|
|
36
|
-
*/
|
|
37
|
-
const p = props.gridProps;
|
|
38
|
-
const layout = state.layout.value;
|
|
39
|
-
const rowSel = state.rowSelection.value;
|
|
40
|
-
const editing = state.editing.value;
|
|
41
|
-
const interaction = state.interaction.value;
|
|
42
|
-
const ctxMenu = state.contextMenu.value;
|
|
43
|
-
const viewModels = state.viewModels.value;
|
|
44
|
-
const pinning = state.pinning.value;
|
|
45
|
-
const { headerMenu } = pinning;
|
|
46
|
-
const { visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset: _colOffset, containerWidth, minTableWidth, desiredTableWidth, } = layout;
|
|
47
|
-
const currentPage = props.gridProps.currentPage ?? 1;
|
|
48
|
-
const pageSize = props.gridProps.pageSize ?? 25;
|
|
49
|
-
const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
|
|
50
|
-
const { selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
|
|
51
|
-
const { editingCell: _editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
|
|
52
|
-
const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange: _cutRange, copyRange: _copyRange, canUndo, canRedo, onUndo, onRedo, isDragging: _isDragging, } = interaction;
|
|
53
|
-
const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
|
|
54
|
-
const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError: _onCellError } = viewModels;
|
|
55
|
-
const items = p.items;
|
|
56
|
-
const getRowId = p.getRowId;
|
|
57
|
-
const layoutMode = p.layoutMode ?? 'fill';
|
|
58
|
-
const rowSelection = p.rowSelection ?? 'none';
|
|
59
|
-
const freezeRows = p.freezeRows;
|
|
60
|
-
const freezeCols = p.freezeCols;
|
|
61
|
-
const suppressHorizontalScroll = p.suppressHorizontalScroll;
|
|
62
|
-
const isLoading = p.isLoading ?? false;
|
|
63
|
-
const loadingMessage = p.loadingMessage ?? 'Loading\u2026';
|
|
64
|
-
const ariaLabel = p['aria-label'];
|
|
65
|
-
const ariaLabelledBy = p['aria-labelledby'];
|
|
66
|
-
const fitToContent = layoutMode === 'content';
|
|
67
|
-
const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
|
|
68
|
-
const headerRows = buildHeaderRows(p.columns, p.visibleColumns);
|
|
69
|
-
const editCallbacks = { commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit };
|
|
70
|
-
const interactionHandlers = { handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu };
|
|
71
|
-
const handleSingleRowClick = (e) => {
|
|
72
|
-
if (rowSelection !== 'single')
|
|
73
|
-
return;
|
|
74
|
-
const tr = e.currentTarget;
|
|
75
|
-
const rowId = tr.dataset.rowId;
|
|
76
|
-
if (!rowId)
|
|
77
|
-
return;
|
|
78
|
-
rowSel.updateSelection(selectedRowIds.has(rowId) ? new Set() : new Set([rowId]));
|
|
79
|
-
};
|
|
80
|
-
// Render a cell's content
|
|
81
|
-
const renderCellContent = (item, col, rowIndex, colIdx) => {
|
|
82
|
-
const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInput);
|
|
83
|
-
const _rowId = getRowId(item);
|
|
84
|
-
if (descriptor.mode === 'editing-inline') {
|
|
85
|
-
const editorProps = buildInlineEditorProps(item, col, descriptor, editCallbacks);
|
|
86
|
-
return h(InlineCellEditor, {
|
|
87
|
-
value: editorProps.value,
|
|
88
|
-
item: editorProps.item,
|
|
89
|
-
column: editorProps.column,
|
|
90
|
-
rowIndex: editorProps.rowIndex,
|
|
91
|
-
editorType: editorProps.editorType,
|
|
92
|
-
onCommit: editorProps.onCommit,
|
|
93
|
-
onCancel: editorProps.onCancel,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
|
|
97
|
-
const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValue, editCallbacks);
|
|
98
|
-
// For Vue, custom editors are components
|
|
99
|
-
const CustomEditor = col.cellEditor;
|
|
100
|
-
return h('div', [
|
|
101
|
-
h('div', {
|
|
102
|
-
ref: (el) => { if (el)
|
|
103
|
-
setPopoverAnchorEl(el); },
|
|
104
|
-
style: { minHeight: '100%', minWidth: '40px' },
|
|
105
|
-
'aria-hidden': 'true',
|
|
106
|
-
}),
|
|
107
|
-
// Render custom editor inline (Vue doesn't have Popover built-in to Vuetify in this pattern)
|
|
108
|
-
popoverAnchorEl
|
|
109
|
-
? h(CustomEditor, editorProps)
|
|
110
|
-
: null,
|
|
111
|
-
]);
|
|
112
|
-
}
|
|
113
|
-
// Display mode
|
|
114
|
-
const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
|
|
115
|
-
const cellStyle = resolveCellStyle(col, item);
|
|
116
|
-
const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
|
|
117
|
-
// Compute cell CSS classes based on state
|
|
118
|
-
const _cellClasses = ['ogrid-cell'];
|
|
119
|
-
const cellInlineStyle = {
|
|
120
|
-
width: '100%',
|
|
121
|
-
height: '100%',
|
|
122
|
-
display: 'flex',
|
|
123
|
-
alignItems: 'center',
|
|
124
|
-
minWidth: '0',
|
|
125
|
-
padding: '6px 10px',
|
|
126
|
-
boxSizing: 'border-box',
|
|
127
|
-
overflow: 'hidden',
|
|
128
|
-
textOverflow: 'ellipsis',
|
|
129
|
-
whiteSpace: 'nowrap',
|
|
130
|
-
userSelect: 'none',
|
|
131
|
-
outline: 'none',
|
|
132
|
-
};
|
|
133
|
-
if (col.type === 'numeric') {
|
|
134
|
-
cellInlineStyle.justifyContent = 'flex-end';
|
|
135
|
-
cellInlineStyle.textAlign = 'right';
|
|
136
|
-
}
|
|
137
|
-
else if (col.type === 'boolean') {
|
|
138
|
-
cellInlineStyle.justifyContent = 'center';
|
|
139
|
-
cellInlineStyle.textAlign = 'center';
|
|
140
|
-
}
|
|
141
|
-
if (descriptor.canEditAny) {
|
|
142
|
-
cellInlineStyle.cursor = 'cell';
|
|
143
|
-
}
|
|
144
|
-
if (descriptor.isActive && !descriptor.isInRange) {
|
|
145
|
-
cellInlineStyle.outline = '2px solid var(--ogrid-selection, #217346)';
|
|
146
|
-
cellInlineStyle.outlineOffset = '-1px';
|
|
147
|
-
cellInlineStyle.zIndex = '2';
|
|
148
|
-
cellInlineStyle.position = 'relative';
|
|
149
|
-
cellInlineStyle.overflow = 'visible';
|
|
150
|
-
}
|
|
151
|
-
// Apply range/cut highlighting via CSS classes + data attributes for proper precedence
|
|
152
|
-
let highlightClass = '';
|
|
153
|
-
if (descriptor.isInRange) {
|
|
154
|
-
highlightClass = 'ogrid-cell-in-range';
|
|
155
|
-
}
|
|
156
|
-
if (descriptor.isInCutRange) {
|
|
157
|
-
highlightClass = 'ogrid-cell-cut';
|
|
158
|
-
}
|
|
159
|
-
const styledContent = cellStyle
|
|
160
|
-
? h('span', { style: cellStyle }, content)
|
|
161
|
-
: content;
|
|
162
|
-
return h('div', {
|
|
163
|
-
...interactionProps,
|
|
164
|
-
class: highlightClass || undefined,
|
|
165
|
-
style: cellInlineStyle,
|
|
166
|
-
}, [
|
|
167
|
-
styledContent,
|
|
168
|
-
...(descriptor.canEditAny && descriptor.isSelectionEndCell ? [
|
|
169
|
-
h('div', {
|
|
170
|
-
onMousedown: handleFillHandleMouseDown,
|
|
171
|
-
'aria-label': 'Fill handle',
|
|
172
|
-
style: {
|
|
173
|
-
position: 'absolute',
|
|
174
|
-
right: '-3px',
|
|
175
|
-
bottom: '-3px',
|
|
176
|
-
width: '7px',
|
|
177
|
-
height: '7px',
|
|
178
|
-
backgroundColor: 'var(--ogrid-selection, #217346)',
|
|
179
|
-
border: '1px solid var(--ogrid-bg, #fff)',
|
|
180
|
-
borderRadius: '1px',
|
|
181
|
-
cursor: 'crosshair',
|
|
182
|
-
pointerEvents: 'auto',
|
|
183
|
-
zIndex: '3',
|
|
184
|
-
},
|
|
185
|
-
}),
|
|
186
|
-
] : []),
|
|
187
|
-
]);
|
|
188
|
-
};
|
|
189
|
-
// Compute pinning offsets
|
|
190
|
-
const columnWidthsMap = {};
|
|
191
|
-
visibleCols.forEach((col) => {
|
|
192
|
-
columnWidthsMap[col.columnId] = getColumnWidth(col);
|
|
193
|
-
});
|
|
194
|
-
const leftOffsets = pinning.computeLeftOffsets(visibleCols, columnWidthsMap, DEFAULT_MIN_COLUMN_WIDTH, hasCheckboxCol, CHECKBOX_COLUMN_WIDTH);
|
|
195
|
-
const rightOffsets = pinning.computeRightOffsets(visibleCols, columnWidthsMap, DEFAULT_MIN_COLUMN_WIDTH);
|
|
196
|
-
// Build column layouts
|
|
197
|
-
const columnLayouts = visibleCols.map((col, colIdx) => {
|
|
198
|
-
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
199
|
-
const isPinnedLeft = col.pinned === 'left';
|
|
200
|
-
const isPinnedRight = col.pinned === 'right';
|
|
201
|
-
const columnWidth = getColumnWidth(col);
|
|
202
|
-
const tdStyle = {
|
|
203
|
-
position: 'relative',
|
|
204
|
-
padding: '0',
|
|
205
|
-
height: '1px',
|
|
206
|
-
};
|
|
207
|
-
if (isPinnedLeft || (isFreezeCol && colIdx === 0)) {
|
|
208
|
-
tdStyle.position = 'sticky';
|
|
209
|
-
tdStyle.left = `${leftOffsets[col.columnId] ?? 0}px`;
|
|
210
|
-
tdStyle.zIndex = '6';
|
|
211
|
-
tdStyle.backgroundColor = '#fff';
|
|
212
|
-
tdStyle.willChange = 'transform';
|
|
213
|
-
}
|
|
214
|
-
else if (isPinnedRight) {
|
|
215
|
-
tdStyle.position = 'sticky';
|
|
216
|
-
tdStyle.right = `${rightOffsets[col.columnId] ?? 0}px`;
|
|
217
|
-
tdStyle.zIndex = '6';
|
|
218
|
-
tdStyle.backgroundColor = '#fff';
|
|
219
|
-
tdStyle.willChange = 'transform';
|
|
220
|
-
}
|
|
221
|
-
return { col, tdStyle, minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH, width: columnWidth, maxWidth: columnWidth };
|
|
222
|
-
});
|
|
223
|
-
// Build header cell styles
|
|
224
|
-
const getHeaderStyle = (col, colIdx) => {
|
|
225
|
-
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
226
|
-
const isPinnedLeft = col.pinned === 'left';
|
|
227
|
-
const isPinnedRight = col.pinned === 'right';
|
|
228
|
-
const base = { fontWeight: '600', position: 'sticky', top: '0', backgroundColor: 'rgba(0,0,0,0.04)', zIndex: '8' };
|
|
229
|
-
if (isPinnedLeft || (isFreezeCol && colIdx === 0)) {
|
|
230
|
-
const leftOffset = leftOffsets[col.columnId] ?? 0;
|
|
231
|
-
return { ...base, position: 'sticky', left: `${leftOffset}px`, top: '0', zIndex: '10', backgroundColor: 'rgba(0,0,0,0.04)', willChange: 'transform' };
|
|
232
|
-
}
|
|
233
|
-
if (isPinnedRight) {
|
|
234
|
-
const rightOffset = rightOffsets[col.columnId] ?? 0;
|
|
235
|
-
return { ...base, position: 'sticky', right: `${rightOffset}px`, top: '0', zIndex: '10', backgroundColor: 'rgba(0,0,0,0.04)', willChange: 'transform' };
|
|
236
|
-
}
|
|
237
|
-
return base;
|
|
238
|
-
};
|
|
239
|
-
const wrapperStyle = {
|
|
240
|
-
position: 'relative',
|
|
241
|
-
flex: '1',
|
|
242
|
-
minHeight: '0',
|
|
243
|
-
width: fitToContent ? 'fit-content' : '100%',
|
|
244
|
-
maxWidth: '100%',
|
|
245
|
-
overflowX: suppressHorizontalScroll ? 'hidden' : allowOverflowX ? 'auto' : 'hidden',
|
|
246
|
-
overflowY: 'auto',
|
|
247
|
-
backgroundColor: '#fff',
|
|
248
|
-
willChange: 'scroll-position',
|
|
249
|
-
};
|
|
250
|
-
return h('div', { style: { position: 'relative', flex: '1', minHeight: '0', display: 'flex', flexDirection: 'column' } }, [
|
|
251
|
-
// Scrollable wrapper
|
|
252
|
-
h('div', {
|
|
253
|
-
ref: (el) => { wrapperRef.value = el; vsContainerRef.value = el; },
|
|
254
|
-
tabindex: 0,
|
|
255
|
-
role: 'region',
|
|
256
|
-
'aria-label': ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'),
|
|
257
|
-
'aria-labelledby': ariaLabelledBy,
|
|
258
|
-
onMousedown: (e) => { lastMouseShift.value = e.shiftKey; },
|
|
259
|
-
onKeydown: handleGridKeyDown,
|
|
260
|
-
onContextmenu: (e) => e.preventDefault(),
|
|
261
|
-
'data-overflow-x': allowOverflowX ? 'true' : 'false',
|
|
262
|
-
style: wrapperStyle,
|
|
263
|
-
}, [
|
|
264
|
-
h('div', { style: { display: 'flex', flexDirection: 'column', minHeight: '100%' } }, [
|
|
265
|
-
h('div', { style: { minWidth: allowOverflowX ? `${minTableWidth}px` : undefined } }, [
|
|
266
|
-
h('div', {
|
|
267
|
-
ref: (el) => { tableContainerRef.value = el; },
|
|
268
|
-
style: isLoading && items.length > 0
|
|
269
|
-
? { position: 'relative', opacity: '0.6' }
|
|
270
|
-
: { position: 'relative', opacity: '1' },
|
|
271
|
-
}, [
|
|
272
|
-
// Drop indicator for column reorder
|
|
273
|
-
...(isReorderDragging.value && dropIndicatorX.value !== null ? [
|
|
274
|
-
h('div', {
|
|
275
|
-
style: {
|
|
276
|
-
position: 'absolute',
|
|
277
|
-
top: '0',
|
|
278
|
-
bottom: '0',
|
|
279
|
-
width: '3px',
|
|
280
|
-
background: 'var(--ogrid-primary, #217346)',
|
|
281
|
-
pointerEvents: 'none',
|
|
282
|
-
zIndex: '100',
|
|
283
|
-
left: `${dropIndicatorX.value}px`,
|
|
284
|
-
},
|
|
285
|
-
}),
|
|
286
|
-
] : []),
|
|
287
|
-
// Table
|
|
288
|
-
h('table', {
|
|
289
|
-
ref: (el) => { tableRef.value = el; },
|
|
290
|
-
style: {
|
|
291
|
-
width: '100%',
|
|
292
|
-
borderCollapse: 'collapse',
|
|
293
|
-
minWidth: `${minTableWidth}px`,
|
|
294
|
-
fontSize: '0.875rem',
|
|
295
|
-
},
|
|
296
|
-
'data-freeze-rows': freezeRows != null && freezeRows >= 1 ? freezeRows : undefined,
|
|
297
|
-
'data-freeze-cols': freezeCols != null && freezeCols >= 1 ? freezeCols : undefined,
|
|
298
|
-
}, [
|
|
299
|
-
// Header
|
|
300
|
-
h('thead', { style: { zIndex: '8', backgroundColor: 'rgba(0,0,0,0.04)' } }, headerRows.map((row, rowIdx) => h('tr', { key: rowIdx, style: { backgroundColor: 'rgba(0,0,0,0.04)' } }, [
|
|
301
|
-
// Checkbox header cell (last leaf row only)
|
|
302
|
-
...(rowIdx === headerRows.length - 1 && hasCheckboxCol ? [
|
|
303
|
-
h('th', {
|
|
304
|
-
style: {
|
|
305
|
-
width: `${CHECKBOX_COLUMN_WIDTH}px`,
|
|
306
|
-
minWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
|
|
307
|
-
maxWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
|
|
308
|
-
textAlign: 'center',
|
|
309
|
-
padding: '4px',
|
|
310
|
-
},
|
|
311
|
-
}, h(VCheckbox, {
|
|
312
|
-
modelValue: allSelected,
|
|
313
|
-
indeterminate: someSelected,
|
|
314
|
-
hideDetails: true,
|
|
315
|
-
density: 'compact',
|
|
316
|
-
'aria-label': 'Select all rows',
|
|
317
|
-
'onUpdate:modelValue': (c) => handleSelectAll(!!c),
|
|
318
|
-
})),
|
|
319
|
-
] : []),
|
|
320
|
-
// Empty placeholder for checkbox in first group row
|
|
321
|
-
...(rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol ? [
|
|
322
|
-
h('th', {
|
|
323
|
-
rowSpan: headerRows.length - 1,
|
|
324
|
-
style: { width: `${CHECKBOX_COLUMN_WIDTH}px`, minWidth: `${CHECKBOX_COLUMN_WIDTH}px`, padding: '0' },
|
|
325
|
-
}),
|
|
326
|
-
] : []),
|
|
327
|
-
// Row numbers header cell (last leaf row only)
|
|
328
|
-
...(rowIdx === headerRows.length - 1 && hasRowNumbersCol ? [
|
|
329
|
-
h('th', {
|
|
330
|
-
style: {
|
|
331
|
-
width: `${ROW_NUMBER_COLUMN_WIDTH}px`,
|
|
332
|
-
minWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
|
|
333
|
-
maxWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
|
|
334
|
-
textAlign: 'center',
|
|
335
|
-
fontWeight: '600',
|
|
336
|
-
padding: '6px',
|
|
337
|
-
backgroundColor: 'rgba(0,0,0,0.04)',
|
|
338
|
-
color: 'rgba(0,0,0,0.6)',
|
|
339
|
-
},
|
|
340
|
-
}, '#'),
|
|
341
|
-
] : []),
|
|
342
|
-
// Empty placeholder for row numbers in first group row
|
|
343
|
-
...(rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol ? [
|
|
344
|
-
h('th', {
|
|
345
|
-
rowSpan: headerRows.length - 1,
|
|
346
|
-
style: { width: `${ROW_NUMBER_COLUMN_WIDTH}px`, minWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`, padding: '0' },
|
|
347
|
-
}),
|
|
348
|
-
] : []),
|
|
349
|
-
// Header cells
|
|
350
|
-
...row.map((cell, cellIdx) => {
|
|
351
|
-
if (cell.isGroup) {
|
|
352
|
-
return h('th', {
|
|
353
|
-
key: cellIdx,
|
|
354
|
-
colSpan: cell.colSpan,
|
|
355
|
-
scope: 'colgroup',
|
|
356
|
-
style: { textAlign: 'center', fontWeight: '600', borderBottom: '2px solid rgba(0,0,0,0.12)', padding: '6px' },
|
|
357
|
-
}, cell.label);
|
|
358
|
-
}
|
|
359
|
-
const col = cell.columnDef;
|
|
360
|
-
const colIdx = visibleCols.indexOf(col);
|
|
361
|
-
const columnWidth = getColumnWidth(col);
|
|
362
|
-
const headerStyle = getHeaderStyle(col, colIdx);
|
|
363
|
-
return h('th', {
|
|
364
|
-
key: col.columnId,
|
|
365
|
-
scope: 'col',
|
|
366
|
-
'data-column-id': col.columnId,
|
|
367
|
-
rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined,
|
|
368
|
-
style: {
|
|
369
|
-
...headerStyle,
|
|
370
|
-
cursor: isReorderDragging.value ? 'grabbing' : 'grab',
|
|
371
|
-
minWidth: `${col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH}px`,
|
|
372
|
-
width: `${columnWidth}px`,
|
|
373
|
-
maxWidth: `${columnWidth}px`,
|
|
374
|
-
},
|
|
375
|
-
onMousedown: (e) => handleReorderMouseDown(col.columnId, e),
|
|
376
|
-
}, [
|
|
377
|
-
h('div', { style: { display: 'flex', alignItems: 'center', width: '100%' } }, [
|
|
378
|
-
h(ColumnHeaderFilter, getHeaderFilterConfig(col, headerFilterInput)),
|
|
379
|
-
h('button', {
|
|
380
|
-
onClick: (e) => {
|
|
381
|
-
e.stopPropagation();
|
|
382
|
-
headerMenu.open(col.columnId, e.currentTarget);
|
|
383
|
-
},
|
|
384
|
-
'aria-label': 'Column options',
|
|
385
|
-
title: 'Column options',
|
|
386
|
-
style: {
|
|
387
|
-
background: 'none',
|
|
388
|
-
border: 'none',
|
|
389
|
-
cursor: 'pointer',
|
|
390
|
-
padding: '2px 4px',
|
|
391
|
-
fontSize: '14px',
|
|
392
|
-
color: 'rgba(0,0,0,0.5)',
|
|
393
|
-
lineHeight: '1',
|
|
394
|
-
flexShrink: '0',
|
|
395
|
-
},
|
|
396
|
-
}, '\u22EE'),
|
|
397
|
-
]),
|
|
398
|
-
h('div', {
|
|
399
|
-
onMousedown: (e) => { e.stopPropagation(); handleResizeStart(e, col); },
|
|
400
|
-
style: {
|
|
401
|
-
position: 'absolute',
|
|
402
|
-
top: '0',
|
|
403
|
-
right: '-3px',
|
|
404
|
-
bottom: '0',
|
|
405
|
-
width: '8px',
|
|
406
|
-
cursor: 'col-resize',
|
|
407
|
-
userSelect: 'none',
|
|
408
|
-
},
|
|
409
|
-
}),
|
|
410
|
-
]);
|
|
411
|
-
}),
|
|
412
|
-
]))),
|
|
413
|
-
// Body
|
|
414
|
-
...(!showEmptyInGrid ? [
|
|
415
|
-
h('tbody', {}, (() => {
|
|
416
|
-
const vsEnabled = virtualScrollEnabled.value;
|
|
417
|
-
const vr = visibleRange.value;
|
|
418
|
-
const startIdx = vsEnabled ? vr.startIndex : 0;
|
|
419
|
-
const endIdx = vsEnabled ? Math.min(vr.endIndex, items.length - 1) : items.length - 1;
|
|
420
|
-
const rows = [];
|
|
421
|
-
// Top spacer for virtual scrolling
|
|
422
|
-
if (vsEnabled && vr.offsetTop > 0) {
|
|
423
|
-
rows.push(h('tr', { key: '__vs-top', style: { height: `${vr.offsetTop}px` } }));
|
|
424
|
-
}
|
|
425
|
-
for (let rowIndex = startIdx; rowIndex <= endIdx; rowIndex++) {
|
|
426
|
-
const item = items[rowIndex];
|
|
427
|
-
if (!item)
|
|
428
|
-
continue;
|
|
429
|
-
const rowIdStr = getRowId(item);
|
|
430
|
-
const isSelected = selectedRowIds.has(rowIdStr);
|
|
431
|
-
rows.push(h('tr', {
|
|
432
|
-
key: rowIdStr,
|
|
433
|
-
'data-row-id': rowIdStr,
|
|
434
|
-
onClick: handleSingleRowClick,
|
|
435
|
-
style: { cursor: rowSelection === 'single' ? 'pointer' : undefined },
|
|
436
|
-
}, [
|
|
437
|
-
// Checkbox cell
|
|
438
|
-
...(hasCheckboxCol ? [
|
|
439
|
-
h('td', {
|
|
440
|
-
style: {
|
|
441
|
-
width: `${CHECKBOX_COLUMN_WIDTH}px`,
|
|
442
|
-
minWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
|
|
443
|
-
maxWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
|
|
444
|
-
textAlign: 'center',
|
|
445
|
-
padding: '0',
|
|
446
|
-
},
|
|
447
|
-
}, h('div', {
|
|
448
|
-
'data-row-index': rowIndex,
|
|
449
|
-
'data-col-index': 0,
|
|
450
|
-
onClick: (e) => e.stopPropagation(),
|
|
451
|
-
style: { display: 'flex', alignItems: 'center', justifyContent: 'center' },
|
|
452
|
-
}, h(VCheckbox, {
|
|
453
|
-
modelValue: isSelected,
|
|
454
|
-
hideDetails: true,
|
|
455
|
-
density: 'compact',
|
|
456
|
-
'aria-label': `Select row ${rowIndex + 1}`,
|
|
457
|
-
'onUpdate:modelValue': (checked) => handleRowCheckboxChange(rowIdStr, checked, rowIndex, lastMouseShift.value),
|
|
458
|
-
}))),
|
|
459
|
-
] : []),
|
|
460
|
-
// Row numbers cell
|
|
461
|
-
...(hasRowNumbersCol ? [
|
|
462
|
-
h('td', {
|
|
463
|
-
style: {
|
|
464
|
-
width: `${ROW_NUMBER_COLUMN_WIDTH}px`,
|
|
465
|
-
minWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
|
|
466
|
-
maxWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
|
|
467
|
-
textAlign: 'center',
|
|
468
|
-
fontWeight: '600',
|
|
469
|
-
fontVariantNumeric: 'tabular-nums',
|
|
470
|
-
padding: '6px',
|
|
471
|
-
backgroundColor: 'rgba(0,0,0,0.04)',
|
|
472
|
-
color: 'rgba(0,0,0,0.6)',
|
|
473
|
-
},
|
|
474
|
-
}, String(rowNumberOffset + rowIndex + 1)),
|
|
475
|
-
] : []),
|
|
476
|
-
// Data cells
|
|
477
|
-
...columnLayouts.map((cl, colIdx) => h('td', {
|
|
478
|
-
key: cl.col.columnId,
|
|
479
|
-
style: {
|
|
480
|
-
...cl.tdStyle,
|
|
481
|
-
minWidth: `${cl.minWidth}px`,
|
|
482
|
-
width: `${cl.width}px`,
|
|
483
|
-
maxWidth: `${cl.maxWidth}px`,
|
|
484
|
-
},
|
|
485
|
-
}, [renderCellContent(item, cl.col, rowIndex, colIdx)])),
|
|
486
|
-
]));
|
|
487
|
-
}
|
|
488
|
-
// Bottom spacer for virtual scrolling
|
|
489
|
-
if (vsEnabled && vr.offsetBottom > 0) {
|
|
490
|
-
rows.push(h('tr', { key: '__vs-bottom', style: { height: `${vr.offsetBottom}px` } }));
|
|
491
|
-
}
|
|
492
|
-
return rows;
|
|
493
|
-
})()),
|
|
494
|
-
] : []),
|
|
495
|
-
]),
|
|
496
|
-
// Empty state
|
|
497
|
-
...(showEmptyInGrid && p.emptyState ? [
|
|
498
|
-
h('div', {
|
|
499
|
-
style: {
|
|
500
|
-
padding: '32px 16px',
|
|
501
|
-
textAlign: 'center',
|
|
502
|
-
borderTop: '1px solid rgba(0,0,0,0.12)',
|
|
503
|
-
backgroundColor: 'rgba(0,0,0,0.04)',
|
|
504
|
-
},
|
|
505
|
-
}, p.emptyState.render
|
|
506
|
-
? [p.emptyState.render()]
|
|
507
|
-
: [
|
|
508
|
-
h('div', { style: { fontSize: '1.25rem', fontWeight: '600', marginBottom: '8px' } }, 'No results found'),
|
|
509
|
-
h('div', { style: { fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' } }, p.emptyState.message != null
|
|
510
|
-
? String(p.emptyState.message)
|
|
511
|
-
: p.emptyState.hasActiveFilters
|
|
512
|
-
? [
|
|
513
|
-
'No items match your current filters. Try adjusting your search or ',
|
|
514
|
-
h(VBtn, {
|
|
515
|
-
variant: 'text',
|
|
516
|
-
size: 'small',
|
|
517
|
-
onClick: p.emptyState.onClearAll,
|
|
518
|
-
}, () => 'clear all filters'),
|
|
519
|
-
' to see all items.',
|
|
520
|
-
]
|
|
521
|
-
: 'There are no items available at this time.'),
|
|
522
|
-
]),
|
|
523
|
-
] : []),
|
|
524
|
-
]),
|
|
525
|
-
]),
|
|
526
|
-
]),
|
|
527
|
-
]),
|
|
528
|
-
// Context menu (teleported to body)
|
|
529
|
-
...(menuPosition ? [
|
|
530
|
-
h(Teleport, { to: 'body' }, h(GridContextMenu, {
|
|
531
|
-
x: menuPosition.x,
|
|
532
|
-
y: menuPosition.y,
|
|
533
|
-
hasSelection: hasCellSelection,
|
|
534
|
-
canUndo,
|
|
535
|
-
canRedo,
|
|
536
|
-
onUndo: onUndo ?? NOOP,
|
|
537
|
-
onRedo: onRedo ?? NOOP,
|
|
538
|
-
onCopy: handleCopy,
|
|
539
|
-
onCut: handleCut,
|
|
540
|
-
onPaste: () => { void handlePaste(); },
|
|
541
|
-
onSelectAll: handleSelectAllCells,
|
|
542
|
-
onClose: closeContextMenu,
|
|
543
|
-
})),
|
|
544
|
-
] : []),
|
|
545
|
-
// Column header menu
|
|
546
|
-
h(ColumnHeaderMenu, {
|
|
547
|
-
isOpen: headerMenu.isOpen,
|
|
548
|
-
anchorElement: headerMenu.anchorElement,
|
|
549
|
-
onClose: headerMenu.close,
|
|
550
|
-
onPinLeft: headerMenu.handlePinLeft,
|
|
551
|
-
onPinRight: headerMenu.handlePinRight,
|
|
552
|
-
onUnpin: headerMenu.handleUnpin,
|
|
553
|
-
onSortAsc: headerMenu.handleSortAsc,
|
|
554
|
-
onSortDesc: headerMenu.handleSortDesc,
|
|
555
|
-
onClearSort: headerMenu.handleClearSort,
|
|
556
|
-
onAutosizeThis: headerMenu.handleAutosizeThis,
|
|
557
|
-
onAutosizeAll: headerMenu.handleAutosizeAll,
|
|
558
|
-
canPinLeft: headerMenu.canPinLeft,
|
|
559
|
-
canPinRight: headerMenu.canPinRight,
|
|
560
|
-
canUnpin: headerMenu.canUnpin,
|
|
561
|
-
currentSort: headerMenu.currentSort,
|
|
562
|
-
isSortable: headerMenu.isSortable,
|
|
563
|
-
isResizable: headerMenu.isResizable,
|
|
564
|
-
}),
|
|
565
|
-
// Status bar
|
|
566
|
-
...(statusBarConfig ? [
|
|
567
|
-
h(StatusBar, {
|
|
568
|
-
totalCount: statusBarConfig.totalCount,
|
|
569
|
-
filteredCount: statusBarConfig.filteredCount,
|
|
570
|
-
selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size,
|
|
571
|
-
selectedCellCount: selectionRange
|
|
572
|
-
? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1)
|
|
573
|
-
: undefined,
|
|
574
|
-
aggregation: statusBarConfig.aggregation,
|
|
575
|
-
suppressRowCount: statusBarConfig.suppressRowCount,
|
|
576
|
-
}),
|
|
577
|
-
] : []),
|
|
578
|
-
// Loading overlay
|
|
579
|
-
...(isLoading ? [
|
|
580
|
-
h('div', {
|
|
581
|
-
style: {
|
|
582
|
-
position: 'absolute',
|
|
583
|
-
inset: '0',
|
|
584
|
-
zIndex: '2',
|
|
585
|
-
display: 'flex',
|
|
586
|
-
alignItems: 'center',
|
|
587
|
-
justifyContent: 'center',
|
|
588
|
-
backgroundColor: 'rgba(255,255,255,0.7)',
|
|
589
|
-
},
|
|
590
|
-
}, h('div', {
|
|
591
|
-
style: {
|
|
592
|
-
display: 'flex',
|
|
593
|
-
flexDirection: 'column',
|
|
594
|
-
alignItems: 'center',
|
|
595
|
-
gap: '8px',
|
|
596
|
-
padding: '16px',
|
|
597
|
-
backgroundColor: '#fff',
|
|
598
|
-
border: '1px solid rgba(0,0,0,0.12)',
|
|
599
|
-
borderRadius: '4px',
|
|
600
|
-
},
|
|
601
|
-
}, [
|
|
602
|
-
h(VProgressCircular, { size: 24, indeterminate: true }),
|
|
603
|
-
h('span', { style: { fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' } }, loadingMessage),
|
|
604
|
-
])),
|
|
605
|
-
] : []),
|
|
606
|
-
]);
|
|
607
|
-
};
|
|
608
|
-
},
|
|
10
|
+
export const DataGridTable = createDataGridTable({
|
|
11
|
+
renderCheckbox: ({ modelValue, indeterminate, ariaLabel, onChange }) => h(VCheckbox, {
|
|
12
|
+
modelValue,
|
|
13
|
+
indeterminate,
|
|
14
|
+
hideDetails: true,
|
|
15
|
+
density: 'compact',
|
|
16
|
+
'aria-label': ariaLabel,
|
|
17
|
+
'onUpdate:modelValue': (c) => onChange(!!c),
|
|
18
|
+
}),
|
|
19
|
+
renderSpinner: (message) => h('div', { class: 'ogrid-loading-inner' }, [
|
|
20
|
+
h(VProgressCircular, { size: 24, indeterminate: true }),
|
|
21
|
+
h('span', { class: 'ogrid-loading-message' }, message),
|
|
22
|
+
]),
|
|
23
|
+
ColumnHeaderFilter,
|
|
24
|
+
ColumnHeaderMenu,
|
|
25
|
+
InlineCellEditor,
|
|
26
|
+
GridContextMenu,
|
|
27
|
+
renderEmptyState: (emptyState) => renderEmptyState({ emptyState }),
|
|
609
28
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { h } from 'vue';
|
|
2
|
+
import { VBtn } from 'vuetify/components';
|
|
3
|
+
export function renderEmptyState({ emptyState }) {
|
|
4
|
+
return h('div', { class: 'ogrid-empty-state' }, emptyState.render
|
|
5
|
+
? [emptyState.render()]
|
|
6
|
+
: [
|
|
7
|
+
h('div', { class: 'ogrid-empty-state-title' }, 'No results found'),
|
|
8
|
+
h('div', { class: 'ogrid-empty-state-message' }, emptyState.message != null
|
|
9
|
+
? String(emptyState.message)
|
|
10
|
+
: emptyState.hasActiveFilters
|
|
11
|
+
? [
|
|
12
|
+
'No items match your current filters. Try adjusting your search or ',
|
|
13
|
+
h(VBtn, {
|
|
14
|
+
variant: 'text',
|
|
15
|
+
size: 'small',
|
|
16
|
+
onClick: emptyState.onClearAll,
|
|
17
|
+
}, () => 'clear all filters'),
|
|
18
|
+
' to see all items.',
|
|
19
|
+
]
|
|
20
|
+
: 'There are no items available at this time.'),
|
|
21
|
+
]);
|
|
22
|
+
}
|
package/dist/esm/OGrid/OGrid.js
CHANGED
|
@@ -284,7 +284,7 @@ export const OGrid = defineComponent({
|
|
|
284
284
|
style: {
|
|
285
285
|
display: 'flex',
|
|
286
286
|
flexDirection: 'column',
|
|
287
|
-
border: '1px solid rgba(0,0,0,0.12)',
|
|
287
|
+
border: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
|
|
288
288
|
borderRadius: '4px',
|
|
289
289
|
overflow: 'hidden',
|
|
290
290
|
},
|
|
@@ -297,7 +297,7 @@ export const OGrid = defineComponent({
|
|
|
297
297
|
alignItems: 'center',
|
|
298
298
|
justifyContent: 'space-between',
|
|
299
299
|
padding: '8px 12px',
|
|
300
|
-
borderBottom: '1px solid rgba(0,0,0,0.12)',
|
|
300
|
+
borderBottom: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
|
|
301
301
|
gap: '8px',
|
|
302
302
|
},
|
|
303
303
|
}, [
|
|
@@ -308,7 +308,7 @@ export const OGrid = defineComponent({
|
|
|
308
308
|
// Below toolbar strip
|
|
309
309
|
...(layout.value.toolbarBelow ? [
|
|
310
310
|
h('div', {
|
|
311
|
-
style: { padding: '8px 12px', borderBottom: '1px solid rgba(0,0,0,0.12)' },
|
|
311
|
+
style: { padding: '8px 12px', borderBottom: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))' },
|
|
312
312
|
}, [layout.value.toolbarBelow]),
|
|
313
313
|
] : []),
|
|
314
314
|
// Main content area (sidebar + grid)
|
|
@@ -319,7 +319,7 @@ export const OGrid = defineComponent({
|
|
|
319
319
|
display: 'flex',
|
|
320
320
|
alignItems: 'center',
|
|
321
321
|
padding: '8px 0',
|
|
322
|
-
borderTop: '1px solid rgba(0,0,0,0.12)',
|
|
322
|
+
borderTop: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
|
|
323
323
|
},
|
|
324
324
|
}, [paginationNode]),
|
|
325
325
|
]);
|
|
@@ -37,7 +37,7 @@ export const PaginationControls = defineComponent({
|
|
|
37
37
|
}, [
|
|
38
38
|
// Summary text
|
|
39
39
|
h('span', {
|
|
40
|
-
style: { fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' },
|
|
40
|
+
style: { fontSize: '0.875rem', color: 'var(--ogrid-fg-secondary, rgba(0,0,0,0.6))' },
|
|
41
41
|
}, `Showing ${startItem} to ${endItem} of ${props.totalCount.toLocaleString()} ${label}`),
|
|
42
42
|
// Page buttons
|
|
43
43
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: '4px' } }, [
|
|
@@ -68,7 +68,7 @@ export const PaginationControls = defineComponent({
|
|
|
68
68
|
style: { minWidth: '32px' },
|
|
69
69
|
onClick: () => props.onPageChange(1),
|
|
70
70
|
}, () => '1'),
|
|
71
|
-
h('span', { style: { margin: '0 4px', color: 'rgba(0,0,0,0.6)' }, 'aria-hidden': 'true' }, '\u2026'),
|
|
71
|
+
h('span', { style: { margin: '0 4px', color: 'var(--ogrid-fg-secondary, rgba(0,0,0,0.6))' }, 'aria-hidden': 'true' }, '\u2026'),
|
|
72
72
|
] : []),
|
|
73
73
|
// Page numbers
|
|
74
74
|
...pageNumbers.map((pageNum) => h(VBtn, {
|
|
@@ -83,7 +83,7 @@ export const PaginationControls = defineComponent({
|
|
|
83
83
|
}, () => String(pageNum))),
|
|
84
84
|
// End ellipsis
|
|
85
85
|
...(showEndEllipsis ? [
|
|
86
|
-
h('span', { style: { margin: '0 4px', color: 'rgba(0,0,0,0.6)' }, 'aria-hidden': 'true' }, '\u2026'),
|
|
86
|
+
h('span', { style: { margin: '0 4px', color: 'var(--ogrid-fg-secondary, rgba(0,0,0,0.6))' }, 'aria-hidden': 'true' }, '\u2026'),
|
|
87
87
|
h(VBtn, {
|
|
88
88
|
size: 'small',
|
|
89
89
|
variant: 'outlined',
|
|
@@ -113,7 +113,7 @@ export const PaginationControls = defineComponent({
|
|
|
113
113
|
]),
|
|
114
114
|
// Page size selector
|
|
115
115
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, [
|
|
116
|
-
h('span', { style: { fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' } }, 'Rows'),
|
|
116
|
+
h('span', { style: { fontSize: '0.875rem', color: 'var(--ogrid-fg-secondary, rgba(0,0,0,0.6))' } }, 'Rows'),
|
|
117
117
|
h(VSelect, {
|
|
118
118
|
modelValue: props.pageSize,
|
|
119
119
|
items: v.pageSizeOptions,
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import { type PropType, type VNode } from 'vue';
|
|
2
|
-
import { type IOGridDataGridProps } from '@alaarab/ogrid-vue';
|
|
3
1
|
import './DataGridTable.css';
|
|
4
2
|
export declare const DataGridTable: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
5
3
|
gridProps: {
|
|
6
|
-
type: PropType<IOGridDataGridProps<unknown>>;
|
|
4
|
+
type: import("vue").PropType<import("@alaarab/ogrid-vue").IOGridDataGridProps<unknown>>;
|
|
7
5
|
required: true;
|
|
8
6
|
};
|
|
9
|
-
}>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
7
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
10
8
|
[key: string]: any;
|
|
11
9
|
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
12
10
|
gridProps: {
|
|
13
|
-
type: PropType<IOGridDataGridProps<unknown>>;
|
|
11
|
+
type: import("vue").PropType<import("@alaarab/ogrid-vue").IOGridDataGridProps<unknown>>;
|
|
14
12
|
required: true;
|
|
15
13
|
};
|
|
16
14
|
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type VNode } from 'vue';
|
|
2
|
+
interface EmptyStateProps {
|
|
3
|
+
emptyState: {
|
|
4
|
+
render?: () => unknown;
|
|
5
|
+
message?: string | null;
|
|
6
|
+
hasActiveFilters?: boolean;
|
|
7
|
+
onClearAll?: () => void;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function renderEmptyState({ emptyState }: EmptyStateProps): VNode;
|
|
11
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-vue-vuetify",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
4
4
|
"description": "OGrid Vuetify – Vuetify-based data grid with sorting, filtering, pagination, column chooser, and CSV export.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"node": ">=18"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@alaarab/ogrid-vue": "2.0.
|
|
40
|
+
"@alaarab/ogrid-vue": "2.0.13"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"vue": "^3.3.0",
|