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