webcake-ui-kit 1.1.28 → 1.1.29
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/package.json +1 -1
- package/src/components/alert/Alert.vue +3 -2
- package/src/components/alert/alert.css +29 -3
- package/src/components/dropdown/Dropdown.vue +14 -2
- package/src/components/dropdown/DropdownMenuItems.vue +7 -3
- package/src/components/dropdown/dropdown-menu-items.css +4 -11
- package/src/components/input/input.css +2 -2
- package/src/components/menu/Menu.vue +7 -2
- package/src/components/select/Select.vue +5 -1
- package/src/components/select/select.css +13 -7
- package/src/components/select-option/SelectOption.vue +6 -0
- package/src/components/select-option/select_option.css +20 -0
- package/src/components/table/Table.vue +424 -91
- package/src/components/table/table.css +162 -5
- package/src/components/tooltip/Tooltip.vue +6 -3
- package/src/floating/click-outside-mixin.js +21 -1
- package/src/styles/color_general.css +20 -12
- package/src/styles/raw_colors.css +4 -4
- package/src/styles/shadow.css +3 -0
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
:class="[
|
|
3
|
+
:class="[
|
|
4
|
+
'ui-table',
|
|
5
|
+
bordered && 'ui-table--bordered',
|
|
6
|
+
isFixedLayout && 'ui-table--scroll-y',
|
|
7
|
+
resizeState && 'ui-table--resizing'
|
|
8
|
+
]"
|
|
4
9
|
:style="containerStyle"
|
|
5
10
|
>
|
|
6
11
|
<table class="ui-table__table" :style="tableStyle">
|
|
7
12
|
<colgroup>
|
|
13
|
+
<col v-if="hasDrag" class="ui-table__col ui-table__col--drag" />
|
|
8
14
|
<col v-if="hasSelection" class="ui-table__col ui-table__col--selection" />
|
|
9
15
|
<col v-for="col in normalizedColumns" :key="col._key" :style="col._style" />
|
|
10
16
|
</colgroup>
|
|
11
|
-
<thead class="ui-table__head">
|
|
17
|
+
<thead ref="tableHead" class="ui-table__head">
|
|
12
18
|
<tr
|
|
13
19
|
:class="['ui-table__row', 'ui-table__row--head', scrollbarWidth > 0 && 'ui-table__row--has-scrollbar']"
|
|
14
|
-
:style="{ height: toCssSize(headerHeight) }"
|
|
20
|
+
:style="[{ height: toCssSize(headerHeight) }, rowScrollStyle]"
|
|
15
21
|
>
|
|
22
|
+
<th
|
|
23
|
+
v-if="hasDrag"
|
|
24
|
+
:class="['ui-table__cell', 'ui-table__th', 'ui-table__cell--drag', 'ui-table__cell--fixed-left']"
|
|
25
|
+
:style="[headCellStyle(0), { left: '0px' }]"
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
/>
|
|
16
28
|
<th
|
|
17
29
|
v-if="hasSelection"
|
|
18
|
-
class="
|
|
19
|
-
|
|
30
|
+
:class="[
|
|
31
|
+
'ui-table__cell',
|
|
32
|
+
'ui-table__th',
|
|
33
|
+
'ui-table__cell--selection',
|
|
34
|
+
'ui-table__cell--fixed-left',
|
|
35
|
+
!prefixStickyLeft && 'ui-table__cell--fixed-left-last'
|
|
36
|
+
]"
|
|
37
|
+
:style="[headCellStyle(hasDrag ? 1 : 0), { left: selectionStickyOffset + 'px' }]"
|
|
20
38
|
>
|
|
21
39
|
<WkCheckbox :checked="allSelected" :disabled="!rows.length" @change="onToggleAll" />
|
|
22
40
|
</th>
|
|
@@ -26,40 +44,55 @@
|
|
|
26
44
|
:class="[
|
|
27
45
|
'ui-table__cell',
|
|
28
46
|
'ui-table__th',
|
|
47
|
+
'ui-table__th-flex',
|
|
29
48
|
`ui-table__cell--${col._align}`,
|
|
30
|
-
col._isSortable && 'ui-table__th--sortable'
|
|
49
|
+
col._isSortable && 'ui-table__th--sortable',
|
|
50
|
+
col._fixed && `ui-table__cell--fixed-${col._fixed}`,
|
|
51
|
+
col._isLastFixedLeft && 'ui-table__cell--fixed-left-last',
|
|
52
|
+
col._isFirstFixedRight && 'ui-table__cell--fixed-right-first'
|
|
53
|
+
]"
|
|
54
|
+
:style="[
|
|
55
|
+
headCellStyle((hasDrag ? 1 : 0) + (hasSelection ? 1 : 0) + i, col),
|
|
56
|
+
col._fixed === 'left' ? { [col._fixed]: col._stickyOffset + 'px' } : {},
|
|
57
|
+
col._fixed === 'right' ? { [col._fixed]: col._stickyOffset + 'px' } : {}
|
|
31
58
|
]"
|
|
32
|
-
:style="headCellStyle(hasSelection ? i + 1 : i, col)"
|
|
33
59
|
:tabindex="col._isSortable ? 0 : undefined"
|
|
34
60
|
:aria-sort="col._ariaSort"
|
|
35
61
|
@click="onHeaderClick(col)"
|
|
36
62
|
@keydown.enter.prevent="onHeaderClick(col)"
|
|
37
63
|
@keydown.space.prevent="onHeaderClick(col)"
|
|
38
|
-
v-bind="
|
|
39
|
-
v-on="
|
|
64
|
+
v-bind="col._headerAttrs"
|
|
65
|
+
v-on="col._headerListeners"
|
|
40
66
|
>
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
<div class="ui-table__th-inner-wrap">
|
|
68
|
+
<span class="ui-table__th-inner">
|
|
69
|
+
<slot name="headerCell" :column="col">{{ col.title }}</slot>
|
|
70
|
+
<span
|
|
71
|
+
v-if="col._isSortable"
|
|
72
|
+
:class="['ui-table__sort', col._sortState ? 'ui-table__sort--active' : 'ui-table__sort--inactive']"
|
|
73
|
+
aria-hidden="true"
|
|
74
|
+
>
|
|
75
|
+
<WkiArrowUp v-if="col._sortState === 'ascend'" :size="16" />
|
|
76
|
+
<WkiArrowDown v-else-if="col._sortState === 'descend'" :size="16" />
|
|
77
|
+
<WkiArrowDownUp v-else :size="16" />
|
|
78
|
+
</span>
|
|
51
79
|
</span>
|
|
52
|
-
|
|
80
|
+
<div
|
|
81
|
+
v-if="col._resizable"
|
|
82
|
+
:class="['ui-table__resize-handle', col._fixed === 'right' && 'ui-table__resize-handle--left']"
|
|
83
|
+
@pointerdown.stop.prevent="onResizeStart($event, col, i)"
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
53
86
|
</th>
|
|
54
87
|
<th
|
|
55
88
|
v-if="scrollbarWidth > 0"
|
|
56
89
|
class="ui-table__th--scrollbar"
|
|
57
|
-
:style="{ width: scrollbarWidth + 'px' }"
|
|
90
|
+
:style="{ width: scrollbarWidth + 'px', position: 'sticky', right: '0px' }"
|
|
58
91
|
aria-hidden="true"
|
|
59
92
|
/>
|
|
60
93
|
</tr>
|
|
61
94
|
</thead>
|
|
62
|
-
<tbody class="ui-table__body" :style="bodyStyle" @scroll="onBodyScroll">
|
|
95
|
+
<tbody ref="tableBody" class="ui-table__body" :style="bodyStyle" @scroll="onBodyScroll" @dragleave="onDragLeave">
|
|
63
96
|
<tr v-if="!rows.length" class="ui-table__row ui-table__row--empty">
|
|
64
97
|
<td class="ui-table__cell ui-table__empty-cell" :colspan="totalColumns">
|
|
65
98
|
<slot name="emptyText">
|
|
@@ -73,24 +106,51 @@
|
|
|
73
106
|
<td class="ui-table__spacer-cell" :colspan="totalColumns" />
|
|
74
107
|
</tr>
|
|
75
108
|
<tr
|
|
76
|
-
v-for="
|
|
77
|
-
:key="
|
|
109
|
+
v-for="row in mappedRenderRows"
|
|
110
|
+
:key="row._key"
|
|
111
|
+
:ref="'row-' + row._key"
|
|
78
112
|
:class="[
|
|
79
113
|
'ui-table__row',
|
|
80
114
|
'ui-table__row--body',
|
|
81
|
-
|
|
115
|
+
row._selected && 'ui-table__row--selected',
|
|
116
|
+
hasDrag && dragState && row._index === dragState.fromIndex && 'ui-table__row--dragging',
|
|
117
|
+
hasDrag &&
|
|
118
|
+
row._index === dragOverIndex &&
|
|
119
|
+
dragState &&
|
|
120
|
+
row._index !== dragState.fromIndex &&
|
|
121
|
+
'ui-table__row--drag-over'
|
|
82
122
|
]"
|
|
83
|
-
:style="{ height: toCssSize(rowHeight) }"
|
|
84
|
-
v-bind="
|
|
85
|
-
v-on="
|
|
86
|
-
@click="hasSelection ? onRowClick(
|
|
123
|
+
:style="[{ height: toCssSize(rowHeight) }, rowScrollStyle]"
|
|
124
|
+
v-bind="row._attrs"
|
|
125
|
+
v-on="row._listeners"
|
|
126
|
+
@click="hasSelection ? onRowClick(row._record, row._index, $event) : undefined"
|
|
127
|
+
@dragover.prevent="onDragOver($event, row._index)"
|
|
128
|
+
@drop.prevent="onDrop($event, row._index)"
|
|
87
129
|
>
|
|
130
|
+
<td
|
|
131
|
+
v-if="hasDrag"
|
|
132
|
+
:class="['ui-table__cell', 'ui-table__td', 'ui-table__cell--drag', 'ui-table__cell--fixed-left']"
|
|
133
|
+
:style="[{ left: '0px' }]"
|
|
134
|
+
draggable="true"
|
|
135
|
+
@dragstart.stop="onDragStart($event, row)"
|
|
136
|
+
@dragend="onDragEnd"
|
|
137
|
+
@click.stop
|
|
138
|
+
>
|
|
139
|
+
<WkiGripVertical :size="16" />
|
|
140
|
+
</td>
|
|
88
141
|
<td
|
|
89
142
|
v-if="hasSelection"
|
|
90
|
-
class="
|
|
91
|
-
|
|
143
|
+
:class="[
|
|
144
|
+
'ui-table__cell',
|
|
145
|
+
'ui-table__td',
|
|
146
|
+
'ui-table__cell--selection',
|
|
147
|
+
'ui-table__cell--fixed-left',
|
|
148
|
+
!prefixStickyLeft && 'ui-table__cell--fixed-left-last'
|
|
149
|
+
]"
|
|
150
|
+
:style="[{ left: selectionStickyOffset + 'px' }]"
|
|
151
|
+
@click.stop="onSelectionCellClick(row._record, row._index, $event)"
|
|
92
152
|
>
|
|
93
|
-
<WkCheckbox :checked="
|
|
153
|
+
<WkCheckbox :checked="row._selected" />
|
|
94
154
|
</td>
|
|
95
155
|
<td
|
|
96
156
|
v-for="col in normalizedColumns"
|
|
@@ -99,13 +159,26 @@
|
|
|
99
159
|
'ui-table__cell',
|
|
100
160
|
'ui-table__td',
|
|
101
161
|
`ui-table__cell--${col._align}`,
|
|
102
|
-
col.ellipsis && 'ui-table__cell--ellipsis'
|
|
162
|
+
col.ellipsis && 'ui-table__cell--ellipsis',
|
|
163
|
+
col._fixed && `ui-table__cell--fixed-${col._fixed}`,
|
|
164
|
+
col._isLastFixedLeft && 'ui-table__cell--fixed-left-last',
|
|
165
|
+
col._isFirstFixedRight && 'ui-table__cell--fixed-right-first'
|
|
166
|
+
]"
|
|
167
|
+
:style="[
|
|
168
|
+
col._style,
|
|
169
|
+
col._fixed
|
|
170
|
+
? { [col._fixed]: col._stickyOffset - (col._fixed === 'right' ? scrollbarWidth : 0) + 'px' }
|
|
171
|
+
: {}
|
|
103
172
|
]"
|
|
104
|
-
:style="col._style"
|
|
105
173
|
>
|
|
106
|
-
<slot
|
|
107
|
-
|
|
108
|
-
|
|
174
|
+
<slot
|
|
175
|
+
name="bodyCell"
|
|
176
|
+
:column="col"
|
|
177
|
+
:record="row._record"
|
|
178
|
+
:text="cellText(col, row._record)"
|
|
179
|
+
:index="row._index"
|
|
180
|
+
>{{ cellText(col, row._record) }}</slot
|
|
181
|
+
>
|
|
109
182
|
</td>
|
|
110
183
|
</tr>
|
|
111
184
|
<!-- Bottom spacer reserves the height of the rows below the window. -->
|
|
@@ -126,9 +199,7 @@
|
|
|
126
199
|
<script>
|
|
127
200
|
import WkCheckbox from '../checkbox/Checkbox.vue'
|
|
128
201
|
import WkEmpty from '../empty/Empty.vue'
|
|
129
|
-
import WkiArrowUp from '../../icons
|
|
130
|
-
import WkiArrowDown from '../../icons/ArrowDown.vue'
|
|
131
|
-
import WkiArrowDownUp from '../../icons/ArrowDownUp.vue'
|
|
202
|
+
import { WkiGripVertical, WkiArrowDown, WkiArrowDownUp, WkiArrowUp } from '../../icons'
|
|
132
203
|
import { toCssSize } from '../../utils/common'
|
|
133
204
|
import { computeVirtualWindow } from '../../utils/virtualScroll'
|
|
134
205
|
|
|
@@ -140,9 +211,11 @@ function defaultCompare(a, b) {
|
|
|
140
211
|
return String(a).localeCompare(String(b))
|
|
141
212
|
}
|
|
142
213
|
|
|
214
|
+
const EMPTY_BINDINGS = Object.freeze({ attrs: {}, on: {} })
|
|
215
|
+
|
|
143
216
|
export default {
|
|
144
217
|
name: 'Table',
|
|
145
|
-
components: { WkCheckbox, WkEmpty,
|
|
218
|
+
components: { WkCheckbox, WkEmpty, WkiGripVertical, WkiArrowDown, WkiArrowDownUp, WkiArrowUp },
|
|
146
219
|
props: {
|
|
147
220
|
// Column descriptors: { title, dataIndex, key?, width?, align?, sorter?, ellipsis? }
|
|
148
221
|
columns: { type: Array, default: () => [] },
|
|
@@ -154,6 +227,7 @@ export default {
|
|
|
154
227
|
bordered: { type: Boolean, default: false },
|
|
155
228
|
// Render a leading checkbox column for row selection.
|
|
156
229
|
rowSelection: { type: Boolean, default: false },
|
|
230
|
+
isDrag: { type: Boolean, default: false },
|
|
157
231
|
// Controlled selection — array of selected row keys.
|
|
158
232
|
selectedRowKeys: { type: Array, default: () => [] },
|
|
159
233
|
// Placeholder shown when dataSource is empty.
|
|
@@ -173,7 +247,7 @@ export default {
|
|
|
173
247
|
// Extra rows rendered above and below the viewport to cushion fast scrolling.
|
|
174
248
|
overscan: { type: Number, default: 8 }
|
|
175
249
|
},
|
|
176
|
-
emits: ['change', 'sort-change', 'update:selectedRowKeys', 'selection-change'],
|
|
250
|
+
emits: ['change', 'sort-change', 'update:selectedRowKeys', 'selection-change', 'drag-record'],
|
|
177
251
|
data() {
|
|
178
252
|
return {
|
|
179
253
|
sortField: null,
|
|
@@ -185,7 +259,11 @@ export default {
|
|
|
185
259
|
scrollTop: 0,
|
|
186
260
|
containerHeight: 0,
|
|
187
261
|
rafId: null,
|
|
188
|
-
toCssSize
|
|
262
|
+
toCssSize,
|
|
263
|
+
resizeState: null,
|
|
264
|
+
columnWidths: {},
|
|
265
|
+
dragState: null,
|
|
266
|
+
dragOverIndex: null
|
|
189
267
|
}
|
|
190
268
|
},
|
|
191
269
|
computed: {
|
|
@@ -193,11 +271,31 @@ export default {
|
|
|
193
271
|
return new Set(this.internalSelected)
|
|
194
272
|
},
|
|
195
273
|
normalizedColumns() {
|
|
196
|
-
|
|
274
|
+
const prefixDomCount = (this.hasDrag ? 1 : 0) + (this.hasSelection ? 1 : 0)
|
|
275
|
+
|
|
276
|
+
const domWidth = domIdx => {
|
|
277
|
+
const dataIdx = domIdx - prefixDomCount
|
|
278
|
+
if (dataIdx >= 0 && dataIdx < this.columns.length) {
|
|
279
|
+
const col = this.columns[dataIdx]
|
|
280
|
+
const key = col.key != null ? col.key : col.dataIndex
|
|
281
|
+
// resize override > DOM measurement > width prop
|
|
282
|
+
if (key != null && this.columnWidths[key] != null) return this.columnWidths[key]
|
|
283
|
+
if (this.colWidths[domIdx] != null) return this.colWidths[domIdx]
|
|
284
|
+
const w = col.width
|
|
285
|
+
return w != null ? (typeof w === 'number' ? w : parseFloat(w) || 0) : 0
|
|
286
|
+
}
|
|
287
|
+
// prefix columns (drag, selection) — DOM only, no prop fallback
|
|
288
|
+
return this.colWidths[domIdx] != null ? this.colWidths[domIdx] : 0
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const cols = this.columns.map((col, i) => {
|
|
197
292
|
const _key = col.key != null ? col.key : col.dataIndex
|
|
198
293
|
const _align = ['left', 'right', 'center'].includes(col.align) ? col.align : 'left'
|
|
294
|
+
const overrideW = this.columnWidths[_key]
|
|
199
295
|
let _style = {}
|
|
200
|
-
if (
|
|
296
|
+
if (overrideW != null) {
|
|
297
|
+
_style = { width: `${overrideW}px` }
|
|
298
|
+
} else if (col.width != null) {
|
|
201
299
|
_style = { width: typeof col.width === 'number' ? `${col.width}px` : col.width }
|
|
202
300
|
}
|
|
203
301
|
const _isSortable = !!col.sorter
|
|
@@ -212,6 +310,10 @@ export default {
|
|
|
212
310
|
if (_sortState === 'ascend') _ariaSort = 'ascending'
|
|
213
311
|
else if (_sortState === 'descend') _ariaSort = 'descending'
|
|
214
312
|
|
|
313
|
+
// Pre-compute header bindings once per column instead of calling
|
|
314
|
+
// customHeaderAttrs / customHeaderListeners every render.
|
|
315
|
+
const _headerBindings = this.customHeader ? this.splitBindings(this.customHeader(col, i)) : EMPTY_BINDINGS
|
|
316
|
+
const _fixed = col.fixed === 'left' ? 'left' : col.fixed === 'right' ? 'right' : null
|
|
215
317
|
return {
|
|
216
318
|
...col,
|
|
217
319
|
_key,
|
|
@@ -220,15 +322,62 @@ export default {
|
|
|
220
322
|
_isSortable,
|
|
221
323
|
_field,
|
|
222
324
|
_sortState,
|
|
223
|
-
_ariaSort
|
|
325
|
+
_ariaSort,
|
|
326
|
+
_headerAttrs: _headerBindings.attrs,
|
|
327
|
+
_headerListeners: _headerBindings.on,
|
|
328
|
+
_fixed,
|
|
329
|
+
_isLastFixedLeft: false,
|
|
330
|
+
_isLastFixedRight: false,
|
|
331
|
+
_resizable: col.resizable != null ? col.resizable : false
|
|
224
332
|
}
|
|
225
333
|
})
|
|
334
|
+
|
|
335
|
+
let leftAccum = 0
|
|
336
|
+
for (let i = 0; i < prefixDomCount; i++) leftAccum += domWidth(i)
|
|
337
|
+
for (let i = 0; i < cols.length; i++) {
|
|
338
|
+
cols[i]._stickyOffset = leftAccum
|
|
339
|
+
leftAccum += domWidth(prefixDomCount + i)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let rightAccum = this.scrollbarWidth
|
|
343
|
+
for (let i = cols.length - 1; i >= 0; i--) {
|
|
344
|
+
if (cols[i]._fixed === 'right') {
|
|
345
|
+
cols[i]._stickyOffset = rightAccum
|
|
346
|
+
rightAccum += domWidth(prefixDomCount + i)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let lastLeftIdx,
|
|
351
|
+
firstRightIdx = -1
|
|
352
|
+
for (let i = 0; i < cols.length; i++) {
|
|
353
|
+
if (cols[i]._fixed === 'left') lastLeftIdx = i
|
|
354
|
+
if (cols[i]._fixed === 'right' && firstRightIdx === -1) firstRightIdx = i
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (lastLeftIdx >= 0) cols[lastLeftIdx]._isLastFixedLeft = true
|
|
358
|
+
if (firstRightIdx >= 0) cols[firstRightIdx]._isFirstFixedRight = true
|
|
359
|
+
|
|
360
|
+
return cols
|
|
226
361
|
},
|
|
227
362
|
hasSelection() {
|
|
228
363
|
return !!this.rowSelection
|
|
229
364
|
},
|
|
365
|
+
hasDrag() {
|
|
366
|
+
return !!this.isDrag
|
|
367
|
+
},
|
|
368
|
+
// True when any data column is fixed-left — prefix columns must also become sticky.
|
|
369
|
+
prefixStickyLeft() {
|
|
370
|
+
return this.normalizedColumns.some(c => c._fixed === 'left')
|
|
371
|
+
},
|
|
372
|
+
hasStickyRight() {
|
|
373
|
+
return this.normalizedColumns.some(c => c._fixed === 'right')
|
|
374
|
+
},
|
|
375
|
+
// Pixel offset of the selection column when sticky (sits after the drag column).
|
|
376
|
+
selectionStickyOffset() {
|
|
377
|
+
return this.hasDrag ? (this.colWidths[0] != null ? this.colWidths[0] : 44) : 0
|
|
378
|
+
},
|
|
230
379
|
totalColumns() {
|
|
231
|
-
return this.columns.length + (this.hasSelection ? 1 : 0)
|
|
380
|
+
return this.columns.length + (this.hasDrag ? 1 : 0) + (this.hasSelection ? 1 : 0)
|
|
232
381
|
},
|
|
233
382
|
rows() {
|
|
234
383
|
const data = this.dataSource.slice()
|
|
@@ -245,22 +394,33 @@ export default {
|
|
|
245
394
|
if (!this.rows.length) return false
|
|
246
395
|
return this.rows.every((record, index) => this.selectedKeysSet.has(this.getRowKey(record, index)))
|
|
247
396
|
},
|
|
248
|
-
|
|
249
|
-
return this.height != null || !!(this.scroll && this.scroll.y != null)
|
|
250
|
-
},
|
|
251
|
-
containerStyle() {
|
|
397
|
+
isFixedLayout() {
|
|
252
398
|
const scroll = this.scroll || {}
|
|
253
|
-
return
|
|
399
|
+
return this.height != null || scroll.y != null || scroll.x != null
|
|
254
400
|
},
|
|
255
|
-
|
|
256
|
-
const scroll = this.scroll || {}
|
|
257
|
-
if (this.height != null) return { height: toCssSize(this.height) }
|
|
258
|
-
if (scroll.y != null) return { maxHeight: toCssSize(scroll.y) }
|
|
401
|
+
containerStyle() {
|
|
259
402
|
return {}
|
|
260
403
|
},
|
|
261
404
|
tableStyle() {
|
|
405
|
+
return {}
|
|
406
|
+
},
|
|
407
|
+
rowScrollStyle() {
|
|
262
408
|
const scroll = this.scroll || {}
|
|
263
|
-
return scroll.x != null ? { minWidth: toCssSize(scroll.x) } : {}
|
|
409
|
+
return scroll.x != null && this.isFixedLayout ? { minWidth: toCssSize(scroll.x) } : {}
|
|
410
|
+
},
|
|
411
|
+
bodyStyle() {
|
|
412
|
+
const scroll = this.scroll || {}
|
|
413
|
+
const style = {}
|
|
414
|
+
if (this.height != null) {
|
|
415
|
+
style.height = toCssSize(this.height)
|
|
416
|
+
} else if (scroll.y != null) {
|
|
417
|
+
style.maxHeight = toCssSize(scroll.y)
|
|
418
|
+
}
|
|
419
|
+
if (this.isFixedLayout) {
|
|
420
|
+
style.overflowY = scroll.y != null || this.height != null ? 'auto' : 'visible'
|
|
421
|
+
style.overflowX = scroll.x != null ? 'auto' : 'hidden'
|
|
422
|
+
}
|
|
423
|
+
return style
|
|
264
424
|
},
|
|
265
425
|
// Numeric row height — virtualization needs a fixed px value. String sizes are
|
|
266
426
|
// parsed (e.g. '40px' -> 40); anything unparseable falls back to the prop default.
|
|
@@ -271,7 +431,8 @@ export default {
|
|
|
271
431
|
},
|
|
272
432
|
// Virtualization is only meaningful inside a bounded, scrollable viewport.
|
|
273
433
|
isVirtual() {
|
|
274
|
-
|
|
434
|
+
// Virtualization is only meaningful inside a bounded vertical viewport.
|
|
435
|
+
return this.virtual && (this.height != null || (this.scroll && this.scroll.y != null)) && this.rows.length > 0
|
|
275
436
|
},
|
|
276
437
|
virtualWindow() {
|
|
277
438
|
return computeVirtualWindow({
|
|
@@ -283,16 +444,41 @@ export default {
|
|
|
283
444
|
})
|
|
284
445
|
},
|
|
285
446
|
// Rows actually committed to the DOM: a slice when virtual, otherwise everything.
|
|
286
|
-
|
|
447
|
+
slicedRenderRows() {
|
|
287
448
|
if (!this.isVirtual) return this.rows
|
|
288
449
|
return this.rows.slice(this.virtualWindow.startIndex, this.virtualWindow.endIndex)
|
|
289
450
|
},
|
|
451
|
+
// Pre-compute key, index, selected state, and customRow bindings for each row.
|
|
452
|
+
// This eliminates repeated method calls (getRowKey, rowIndex, isSelected,
|
|
453
|
+
// customRowAttrs, customRowListeners) inside the v-for template.
|
|
454
|
+
mappedRenderRows() {
|
|
455
|
+
const startIndex = this.isVirtual ? this.virtualWindow.startIndex : 0
|
|
456
|
+
const hasCustomRow = !!this.customRow
|
|
457
|
+
return this.slicedRenderRows.map((record, i) => {
|
|
458
|
+
const index = startIndex + i
|
|
459
|
+
const key = this.getRowKey(record, index)
|
|
460
|
+
const selected = this.selectedKeysSet.has(key)
|
|
461
|
+
const bindings = hasCustomRow ? this.splitBindings(this.customRow(record, index)) : EMPTY_BINDINGS
|
|
462
|
+
return {
|
|
463
|
+
_record: record,
|
|
464
|
+
_index: index,
|
|
465
|
+
_key: key,
|
|
466
|
+
_selected: selected,
|
|
467
|
+
_attrs: bindings.attrs,
|
|
468
|
+
_listeners: bindings.on
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
},
|
|
472
|
+
// Kept as aliases so template references (renderRows) don't break if used externally.
|
|
473
|
+
renderRows() {
|
|
474
|
+
return this.mappedRenderRows
|
|
475
|
+
},
|
|
290
476
|
topSpacerHeight() {
|
|
291
477
|
return this.isVirtual ? this.virtualWindow.offsetY : 0
|
|
292
478
|
},
|
|
293
479
|
bottomSpacerHeight() {
|
|
294
480
|
if (!this.isVirtual) return 0
|
|
295
|
-
const rendered = this.
|
|
481
|
+
const rendered = this.slicedRenderRows.length * this.rowHeightPx
|
|
296
482
|
return Math.max(0, this.virtualWindow.totalHeight - this.virtualWindow.offsetY - rendered)
|
|
297
483
|
}
|
|
298
484
|
},
|
|
@@ -302,11 +488,7 @@ export default {
|
|
|
302
488
|
}
|
|
303
489
|
},
|
|
304
490
|
mounted() {
|
|
305
|
-
this
|
|
306
|
-
window.addEventListener('resize', this.onResize)
|
|
307
|
-
},
|
|
308
|
-
updated() {
|
|
309
|
-
this.$nextTick(this.syncScrollbarGutter)
|
|
491
|
+
this.initResizeObserver()
|
|
310
492
|
},
|
|
311
493
|
// Declare both teardown hooks: Vue 2.7 fires beforeDestroy, Vue 3.4 fires beforeUnmount.
|
|
312
494
|
// The runtime that doesn't recognize a hook simply ignores it.
|
|
@@ -334,52 +516,56 @@ export default {
|
|
|
334
516
|
}
|
|
335
517
|
return { attrs, on }
|
|
336
518
|
},
|
|
337
|
-
customRowAttrs(record, index) {
|
|
338
|
-
return this.customRow ? this.splitBindings(this.customRow(record, index)).attrs : {}
|
|
339
|
-
},
|
|
340
|
-
customRowListeners(record, index) {
|
|
341
|
-
return this.customRow ? this.splitBindings(this.customRow(record, index)).on : {}
|
|
342
|
-
},
|
|
343
|
-
customHeaderAttrs(col, index) {
|
|
344
|
-
return this.customHeader ? this.splitBindings(this.customHeader(col, index)).attrs : {}
|
|
345
|
-
},
|
|
346
|
-
customHeaderListeners(col, index) {
|
|
347
|
-
return this.customHeader ? this.splitBindings(this.customHeader(col, index)).on : {}
|
|
348
|
-
},
|
|
349
519
|
teardown() {
|
|
350
|
-
|
|
520
|
+
if (this._resizeObserver) {
|
|
521
|
+
this._resizeObserver.disconnect()
|
|
522
|
+
this._resizeObserver = null
|
|
523
|
+
}
|
|
351
524
|
if (this.rafId != null) {
|
|
352
525
|
cancelAnimationFrame(this.rafId)
|
|
353
526
|
this.rafId = null
|
|
354
527
|
}
|
|
355
528
|
},
|
|
356
|
-
|
|
357
|
-
|
|
529
|
+
// Use ResizeObserver instead of updated() + window resize to measure scrollbar
|
|
530
|
+
// gutter and container height. This avoids triggering a second render cycle
|
|
531
|
+
// every time Vue updates the DOM.
|
|
532
|
+
initResizeObserver() {
|
|
533
|
+
const body = this.$refs.tableBody
|
|
534
|
+
if (!body) return
|
|
535
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
536
|
+
this.syncScrollbarGutter()
|
|
537
|
+
})
|
|
538
|
+
this._resizeObserver.observe(body)
|
|
539
|
+
// Initial measurement.
|
|
540
|
+
this.$nextTick(this.syncScrollbarGutter)
|
|
358
541
|
},
|
|
359
542
|
onBodyScroll(e) {
|
|
360
543
|
const top = e.target.scrollTop
|
|
544
|
+
const left = e.target.scrollLeft
|
|
361
545
|
// Coalesce bursts of scroll events into a single state update per frame so we
|
|
362
546
|
// re-slice at most once per repaint — keeps 10k+ rows smooth.
|
|
363
547
|
if (this.rafId != null) return
|
|
364
548
|
this.rafId = requestAnimationFrame(() => {
|
|
365
549
|
this.rafId = null
|
|
366
550
|
this.scrollTop = top
|
|
551
|
+
// Sync horizontal scroll to keep the header aligned with the body.
|
|
552
|
+
const head = this.$refs.tableHead
|
|
553
|
+
if (head) head.scrollLeft = left
|
|
367
554
|
})
|
|
368
555
|
},
|
|
369
|
-
// Absolute index of a rendered row within the full (sorted) dataset.
|
|
370
|
-
rowIndex(i) {
|
|
371
|
-
return this.isVirtual ? this.virtualWindow.startIndex + i : i
|
|
372
|
-
},
|
|
373
556
|
syncScrollbarGutter() {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
557
|
+
const body = this.$refs.tableBody
|
|
558
|
+
if (!body) return
|
|
559
|
+
|
|
560
|
+
const hasY = this.height != null || (this.scroll && this.scroll.y != null)
|
|
561
|
+
this.scrollbarWidth = hasY ? body.offsetWidth - body.clientWidth : 0
|
|
562
|
+
|
|
563
|
+
if (hasY) {
|
|
378
564
|
this.containerHeight = body.clientHeight
|
|
379
565
|
}
|
|
380
566
|
|
|
381
|
-
if (this.
|
|
382
|
-
const firstRow =
|
|
567
|
+
if (this.isFixedLayout) {
|
|
568
|
+
const firstRow = body.querySelector('.ui-table__row--body')
|
|
383
569
|
if (firstRow) {
|
|
384
570
|
const cells = Array.from(firstRow.children)
|
|
385
571
|
const widths = cells.map(c => c.getBoundingClientRect().width)
|
|
@@ -397,7 +583,7 @@ export default {
|
|
|
397
583
|
}
|
|
398
584
|
},
|
|
399
585
|
headCellStyle(index, col = null) {
|
|
400
|
-
if (this.
|
|
586
|
+
if (this.isFixedLayout && this.colWidths[index] !== undefined) {
|
|
401
587
|
return { width: `${this.colWidths[index]}px` }
|
|
402
588
|
}
|
|
403
589
|
return col ? col._style : {}
|
|
@@ -477,6 +663,153 @@ export default {
|
|
|
477
663
|
onToggleAll(checked) {
|
|
478
664
|
this.internalSelected = checked ? this.rows.map((record, index) => this.getRowKey(record, index)) : []
|
|
479
665
|
this.emitSelection()
|
|
666
|
+
},
|
|
667
|
+
onDragStart(e, row) {
|
|
668
|
+
this.dragState = { fromIndex: row._index, record: row._record }
|
|
669
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
670
|
+
|
|
671
|
+
const tr = e.currentTarget.closest('tr')
|
|
672
|
+
const rect = tr.getBoundingClientRect()
|
|
673
|
+
const tableEl = this.$el.querySelector('.ui-table__table')
|
|
674
|
+
|
|
675
|
+
// Shallow-clone the full ancestor chain so scoped CSS selectors
|
|
676
|
+
// (e.g. .ui-table--scroll-y .ui-table__row[data-v-xxx]) still match.
|
|
677
|
+
const wrapper = this.$el.cloneNode(false)
|
|
678
|
+
wrapper.style.position = 'fixed'
|
|
679
|
+
wrapper.style.top = '-9999px'
|
|
680
|
+
wrapper.style.left = '-9999px'
|
|
681
|
+
wrapper.style.width = rect.width + 'px'
|
|
682
|
+
wrapper.style.pointerEvents = 'none'
|
|
683
|
+
|
|
684
|
+
const table = tableEl.cloneNode(false)
|
|
685
|
+
const colgroup = tableEl.querySelector('colgroup')
|
|
686
|
+
if (colgroup) table.appendChild(colgroup.cloneNode(true))
|
|
687
|
+
|
|
688
|
+
const tbody = this.$refs.tableBody.cloneNode(false)
|
|
689
|
+
tbody.style.overflow = 'visible'
|
|
690
|
+
tbody.style.maxHeight = 'none'
|
|
691
|
+
tbody.style.height = 'auto'
|
|
692
|
+
|
|
693
|
+
const clonedRow = tr.cloneNode(true)
|
|
694
|
+
clonedRow.classList.remove('ui-table__row--dragging')
|
|
695
|
+
clonedRow.classList.add('ui-table__row--drag-preview')
|
|
696
|
+
|
|
697
|
+
tbody.appendChild(clonedRow)
|
|
698
|
+
table.appendChild(tbody)
|
|
699
|
+
wrapper.appendChild(table)
|
|
700
|
+
document.body.appendChild(wrapper)
|
|
701
|
+
|
|
702
|
+
e.dataTransfer.setDragImage(wrapper, e.clientX - rect.left, e.clientY - rect.top)
|
|
703
|
+
requestAnimationFrame(() => document.body.removeChild(wrapper))
|
|
704
|
+
},
|
|
705
|
+
onDragOver(e, index) {
|
|
706
|
+
if (!this.dragState) return
|
|
707
|
+
this.dragOverIndex = index
|
|
708
|
+
},
|
|
709
|
+
onDragLeave(e) {
|
|
710
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
711
|
+
this.dragOverIndex = null
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
onDrop(e, toIndex) {
|
|
715
|
+
if (!this.dragState) return
|
|
716
|
+
const { fromIndex, record } = this.dragState
|
|
717
|
+
if (fromIndex === toIndex) {
|
|
718
|
+
this.dragState = null
|
|
719
|
+
this.dragOverIndex = null
|
|
720
|
+
return
|
|
721
|
+
}
|
|
722
|
+
// FLIP — snapshot "First" positions before the data changes.
|
|
723
|
+
const firstRects = this.snapshotRowRects()
|
|
724
|
+
this.$emit('drag-record', { record, fromIndex, toIndex }, e)
|
|
725
|
+
this.dragState = null
|
|
726
|
+
this.dragOverIndex = null
|
|
727
|
+
// After Vue re-renders with the new order, run the Invert+Play phase.
|
|
728
|
+
this.$nextTick(() => this.flipAnimateRows(firstRects))
|
|
729
|
+
},
|
|
730
|
+
onDragEnd() {
|
|
731
|
+
this.dragState = null
|
|
732
|
+
this.dragOverIndex = null
|
|
733
|
+
},
|
|
734
|
+
// FLIP helpers -------------------------------------------------------
|
|
735
|
+
snapshotRowRects() {
|
|
736
|
+
const rects = {}
|
|
737
|
+
this.mappedRenderRows.forEach(row => {
|
|
738
|
+
const refName = 'row-' + row._key
|
|
739
|
+
// Vue 2: $refs for v-for are arrays; Vue 3: also arrays
|
|
740
|
+
const els = this.$refs[refName]
|
|
741
|
+
const el = Array.isArray(els) ? els[0] : els
|
|
742
|
+
if (el) {
|
|
743
|
+
// In Vue 2, el is the DOM node. In Vue 3 with <tr>, el is also the DOM node.
|
|
744
|
+
const dom = el.$el || el
|
|
745
|
+
rects[row._key] = dom.getBoundingClientRect()
|
|
746
|
+
}
|
|
747
|
+
})
|
|
748
|
+
return rects
|
|
749
|
+
},
|
|
750
|
+
flipAnimateRows(firstRects) {
|
|
751
|
+
this.mappedRenderRows.forEach(row => {
|
|
752
|
+
const refName = 'row-' + row._key
|
|
753
|
+
const els = this.$refs[refName]
|
|
754
|
+
const el = Array.isArray(els) ? els[0] : els
|
|
755
|
+
if (!el) return
|
|
756
|
+
const dom = el.$el || el
|
|
757
|
+
const first = firstRects[row._key]
|
|
758
|
+
if (!first) return
|
|
759
|
+
const last = dom.getBoundingClientRect()
|
|
760
|
+
const deltaY = first.top - last.top
|
|
761
|
+
if (Math.abs(deltaY) < 1) return
|
|
762
|
+
// Invert: jump the row back to its old position.
|
|
763
|
+
dom.style.transition = 'none'
|
|
764
|
+
dom.style.transform = `translateY(${deltaY}px)`
|
|
765
|
+
// Play: animate from inverted position to final position.
|
|
766
|
+
requestAnimationFrame(() => {
|
|
767
|
+
dom.style.transition = 'transform 0.25s cubic-bezier(0.22, 1, 0.36, 1)'
|
|
768
|
+
dom.style.transform = ''
|
|
769
|
+
const onEnd = () => {
|
|
770
|
+
dom.style.transition = ''
|
|
771
|
+
dom.style.transform = ''
|
|
772
|
+
dom.removeEventListener('transitionend', onEnd)
|
|
773
|
+
}
|
|
774
|
+
dom.addEventListener('transitionend', onEnd, { once: true })
|
|
775
|
+
})
|
|
776
|
+
})
|
|
777
|
+
},
|
|
778
|
+
onResizeStart(e, col, colIndex) {
|
|
779
|
+
const th = e.currentTarget.closest('th')
|
|
780
|
+
this.resizeState = {
|
|
781
|
+
colKey: col._key,
|
|
782
|
+
domIdx: (this.hasDrag ? 1 : 0) + (this.hasSelection ? 1 : 0) + colIndex,
|
|
783
|
+
startX: e.clientX,
|
|
784
|
+
startWidth: th.getBoundingClientRect().width,
|
|
785
|
+
minWidth: typeof col.minWidth === 'number' ? col.minWidth : 40,
|
|
786
|
+
reverse: col._fixed === 'right'
|
|
787
|
+
}
|
|
788
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
789
|
+
document.addEventListener('pointermove', this.onResizeMove)
|
|
790
|
+
document.addEventListener('pointerup', this.onResizeEnd, { once: true })
|
|
791
|
+
},
|
|
792
|
+
onResizeMove(e) {
|
|
793
|
+
if (!this.resizeState) return
|
|
794
|
+
const { colKey, startX, startWidth, minWidth, reverse } = this.resizeState
|
|
795
|
+
const delta = reverse ? startX - e.clientX : e.clientX - startX
|
|
796
|
+
const newWidth = Math.max(minWidth, startWidth + delta)
|
|
797
|
+
// Spread into a new object so Vue detects the change (direct property
|
|
798
|
+
this.columnWidths = { ...this.columnWidths, [colKey]: newWidth }
|
|
799
|
+
|
|
800
|
+
// colWidths is stale during drag — patch the domIdx slot so sticky
|
|
801
|
+
// offsets for columns AFTER this one are also correct immediately.
|
|
802
|
+
const domIdx = this.resizeState.domIdx
|
|
803
|
+
const next = this.colWidths.slice()
|
|
804
|
+
next[domIdx] = newWidth
|
|
805
|
+
this.colWidths = next
|
|
806
|
+
// Re-measure so colWidths reflects the final DOM state.
|
|
807
|
+
this.$nextTick(this.syncScrollbarGutter)
|
|
808
|
+
},
|
|
809
|
+
|
|
810
|
+
onResizeEnd() {
|
|
811
|
+
document.removeEventListener('pointermove', this.onResizeMove)
|
|
812
|
+
this.resizeState = null
|
|
480
813
|
}
|
|
481
814
|
}
|
|
482
815
|
}
|