webcake-ui-kit 1.2.12 → 1.2.14

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.
Files changed (36) hide show
  1. package/package.json +1 -2
  2. package/src/components/date-picker/date-picker.css +3 -1
  3. package/src/components/dropdown/Dropdown.vue +2 -2
  4. package/src/components/item/item.css +1 -0
  5. package/src/components/select-option/SelectOption.vue +2 -1
  6. package/src/components/select-option/select_option.css +0 -62
  7. package/src/components/select-option/select_option_sub.css +61 -0
  8. package/src/components/{table-v2 → table}/ColumnDragHandle.vue +2 -2
  9. package/src/components/table/Table.vue +250 -772
  10. package/src/components/{table-v2 → table}/TableColGroup.vue +2 -2
  11. package/src/components/{table-v2 → table}/TableEmpty.vue +2 -2
  12. package/src/components/{table-v2 → table}/TableLoading.vue +3 -3
  13. package/src/components/{table-v2 → table}/TableWrapper.vue +14 -9
  14. package/src/components/{table-v2 → table}/body/DragPreview.vue +4 -18
  15. package/src/components/{table-v2 → table}/body/TableBody.vue +6 -6
  16. package/src/components/{table-v2 → table}/body/TableCell.vue +8 -8
  17. package/src/components/{table-v2 → table}/body/TableRow.vue +4 -4
  18. package/src/components/{table-v2 → table}/header/TableHeader.vue +2 -2
  19. package/src/components/{table-v2 → table}/header/TableHeaderCell.vue +9 -9
  20. package/src/components/{table-v2 → table}/header/TableHeaderRow.vue +3 -3
  21. package/src/components/{table-v2 → table}/style/table-loading.css +3 -3
  22. package/src/components/{table-v2 → table}/style/table-wrapper.css +67 -51
  23. package/src/components/{table-v2 → table}/style/table.css +1 -1
  24. package/src/components/textarea/Textarea.vue +1 -1
  25. package/src/index.js +0 -1
  26. package/src/components/table/table.css +0 -396
  27. package/src/components/table-v2/Table.vue +0 -305
  28. /package/src/components/{table-v2 → table}/TableResizeObserver.vue +0 -0
  29. /package/src/components/{table-v2 → table}/body/MeasureCell.vue +0 -0
  30. /package/src/components/{table-v2 → table}/composables/useColumns.js +0 -0
  31. /package/src/components/{table-v2 → table}/composables/useSorter.js +0 -0
  32. /package/src/components/{table-v2 → table}/composables/useSticky.js +0 -0
  33. /package/src/components/{table-v2 → table}/constants.js +0 -0
  34. /package/src/components/{table-v2 → table}/context/createTableContext.js +0 -0
  35. /package/src/components/{table-v2 → table}/style/table-body.css +0 -0
  36. /package/src/components/{table-v2 → table}/style/table-header.css +0 -0
@@ -1,827 +1,305 @@
1
1
  <template>
2
- <div
3
- :class="[
4
- 'ui-table',
5
- bordered && 'ui-table--bordered',
6
- isFixedLayout && 'ui-table--scroll-y',
7
- resizeState && 'ui-table--resizing'
8
- ]"
9
- :style="containerStyle"
10
- >
11
- <table class="ui-table__table" :style="tableStyle">
12
- <colgroup>
13
- <col v-if="hasDrag" class="ui-table__col ui-table__col--drag" />
14
- <col v-if="hasSelection" class="ui-table__col ui-table__col--selection" />
15
- <col v-for="col in normalizedColumns" :key="col._key" :style="col._style" />
16
- </colgroup>
17
- <thead ref="tableHead" class="ui-table__head">
18
- <tr
19
- :class="['ui-table__row', 'ui-table__row--head', scrollbarWidth > 0 && 'ui-table__row--has-scrollbar']"
20
- :style="[{ height: toCssSize(headerHeight) }, rowScrollStyle]"
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
- />
28
- <th
29
- v-if="hasSelection"
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' }]"
38
- >
39
- <WkCheckbox :checked="allSelected" :disabled="!rows.length" @change="onToggleAll" />
40
- </th>
41
- <th
42
- v-for="(col, i) in normalizedColumns"
43
- :key="col._key"
44
- :class="[
45
- 'ui-table__cell',
46
- 'ui-table__th',
47
- 'ui-table__th-flex',
48
- `ui-table__cell--${col._align}`,
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' } : {}
58
- ]"
59
- :tabindex="col._isSortable ? 0 : undefined"
60
- :aria-sort="col._ariaSort"
61
- @click="onHeaderClick(col)"
62
- @keydown.enter.prevent="onHeaderClick(col)"
63
- @keydown.space.prevent="onHeaderClick(col)"
64
- v-bind="col._headerAttrs"
65
- v-on="col._headerListeners"
66
- >
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>
79
- </span>
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>
86
- </th>
87
- <th
88
- v-if="scrollbarWidth > 0"
89
- class="ui-table__th--scrollbar"
90
- :style="{ width: scrollbarWidth + 'px', position: 'sticky', right: '0px' }"
91
- aria-hidden="true"
92
- />
93
- </tr>
94
- </thead>
95
- <tbody ref="tableBody" class="ui-table__body" :style="bodyStyle" @scroll="onBodyScroll" @dragleave="onDragLeave">
96
- <tr v-if="!rows.length" class="ui-table__row ui-table__row--empty">
97
- <td class="ui-table__cell ui-table__empty-cell" :colspan="totalColumns">
98
- <slot name="emptyText">
99
- <WkEmpty :title="emptyText" />
100
- </slot>
101
- </td>
102
- </tr>
103
- <template v-else>
104
- <!-- Top spacer reserves the height of the rows scrolled out above the window. -->
105
- <tr v-if="isVirtual" class="ui-table__spacer" :style="{ height: topSpacerHeight + 'px' }" aria-hidden="true">
106
- <td class="ui-table__spacer-cell" :colspan="totalColumns" />
107
- </tr>
108
- <tr
109
- v-for="row in mappedRenderRows"
110
- :key="row._key"
111
- :ref="'row-' + row._key"
112
- :class="[
113
- 'ui-table__row',
114
- 'ui-table__row--body',
115
- row._selected && 'ui-table__row--selected',
116
- hasDrag && dragState && row._index === dragState.currentIndex && 'ui-table__row--dragging'
117
- ]"
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)"
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>
136
- <td
137
- v-if="hasSelection"
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)"
147
- >
148
- <WkCheckbox :checked="row._selected" />
149
- </td>
150
- <td
151
- v-for="col in normalizedColumns"
152
- :key="col._key"
153
- :class="[
154
- 'ui-table__cell',
155
- 'ui-table__td',
156
- `ui-table__cell--${col._align}`,
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
- : {}
167
- ]"
168
- >
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
- >
177
- </td>
178
- </tr>
179
- <!-- Bottom spacer reserves the height of the rows below the window. -->
180
- <tr
181
- v-if="isVirtual"
182
- class="ui-table__spacer"
183
- :style="{ height: bottomSpacerHeight + 'px' }"
184
- aria-hidden="true"
185
- >
186
- <td class="ui-table__spacer-cell" :colspan="totalColumns" />
187
- </tr>
188
- </template>
189
- </tbody>
190
- </table>
2
+ <div class="ui-table">
3
+ <TableLoading>
4
+ <TableResizeObserver @resize="handleContainerResize">
5
+ <TableWrapper>
6
+ <template #bodyCell="slotData">
7
+ <slot name="bodyCell" v-bind="slotData"></slot>
8
+ </template>
9
+ <template #empty>
10
+ <slot name="empty"></slot>
11
+ </template>
12
+ </TableWrapper>
13
+ </TableResizeObserver>
14
+ </TableLoading>
191
15
  </div>
192
16
  </template>
193
17
 
194
18
  <script>
195
- import WkCheckbox from '../checkbox/Checkbox.vue'
196
- import WkEmpty from '../empty/Empty.vue'
197
- import { WkiGripVertical, WkiArrowDown, WkiArrowDownUp, WkiArrowUp } from '../../icons'
198
- import { toCssSize } from '../../utils/common'
199
- import { computeVirtualWindow } from '../../utils/virtualScroll'
200
-
201
- function defaultCompare(a, b) {
202
- if (a == null && b == null) return 0
203
- if (a == null) return -1
204
- if (b == null) return 1
205
- if (typeof a === 'number' && typeof b === 'number') return a - b
206
- return String(a).localeCompare(String(b))
207
- }
208
-
209
- const EMPTY_BINDINGS = Object.freeze({ attrs: {}, on: {} })
19
+ import { createTableContext } from './context/createTableContext.js'
20
+ import { flattenColumns } from './composables/useColumns.js'
21
+ import { sortData } from './composables/useSorter.js'
22
+ import { computeOffsets } from './composables/useSticky.js'
23
+ import { SELECTION_COLUMN, DRAGGABLE_COLUMN } from './constants.js'
24
+ import TableWrapper from './TableWrapper.vue'
25
+ import TableLoading from './TableLoading.vue'
26
+ import TableResizeObserver from './TableResizeObserver.vue'
210
27
 
211
28
  export default {
212
29
  name: 'Table',
213
- components: { WkCheckbox, WkEmpty, WkiGripVertical, WkiArrowDown, WkiArrowDownUp, WkiArrowUp },
30
+
31
+ components: { TableWrapper, TableLoading, TableResizeObserver },
32
+
33
+ provide: function () {
34
+ return { tableContext: this.tableCtx }
35
+ },
36
+
214
37
  props: {
215
- // Column descriptors: { title, dataIndex, key?, width?, align?, sorter?, ellipsis? }
216
- columns: { type: Array, default: () => [] },
217
- // Row records. Each record should expose a stable key (see rowKey).
218
- dataSource: { type: Array, default: () => [] },
219
- // How to derive a row's key: a record field name, or a function (record) => key.
220
- rowKey: { type: [String, Function], default: 'key' },
221
- // Outer border + vertical column dividers.
38
+ columns: {
39
+ type: Array,
40
+ default: function () {
41
+ return []
42
+ }
43
+ },
44
+ dataSource: {
45
+ type: Array,
46
+ default: function () {
47
+ return []
48
+ }
49
+ },
222
50
  bordered: { type: Boolean, default: false },
223
- // Render a leading checkbox column for row selection.
224
- rowSelection: { type: Boolean, default: false },
225
- isDrag: { type: Boolean, default: false },
226
- // Controlled selection — array of selected row keys.
227
- selectedRowKeys: { type: Array, default: () => [] },
228
- // Placeholder shown when dataSource is empty.
229
- emptyText: { type: String, default: 'No data' },
230
- // Fixed scroll viewport: { x, y } as px (number) or any CSS size (string).
231
- // x sets the table min-width (horizontal scroll); y caps the body height (vertical scroll + sticky header).
51
+ size: { type: String, default: 'md' },
52
+ rowKey: { type: String, default: 'key' },
232
53
  scroll: { type: Object, default: null },
233
- // Fixed overall height of the table viewport (px number or CSS size). Enables vertical scroll + sticky header.
234
- height: { type: [Number, String], default: null },
235
- rowHeight: { type: [Number, String], default: 39 },
236
- headerHeight: { type: [Number, String], default: 39 },
54
+ loading: { type: Boolean, default: false },
55
+ rowSelection: { type: Object, default: null },
237
56
  customRow: { type: Function, default: null },
238
- customHeader: { type: Function, default: null },
239
- // Virtualize the body: render only the rows visible in the viewport (+ overscan).
240
- // Requires a vertical-scroll viewport (height or scroll.y) and a numeric rowHeight.
241
- virtual: { type: Boolean, default: false },
242
- // Extra rows rendered above and below the viewport to cushion fast scrolling.
243
- overscan: { type: Number, default: 8 }
244
- },
245
- emits: ['change', 'sort-change', 'update:selectedRowKeys', 'selection-change', 'drag-record'],
246
- data() {
247
- return {
248
- sortField: null,
249
- sortOrder: null,
250
- internalSelected: this.selectedRowKeys.slice(),
251
- scrollbarWidth: 0,
252
- colWidths: [],
253
- lastClickedIndex: null,
254
- scrollTop: 0,
255
- containerHeight: 0,
256
- rafId: null,
257
- toCssSize,
258
- resizeState: null,
259
- columnWidths: {},
260
- dragState: null,
261
- dragOverIndex: null,
262
- internalData: this.dataSource.slice()
263
- }
57
+ customHeaderRow: { type: Function, default: null },
58
+ rowDraggable: { type: Object, default: null },
59
+ enableFixedLeft: { type: Boolean, default: false },
60
+ rowHeight: { type: Number, default: 50 },
61
+ headerHeight: { type: Number, default: 36 },
62
+ emptyText: { type: String, default: '' },
63
+ height: { type: [Number, String], default: 0 }
264
64
  },
265
- computed: {
266
- selectedKeysSet() {
267
- return new Set(this.internalSelected)
268
- },
269
- normalizedColumns() {
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
65
 
287
- const cols = this.columns.map((col, i) => {
288
- const _key = col.key != null ? col.key : col.dataIndex
289
- const _align = ['left', 'right', 'center'].includes(col.align) ? col.align : 'left'
290
- const overrideW = this.columnWidths[_key]
291
- let _style = {}
292
- if (overrideW != null) {
293
- _style = { width: `${overrideW}px` }
294
- } else if (col.width != null) {
295
- _style = { width: typeof col.width === 'number' ? `${col.width}px` : col.width }
296
- }
297
- const _isSortable = !!col.sorter
298
- const _field = col.dataIndex != null ? col.dataIndex : col.key
66
+ emits: ['change', 'sort-change', 'select-change'],
299
67
 
300
- let _sortState = null
301
- if (this.sortField === _field) {
302
- _sortState = this.sortOrder
303
- }
68
+ data: function () {
69
+ return { tableCtx: createTableContext() }
70
+ },
304
71
 
305
- let _ariaSort = undefined
306
- if (_sortState === 'ascend') _ariaSort = 'ascending'
307
- else if (_sortState === 'descend') _ariaSort = 'descending'
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
313
- return {
314
- ...col,
315
- _key,
316
- _align,
317
- _style,
318
- _isSortable,
319
- _field,
320
- _sortState,
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
328
- }
329
- })
72
+ computed: {
73
+ isEmpty: function () {
74
+ return this.tableCtx.data.display.length === 0
75
+ },
330
76
 
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)
77
+ columnsInput: function () {
78
+ return {
79
+ columns: this.columns,
80
+ rowKey: this.rowKey,
81
+ hasSelection: !!this.rowSelection,
82
+ hasDraggable: !!this.rowDraggable
336
83
  }
84
+ },
337
85
 
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
- }
86
+ rowsInput: function () {
87
+ return { customRow: this.customRow, customHeaderRow: this.customHeaderRow }
88
+ },
345
89
 
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
90
+ layoutInput: function () {
91
+ return {
92
+ bordered: this.bordered,
93
+ size: this.size,
94
+ scroll: this.scroll,
95
+ rowHeight: this.rowHeight,
96
+ headerHeight: this.headerHeight,
97
+ height: this.height
351
98
  }
352
-
353
- if (lastLeftIdx >= 0) cols[lastLeftIdx]._isLastFixedLeft = true
354
- if (firstRightIdx >= 0) cols[firstRightIdx]._isFirstFixedRight = true
355
-
356
- return cols
357
99
  },
358
- hasSelection() {
359
- return !!this.rowSelection
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
- },
375
- totalColumns() {
376
- return this.columns.length + (this.hasDrag ? 1 : 0) + (this.hasSelection ? 1 : 0)
377
- },
378
- rows() {
379
- const data = this.internalData.slice()
380
- if (!this.sortField || !this.sortOrder) return data
381
- const col = this.normalizedColumns.find(c => c._field === this.sortField)
382
- if (!col || !col.sorter) return data
383
- const cmp =
384
- typeof col.sorter === 'function' ? col.sorter : (a, b) => defaultCompare(a[this.sortField], b[this.sortField])
385
- data.sort(cmp)
386
- if (this.sortOrder === 'descend') data.reverse()
387
- return data
388
- },
389
- allSelected() {
390
- if (!this.rows.length) return false
391
- return this.rows.every((record, index) => this.selectedKeysSet.has(this.getRowKey(record, index)))
392
- },
393
- isFixedLayout() {
394
- const scroll = this.scroll || {}
395
- return this.height != null || scroll.y != null || scroll.x != null
396
- },
397
- containerStyle() {
398
- return {}
399
- },
400
- tableStyle() {
401
- return {}
402
- },
403
- rowScrollStyle() {
404
- const scroll = this.scroll || {}
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'
100
+
101
+ dataInput: function () {
102
+ return {
103
+ source: this.dataSource,
104
+ sortKey: this.tableCtx.sort.columnKey,
105
+ sortOrder: this.tableCtx.sort.order,
106
+ loading: this.loading,
107
+ emptyText: this.emptyText
418
108
  }
419
- return style
420
109
  },
421
- // Numeric row height — virtualization needs a fixed px value. String sizes are
422
- // parsed (e.g. '40px' -> 40); anything unparseable falls back to the prop default.
423
- rowHeightPx() {
424
- if (typeof this.rowHeight === 'number') return this.rowHeight
425
- const parsed = parseFloat(this.rowHeight)
426
- return Number.isFinite(parsed) ? parsed : 39
427
- },
428
- // Virtualization is only meaningful inside a bounded, scrollable viewport.
429
- isVirtual() {
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
432
- },
433
- virtualWindow() {
434
- return computeVirtualWindow({
435
- scrollTop: this.scrollTop,
436
- rowHeight: this.rowHeightPx,
437
- containerHeight: this.containerHeight,
438
- total: this.rows.length,
439
- overscan: this.overscan
440
- })
441
- },
442
- // Rows actually committed to the DOM: a slice when virtual, otherwise everything.
443
- slicedRenderRows() {
444
- if (!this.isVirtual) return this.rows
445
- return this.rows.slice(this.virtualWindow.startIndex, this.virtualWindow.endIndex)
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
110
+
111
+ stickyInput: function () {
112
+ return {
113
+ columns: this.tableCtx.columns.flat,
114
+ widths: this.tableCtx.layout.columnWidths
115
+ }
471
116
  },
472
- topSpacerHeight() {
473
- return this.isVirtual ? this.virtualWindow.offsetY : 0
117
+
118
+ selectionInput: function () {
119
+ return this.rowSelection
474
120
  },
475
- bottomSpacerHeight() {
476
- if (!this.isVirtual) return 0
477
- const rendered = this.slicedRenderRows.length * this.rowHeightPx
478
- return Math.max(0, this.virtualWindow.totalHeight - this.virtualWindow.offsetY - rendered)
121
+
122
+ draggableInput: function () {
123
+ return this.rowDraggable
479
124
  }
480
125
  },
126
+
481
127
  watch: {
482
- selectedRowKeys(next) {
483
- this.internalSelected = (next || []).slice()
484
- },
485
- dataSource(next) {
486
- if (!this.dragState) {
487
- this.internalData = (next || []).slice()
488
- }
489
- }
490
- },
491
- mounted() {
492
- this.initResizeObserver()
493
- },
494
- // Declare both teardown hooks: Vue 2.7 fires beforeDestroy, Vue 3.4 fires beforeUnmount.
495
- // The runtime that doesn't recognize a hook simply ignores it.
496
- beforeUnmount() {
497
- this.teardown()
498
- },
499
- methods: {
500
- // Split a customRow/customHeader result into plain attributes and event listeners.
501
- // The public API mirrors ant-design-vue's `onClick`/`onDblclick`/... convention, which
502
- // Vue 3 auto-resolves to listeners when spread via v-bind — but Vue 2 would bind them as
503
- // raw DOM attributes. So we strip the `onXxx` keys into a v-on map (event name lower-cased)
504
- // and leave the rest for v-bind, which works identically on both runtimes.
505
- splitBindings(raw) {
506
- const attrs = {}
507
- const on = {}
508
- if (raw) {
509
- Object.keys(raw).forEach(key => {
510
- const match = /^on([A-Z].*)$/.exec(key)
511
- if (match) {
512
- on[match[1].charAt(0).toLowerCase() + match[1].slice(1)] = raw[key]
513
- } else {
514
- attrs[key] = raw[key]
515
- }
516
- })
517
- }
518
- return { attrs, on }
519
- },
520
- teardown() {
521
- if (this._resizeObserver) {
522
- this._resizeObserver.disconnect()
523
- this._resizeObserver = null
524
- }
525
- if (this.rafId != null) {
526
- cancelAnimationFrame(this.rafId)
527
- this.rafId = null
128
+ columnsInput: {
129
+ immediate: true,
130
+ handler: function (v) {
131
+ var cols = flattenColumns(v.columns)
132
+ const isHasFixedLeft = cols.some(col => col.fixed === 'left')
133
+ if (v.hasSelection) {
134
+ cols.unshift({
135
+ key: '__selection__',
136
+ type: SELECTION_COLUMN,
137
+ width: 36,
138
+ dataIndex: '',
139
+ title: '',
140
+ align: null,
141
+ fixed: isHasFixedLeft || this.enableFixedLeft ? 'left' : null,
142
+ ellipsis: false
143
+ })
144
+ }
145
+ if (v.hasDraggable) {
146
+ cols.unshift({
147
+ key: '__draggable__',
148
+ type: DRAGGABLE_COLUMN,
149
+ width: 36,
150
+ dataIndex: '',
151
+ title: '',
152
+ align: null,
153
+ fixed: isHasFixedLeft || this.enableFixedLeft ? 'left' : null,
154
+ ellipsis: false
155
+ })
156
+ }
157
+ this.tableCtx.columns.flat = cols
158
+ this.tableCtx.layout.rowKey = v.rowKey
528
159
  }
529
160
  },
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)
542
- },
543
- onBodyScroll(e) {
544
- const top = e.target.scrollTop
545
- const left = e.target.scrollLeft
546
- // Coalesce bursts of scroll events into a single state update per frame so we
547
- // re-slice at most once per repaint — keeps 10k+ rows smooth.
548
- if (this.rafId != null) return
549
- this.rafId = requestAnimationFrame(() => {
550
- this.rafId = null
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
555
- })
556
- },
557
- syncScrollbarGutter() {
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
161
 
564
- if (hasY) {
565
- this.containerHeight = body.clientHeight
566
- }
567
-
568
- if (this.isFixedLayout) {
569
- const firstRow = body.querySelector('.ui-table__row--body')
570
- if (firstRow) {
571
- const cells = Array.from(firstRow.children)
572
- const widths = cells.map(c => c.getBoundingClientRect().width)
573
- const changed =
574
- widths.length !== this.colWidths.length ||
575
- widths.some((w, i) => Math.abs(w - (this.colWidths[i] || 0)) > 0.1)
576
- if (changed) {
577
- this.colWidths = widths
578
- }
579
- } else if (this.colWidths.length) {
580
- this.colWidths = []
581
- }
582
- } else if (this.colWidths.length) {
583
- this.colWidths = []
162
+ rowsInput: {
163
+ immediate: true,
164
+ handler: function (v) {
165
+ this.tableCtx.rows.customRow = v.customRow
166
+ this.tableCtx.rows.customHeaderRow = v.customHeaderRow
584
167
  }
585
168
  },
586
- headCellStyle(index, col = null) {
587
- if (this.isFixedLayout && this.colWidths[index] !== undefined) {
588
- return { width: `${this.colWidths[index]}px` }
169
+
170
+ layoutInput: {
171
+ immediate: true,
172
+ deep: true,
173
+ handler: function (v) {
174
+ this.tableCtx.layout.bordered = v.bordered
175
+ this.tableCtx.layout.size = v.size
176
+ this.tableCtx.layout.scroll = v.scroll
177
+ this.tableCtx.layout.tableLayout = v.scroll && v.scroll.y ? 'fixed' : 'auto'
178
+ this.tableCtx.layout.height = v.height
179
+ this.tableCtx.layout.headerHeight = v.headerHeight
180
+ this.tableCtx.layout.rowHeight = v.rowHeight
589
181
  }
590
- return col ? col._style : {}
591
- },
592
- cellText(col, record) {
593
- const field = col._field
594
- return field != null ? record[field] : ''
595
182
  },
596
- onHeaderClick(col) {
597
- if (!col._isSortable) return
598
- const field = col._field
599
- let order
600
- if (this.sortField !== field) {
601
- order = 'ascend'
602
- } else {
603
- order = this.sortOrder === 'ascend' ? 'descend' : this.sortOrder === 'descend' ? null : 'ascend'
183
+
184
+ dataInput: {
185
+ immediate: true,
186
+ handler: function (v) {
187
+ this.tableCtx.data.source = v.source
188
+ ;((this.tableCtx.data.display = v.sortKey ? sortData(v.source, v.sortKey, v.sortOrder) : v.source),
189
+ (this.tableCtx.data.loading = v.loading))
190
+ this.tableCtx.data.emptyText = v.emptyText
604
191
  }
605
- this.sortField = order ? field : null
606
- this.sortOrder = order
607
- const payload = { field: this.sortField, order }
608
- this.$emit('sort-change', payload)
609
- this.$emit('change', { sorter: payload })
610
- },
611
- getRowKey(record, index) {
612
- if (typeof this.rowKey === 'function') return this.rowKey(record)
613
- const key = record[this.rowKey]
614
- return key != null ? key : index
615
- },
616
- isSelected(record, index) {
617
- return this.selectedKeysSet.has(this.getRowKey(record, index))
618
- },
619
- emitSelection() {
620
- const keys = this.internalSelected.slice()
621
- const selectedRows = this.dataSource.filter((record, index) => keys.includes(this.getRowKey(record, index)))
622
- this.$emit('update:selectedRowKeys', keys)
623
- this.$emit('selection-change', keys, selectedRows)
624
192
  },
625
- handleSelection(record, index, checked, e) {
626
- const isShiftKey = e && e.shiftKey
627
-
628
- if (isShiftKey && this.lastClickedIndex != null) {
629
- // Shift+click: select range from lastClickedIndex to current index
630
- const start = Math.min(this.lastClickedIndex, index)
631
- const end = Math.max(this.lastClickedIndex, index)
632
- const rangeKeys = []
633
- for (let i = start; i <= end; i++) {
634
- rangeKeys.push(this.getRowKey(this.rows[i], i))
635
- }
636
- // Merge with existing selection (union)
637
- const set = new Set(this.internalSelected)
638
- rangeKeys.forEach(k => set.add(k))
639
- this.internalSelected = Array.from(set)
640
- } else {
641
- // Normal click / Cmd+click: toggle individual row
642
- const key = this.getRowKey(record, index)
643
- const set = this.internalSelected.filter(k => k !== key)
644
- if (checked) set.push(key)
645
- this.internalSelected = set
193
+
194
+ stickyInput: {
195
+ deep: true,
196
+ immediate: true,
197
+ handler: function (v) {
198
+ if (!v.columns.length) return
199
+ var offsets = computeOffsets(v.columns, v.widths)
200
+ this.tableCtx.sticky.leftOffsets = offsets.left
201
+ this.tableCtx.sticky.rightOffsets = offsets.right
646
202
  }
647
- this.lastClickedIndex = index
648
- this.emitSelection()
649
- },
650
- onSelectionCellClick(record, index, e) {
651
- // Click on the checkbox <td> — MouseEvent has modifier keys
652
- const currentlySelected = this.isSelected(record, index)
653
- this.handleSelection(record, index, !currentlySelected, e)
654
203
  },
655
- onRowClick(record, index, e) {
656
- // Click on the row body — handle Shift/Cmd selection
657
- const isMetaKey = e.metaKey || e.ctrlKey
658
- const isShiftKey = e.shiftKey
659
- if (!isMetaKey && !isShiftKey) return
660
- const key = this.getRowKey(record, index)
661
- const currentlySelected = this.internalSelected.includes(key)
662
- this.handleSelection(record, index, !currentlySelected, e)
663
- },
664
- onToggleAll(checked) {
665
- this.internalSelected = checked ? this.rows.map((record, index) => this.getRowKey(record, index)) : []
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
204
+
205
+ selectionInput: {
206
+ immediate: true,
207
+ deep: true,
208
+ handler: function (v) {
209
+ if (!v) return
210
+ this.tableCtx.selection.selectedRowKeys = v.selectedRowKeys || []
211
+ this.tableCtx.selection.hideSelectAll = v.hideSelectAll
212
+ this.tableCtx.selection.columnWidth = v.columnWidth
213
+ this.tableCtx.selection.type = v.type
214
+ this.tableCtx.selection.onChange = v.onChange
215
+ this.tableCtx.selection.onSelect = v.onSelect
216
+ this.tableCtx.selection.onSelectAll = v.onSelectAll
217
+ this.tableCtx.selection.onSelectNone = v.onSelectNone
726
218
  }
727
219
  },
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()
220
+
221
+ draggableInput: {
222
+ immediate: true,
223
+ deep: true,
224
+ handler: function (v) {
225
+ this.tableCtx.draggable.enabled = !!v
226
+ if (!v) return
227
+ this.tableCtx.draggable.columnWidth = v.columnWidth || 36
228
+ this.tableCtx.draggable.handleReorder = v.handleReorder || null
229
+ this.tableCtx.draggable.enableAnimationFlip = v.enableAnimationFlip || false
739
230
  }
740
- this.dragState = null
741
- this.dragOverIndex = null
231
+ }
232
+ },
233
+
234
+ created: function () {
235
+ this.tableCtx.actions.sort = this.handleSort
236
+ this.tableCtx.actions.select = this.handleSelect
237
+ this.tableCtx.actions.resize = this.handleResize
238
+ this.tableCtx.actions.reorder = this.handleReorder
239
+ },
240
+
241
+ methods: {
242
+ handleSort: function (columnKey, order) {
243
+ this.tableCtx.sort.columnKey = columnKey
244
+ this.tableCtx.sort.order = order
245
+ this.$emit('sort-change', { columnKey: columnKey, order: order })
246
+ this.$emit('change', { sort: { columnKey: columnKey, order: order } })
742
247
  },
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
- }
248
+
249
+ handleSelect: function (keys, meta) {
250
+ var prevKeys = this.tableCtx.selection.selectedRowKeys
251
+ var display = this.tableCtx.data.display
252
+ var rowKey = this.tableCtx.layout.rowKey
253
+ var rs = this.rowSelection || {}
254
+
255
+ this.tableCtx.selection.checkAll = keys.length === display.length && display.length > 0
256
+
257
+ var selectedRows = display.filter(function (r) {
258
+ return keys.indexOf(r[rowKey]) !== -1
756
259
  })
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 })
260
+
261
+ if (rs.onChange) rs.onChange(keys, selectedRows)
262
+
263
+ if (!meta) return
264
+
265
+ if (meta.type === 'row') {
266
+ if (rs.onSelect) rs.onSelect(meta.record, meta.selected, selectedRows, meta.nativeEvent)
267
+ } else if (meta.type === 'all') {
268
+ // changeRows = rows mới được check (chưa có trong prevKeys)
269
+ var changeRows = display.filter(function (r) {
270
+ return prevKeys.indexOf(r[rowKey]) === -1
784
271
  })
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'
272
+ if (rs.onSelectAll) rs.onSelectAll(true, selectedRows, changeRows)
273
+ } else if (meta.type === 'none') {
274
+ // changeRows = rows bị uncheck (đang có trong prevKeys)
275
+ changeRows = display.filter(function (r) {
276
+ return prevKeys.indexOf(r[rowKey]) !== -1
277
+ })
278
+ if (rs.onSelectAll) rs.onSelectAll(false, [], changeRows)
279
+ if (rs.onSelectNone) rs.onSelectNone()
796
280
  }
797
- e.currentTarget.setPointerCapture(e.pointerId)
798
- document.addEventListener('pointermove', this.onResizeMove)
799
- document.addEventListener('pointerup', this.onResizeEnd, { once: true })
800
281
  },
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)
282
+
283
+ handleResize: function (colKey, width) {
284
+ var widths = new Map(this.tableCtx.layout.columnWidths)
285
+ widths.set(colKey, width)
286
+ this.tableCtx.layout.columnWidths = widths
287
+ },
288
+
289
+ handleReorder: function (oldIndex, newIndex) {
290
+ const display = this.tableCtx.data.display.slice()
291
+ const moved = display.splice(oldIndex, 1)[0]
292
+ display.splice(newIndex, 0, moved)
293
+
294
+ const cb = this.tableCtx.draggable.handleReorder
295
+ if (typeof cb === 'function') cb(oldIndex, newIndex, display)
817
296
  },
818
297
 
819
- onResizeEnd() {
820
- document.removeEventListener('pointermove', this.onResizeMove)
821
- this.resizeState = null
298
+ handleContainerResize: function (size) {
299
+ this.tableCtx.layout.containerWidth = size.width
822
300
  }
823
301
  }
824
302
  }
825
303
  </script>
826
304
 
827
- <style src="./table.css" scoped></style>
305
+ <style src="./style/table.css" scoped></style>