@v1nt1248/3nclient-lib 0.3.35 → 0.3.37

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.
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts" generic="T extends Ui3nTableBodyBaseItem">
2
2
  import size from 'lodash/size';
3
+ import { computed, onBeforeUnmount, ref, watch } from 'vue';
3
4
  import { useTable } from './composables/useTable';
4
5
  import type {
5
6
  Ui3nTableBodyBaseItem,
@@ -11,11 +12,18 @@
11
12
  import Ui3nButton from '../ui3n-button/ui3n-button.vue';
12
13
  import Ui3nCheckbox from '../ui3n-checkbox/ui3n-checkbox.vue';
13
14
  import Ui3nTableSortIcon from './ui3n-table-sort-icon.vue';
15
+ import Ui3nScrollbar from '../ui3n-scrollbar/ui3n-scrollbar.vue';
14
16
 
15
17
  const props = defineProps<Ui3nTableProps<T>>();
16
18
  const emits = defineEmits<Ui3nTableEmits<T>>();
17
19
  defineSlots<Ui3nTableSlots<T, string & keyof T>>();
18
20
 
21
+ const scrollbarRef = ref<InstanceType<typeof Ui3nScrollbar> | null>(null);
22
+ const scrollbarContainer = computed(() => scrollbarRef.value?.getContainer() ?? null);
23
+ const headerEl = ref<HTMLDivElement | null>(null);
24
+ const headerHeight = ref(0);
25
+ let headerResizeObserver: ResizeObserver | null = null;
26
+
19
27
  const {
20
28
  tableEl,
21
29
  tableColumnWidth,
@@ -27,6 +35,10 @@
27
35
  selectedRows,
28
36
  selectedRowsArray,
29
37
  selectedRowsSize,
38
+ stickyColumnsActive,
39
+ stickyColumnsCount,
40
+ stickyColumnLefts,
41
+ scrollportWidth,
30
42
  closeGroupActionsRow,
31
43
  getRowKey,
32
44
  isRowSelected,
@@ -35,12 +47,70 @@
35
47
  toggleSelectedRows,
36
48
  changeSortOrder,
37
49
  clear,
38
- } = useTable(props, emits);
50
+ } = useTable(props, emits, scrollbarContainer);
39
51
 
40
52
  const eventHandlers = {
41
53
  select: processSelection,
42
54
  };
43
55
 
56
+ const scrollbarAxes = computed(() => props.config?.scrollbar?.axes ?? 'both');
57
+
58
+ const scrollbarVertical = computed(() => {
59
+ const base = props.config?.scrollbar?.vertical ?? {};
60
+ const axes = scrollbarAxes.value;
61
+ const needsVerticalOffset = axes === 'vertical' || axes === 'both';
62
+
63
+ if (!needsVerticalOffset || !headerHeight.value) {
64
+ return base;
65
+ }
66
+
67
+ return {
68
+ ...base,
69
+ trackOffsetTop: headerHeight.value,
70
+ };
71
+ });
72
+
73
+ const tableStyle = computed(() => {
74
+ const style: Record<string, string> = {
75
+ '--ui3n-table-columns-width': tableColumnWidth.value,
76
+ };
77
+
78
+ if (scrollportWidth.value != null) {
79
+ style['--ui3n-table-scrollport-width'] = `${scrollportWidth.value}px`;
80
+ }
81
+
82
+ return style;
83
+ });
84
+
85
+ function syncHeaderObserver() {
86
+ headerResizeObserver?.disconnect();
87
+ headerResizeObserver = null;
88
+
89
+ const el = headerEl.value;
90
+ if (!el || !props.config?.scrollbar) {
91
+ headerHeight.value = 0;
92
+ return;
93
+ }
94
+
95
+ const update = () => {
96
+ headerHeight.value = el.offsetHeight;
97
+ };
98
+
99
+ update();
100
+ headerResizeObserver = new ResizeObserver(update);
101
+ headerResizeObserver.observe(el);
102
+ }
103
+
104
+ watch([headerEl, () => props.config?.scrollbar, showGroupActionsRow], syncHeaderObserver, {
105
+ immediate: true,
106
+ flush: 'post',
107
+ });
108
+
109
+ onBeforeUnmount(() => {
110
+ headerResizeObserver?.disconnect();
111
+ headerResizeObserver = null;
112
+ });
113
+
44
114
  defineExpose<Ui3nTableExpose<T>>({
45
115
  getRowStyle,
46
116
  selectedRows,
@@ -52,10 +122,202 @@
52
122
  </script>
53
123
 
54
124
  <template>
125
+ <Ui3nScrollbar
126
+ v-if="config?.scrollbar"
127
+ ref="scrollbarRef"
128
+ :class="$style.tableHost"
129
+ :axes="scrollbarAxes"
130
+ :vertical="scrollbarVertical"
131
+ :horizontal="config.scrollbar.horizontal"
132
+ >
133
+ <div
134
+ ref="tableEl"
135
+ :style="tableStyle"
136
+ :class="[$style.ui3nTable, $style.ui3nTableInsideScrollbar, stickyColumnsActive && $style.stickyColumns]"
137
+ >
138
+ <fieldset
139
+ v-if="config?.tableName && selectedRowsSize > 0"
140
+ :class="$style.hiddenBlock"
141
+ >
142
+ <input
143
+ v-for="(row, rowIndex) in selectedRowsArray"
144
+ :key="getRowKey(row, rowIndex)"
145
+ type="hidden"
146
+ :name="config.selectable === 'multiple' ? `${config.tableName}[]` : config.tableName"
147
+ :value="String(getRowKey(row, rowIndex))"
148
+ />
149
+ </fieldset>
150
+
151
+ <div
152
+ ref="headerEl"
153
+ :class="[$style.header, showGroupActionsRow && $style.withGroupActions]"
154
+ >
155
+ <!-- group actions -->
156
+ <div
157
+ v-if="showGroupActionsRow"
158
+ :class="$style.groupActions"
159
+ >
160
+ <ui3n-checkbox
161
+ :indeterminate="selectedRowsSize < body.content.length && selectedRowsSize !== 0"
162
+ :model-value="selectedRowsSize === body.content.length"
163
+ @change="toggleSelectedRows"
164
+ >
165
+ Selected: {{ selectedRowsSize }}
166
+ </ui3n-checkbox>
167
+
168
+ <div :class="$style.groupActionsBody">
169
+ <slot
170
+ name="group-actions"
171
+ :selected-rows="selectedRowsArray"
172
+ />
173
+ </div>
174
+
175
+ <ui3n-button
176
+ type="secondary"
177
+ @click="closeGroupActionsRow"
178
+ >
179
+ Cancel
180
+ </ui3n-button>
181
+ </div>
182
+ <!-- table header -->
183
+ <template
184
+ v-for="(h, hIndex) in visibleColumns"
185
+ :key="h.key"
186
+ >
187
+ <div
188
+ :class="[
189
+ $style.headerItemWrapper,
190
+ hIndex === 0 && $style.headerItemFirst,
191
+ hIndex < stickyColumnsCount && $style.headerItemSticky,
192
+ h.sortable && $style.sortable,
193
+ h.sortable && currentConfig.sortOrder?.field === h.key && $style.sortableActive,
194
+ ]"
195
+ :style="[
196
+ h.headCellStyle,
197
+ hIndex < stickyColumnsCount ? { left: stickyColumnLefts[hIndex], zIndex: 6 + hIndex } : undefined,
198
+ ]"
199
+ >
200
+ <div
201
+ :class="$style.headerItem"
202
+ :style="h.headCellStyle"
203
+ v-on="h.sortable ? { click: () => changeSortOrder(h.key) } : {}"
204
+ >
205
+ <slot :name="`header-cell-${h.key as string & keyof T}`">
206
+ <span :class="$style.headerText">
207
+ {{ h.text }}
208
+ </span>
209
+ </slot>
210
+ <ui3n-table-sort-icon
211
+ v-if="h.sortable && currentConfig.sortOrder?.field === h.key"
212
+ :value="currentConfig.sortOrder?.direction"
213
+ size="16"
214
+ />
215
+ </div>
216
+ </div>
217
+ </template>
218
+ </div>
219
+
220
+ <!-- table: body -->
221
+ <div :class="$style.body">
222
+ <template v-if="config?.showNoDataMessage && !size(body.content)">
223
+ <slot name="no-data">
224
+ <div :class="$style.noData">No data</div>
225
+ </slot>
226
+ </template>
227
+
228
+ <template v-else>
229
+ <template v-if="$slots['row']">
230
+ <div
231
+ v-for="(row, rowIndex) in body.content"
232
+ :key="getRowKey(row, rowIndex)"
233
+ :class="[
234
+ $style.row,
235
+ $slots['row'] && $style.customRow,
236
+ isRowSelected(row) && $style.selected,
237
+ config.selectable && $style.selectable,
238
+ ]"
239
+ >
240
+ <slot
241
+ name="row"
242
+ :row="row"
243
+ :row-style="getRowStyle(row)"
244
+ :row-index="rowIndex"
245
+ :is-row-selected="isRowSelected(row)"
246
+ :column-style="config?.columnStyle"
247
+ :sticky-columns-count="stickyColumnsCount"
248
+ :sticky-column-lefts="stickyColumnLefts"
249
+ :events="eventHandlers"
250
+ />
251
+ </div>
252
+ </template>
253
+
254
+ <template v-else>
255
+ <div
256
+ v-for="(row, rowIndex) in body.content"
257
+ :key="getRowKey(row, rowIndex)"
258
+ :class="[$style.row, isRowSelected(row) && $style.selected, config.selectable && $style.selectable]"
259
+ :style="getRowStyle(row)"
260
+ >
261
+ <div
262
+ v-if="config.selectable"
263
+ :class="$style.rowCheckbox"
264
+ >
265
+ <ui3n-checkbox
266
+ :model-value="isRowSelected(row)"
267
+ @change="processSelection(row)"
268
+ />
269
+ </div>
270
+
271
+ <template
272
+ v-for="(col, colIndex) in visibleColumns"
273
+ :key="col.key"
274
+ >
275
+ <div
276
+ :class="[
277
+ $style.bodyItem,
278
+ colIndex === 0 && $style.bodyItemFirst,
279
+ colIndex < stickyColumnsCount && $style.bodyItemSticky,
280
+ ]"
281
+ :style="
282
+ colIndex < stickyColumnsCount
283
+ ? { left: stickyColumnLefts[colIndex], zIndex: 2 + colIndex }
284
+ : undefined
285
+ "
286
+ >
287
+ <slot
288
+ :name="`row-cell-${col.key as string & keyof T}`"
289
+ :row="row"
290
+ :row-index="rowIndex"
291
+ :is-row-selected="isRowSelected(row)"
292
+ :column-style="config?.columnStyle"
293
+ :cell="row[col.key]"
294
+ >
295
+ <span :class="$style.cell">
296
+ {{ col.format ? col.format(row[col.key]) : row[col.key] }}
297
+ </span>
298
+ </slot>
299
+ </div>
300
+ </template>
301
+ </div>
302
+ </template>
303
+ </template>
304
+ </div>
305
+
306
+ <!-- table: unused space-->
307
+ <div
308
+ :class="$style.unusedPlace"
309
+ :style="unusedPlaceCssStyle"
310
+ >
311
+ <slot name="unused-place" />
312
+ </div>
313
+ </div>
314
+ </Ui3nScrollbar>
315
+
55
316
  <div
317
+ v-else
56
318
  ref="tableEl"
57
- :style="{ '--ui3n-table-columns-width': tableColumnWidth }"
58
- :class="$style.ui3nTable"
319
+ :style="tableStyle"
320
+ :class="[$style.ui3nTable, stickyColumnsActive && $style.stickyColumns]"
59
321
  >
60
322
  <fieldset
61
323
  v-if="config?.tableName && selectedRowsSize > 0"
@@ -100,16 +362,21 @@
100
362
  </div>
101
363
  <!-- table header -->
102
364
  <template
103
- v-for="h in visibleColumns"
365
+ v-for="(h, hIndex) in visibleColumns"
104
366
  :key="h.key"
105
367
  >
106
368
  <div
107
369
  :class="[
108
370
  $style.headerItemWrapper,
371
+ hIndex === 0 && $style.headerItemFirst,
372
+ hIndex < stickyColumnsCount && $style.headerItemSticky,
109
373
  h.sortable && $style.sortable,
110
374
  h.sortable && currentConfig.sortOrder?.field === h.key && $style.sortableActive,
111
375
  ]"
112
- :style="h.headCellStyle"
376
+ :style="[
377
+ h.headCellStyle,
378
+ hIndex < stickyColumnsCount ? { left: stickyColumnLefts[hIndex], zIndex: 6 + hIndex } : undefined,
379
+ ]"
113
380
  >
114
381
  <div
115
382
  :class="$style.headerItem"
@@ -158,6 +425,8 @@
158
425
  :row-index="rowIndex"
159
426
  :is-row-selected="isRowSelected(row)"
160
427
  :column-style="config?.columnStyle"
428
+ :sticky-columns-count="stickyColumnsCount"
429
+ :sticky-column-lefts="stickyColumnLefts"
161
430
  :events="eventHandlers"
162
431
  />
163
432
  </div>
@@ -184,7 +453,18 @@
184
453
  v-for="(col, colIndex) in visibleColumns"
185
454
  :key="col.key"
186
455
  >
187
- <div :class="[$style.bodyItem, colIndex === 0 && $style.bodyItemFirst]">
456
+ <div
457
+ :class="[
458
+ $style.bodyItem,
459
+ colIndex === 0 && $style.bodyItemFirst,
460
+ colIndex < stickyColumnsCount && $style.bodyItemSticky,
461
+ ]"
462
+ :style="
463
+ colIndex < stickyColumnsCount
464
+ ? { left: stickyColumnLefts[colIndex], zIndex: 2 + colIndex }
465
+ : undefined
466
+ "
467
+ >
188
468
  <slot
189
469
  :name="`row-cell-${col.key as string & keyof T}`"
190
470
  :row="row"
@@ -217,6 +497,13 @@
217
497
  <style lang="scss" module>
218
498
  @use '@/assets/styles/mixins.scss' as mixins;
219
499
 
500
+ .tableHost {
501
+ width: 100%;
502
+ height: 100%;
503
+ position: relative;
504
+ overflow: hidden;
505
+ }
506
+
220
507
  .ui3nTable {
221
508
  --ui3n-table-columns-width: auto;
222
509
  --ui3n-table-base-head-height: 36px;
@@ -245,6 +532,24 @@
245
532
  align-items: stretch;
246
533
  }
247
534
 
535
+ /* Scroll lives on Ui3nScrollbar; overflow must stay visible so sticky sticks to scrollport. */
536
+ .ui3nTableInsideScrollbar {
537
+ width: max-content;
538
+ min-width: 100%;
539
+ height: auto;
540
+ min-height: 100%;
541
+ overflow: visible;
542
+
543
+ &.stickyColumns {
544
+ overflow: visible;
545
+
546
+ .body {
547
+ width: max-content;
548
+ min-width: 100%;
549
+ }
550
+ }
551
+ }
552
+
248
553
  .hiddenBlock {
249
554
  display: none;
250
555
  border: none;
@@ -415,4 +720,69 @@
415
720
  .unusedPlace {
416
721
  flex-grow: 1;
417
722
  }
723
+
724
+ .stickyColumns {
725
+ overflow: auto;
726
+
727
+ .header,
728
+ .row {
729
+ width: max-content;
730
+ min-width: 100%;
731
+ display: grid;
732
+ grid-template-columns: var(--ui3n-table-columns-width);
733
+ }
734
+
735
+ .header {
736
+ padding-left: 0;
737
+ }
738
+
739
+ .row:not(.customRow) {
740
+ padding-left: 0;
741
+ }
742
+
743
+ .row.customRow {
744
+ padding-left: 0;
745
+ }
746
+
747
+ .headerItemSticky {
748
+ position: sticky;
749
+ background-color: var(--ui3n-table-header-bg-color);
750
+ }
751
+
752
+ .headerItemFirst.headerItemSticky {
753
+ padding-left: var(--ui3n-table-padding-inline);
754
+ }
755
+
756
+ .headerItemWrapper:not(.headerItemFirst) {
757
+ padding: 0 calc(var(--ui3n-table-padding-inline) / 4);
758
+ }
759
+
760
+ .bodyItemSticky {
761
+ position: sticky;
762
+ background-color: var(--color-bg-table-cell-default);
763
+ }
764
+
765
+ .bodyItemFirst.bodyItemSticky {
766
+ padding-left: calc(var(--ui3n-table-padding-inline) + var(--ui3n-table-padding-inline) * 1.5);
767
+ }
768
+
769
+ .row:hover .bodyItemSticky {
770
+ background-color: var(--color-bg-control-primary-hover);
771
+ }
772
+
773
+ .row.selected .bodyItemSticky {
774
+ background-color: var(--ui3n-table-row-bg-color-selected);
775
+ }
776
+
777
+ .rowCheckbox {
778
+ position: sticky;
779
+ left: var(--ui3n-table-padding-inline);
780
+ z-index: 3;
781
+ }
782
+
783
+ .groupActions {
784
+ width: var(--ui3n-table-scrollport-width, 100%);
785
+ left: 0;
786
+ }
787
+ }
418
788
  </style>
@@ -16,6 +16,39 @@ export interface Ui3nVirtualScrollProps<T> {
16
16
  * Number of items to render ahead
17
17
  */
18
18
  renderAhead?: number;
19
+ /**
20
+ * When true, if the user is near the bottom of the list,
21
+ * keep the viewport stuck to the bottom after items update
22
+ * (typical chat behavior). Default: false.
23
+ */
24
+ stickToBottom?: boolean;
25
+ /**
26
+ * Distance from the bottom (px) treated as "near bottom" for stickToBottom.
27
+ * Default: 32.
28
+ */
29
+ stickToBottomThreshold?: number;
30
+ /**
31
+ * Distance from an edge (px) at which 'reached-top' / 'reached-bottom' fire.
32
+ * Default: 64.
33
+ */
34
+ reachEdgeThreshold?: number;
35
+ }
36
+
37
+ /**
38
+ * Virtual scroll component emits
39
+ */
40
+ export interface Ui3nVirtualScrollEmits {
41
+ (ev: 'scroll', value: Event): void;
42
+ /**
43
+ * The list has been scrolled close to its top - the moment to load
44
+ * a previous page of items. Fires once per approach: it will not repeat
45
+ * until the list leaves the edge or 'items' change.
46
+ */
47
+ (ev: 'reached-top'): void;
48
+ /**
49
+ * Same as 'reached-top', for the bottom edge.
50
+ */
51
+ (ev: 'reached-bottom'): void;
19
52
  }
20
53
 
21
54
  /**
@@ -27,3 +60,10 @@ export interface Ui3nVirtualScrollSlots<T> {
27
60
  */
28
61
  item: ({ value, index }: { value: T; index: number }) => VNode;
29
62
  }
63
+
64
+ export interface Ui3nVirtualScrollExpose {
65
+ scrollToBottom(): void;
66
+ scrollToItem(id: string, align?: 'start' | 'end' | 'center'): void;
67
+ getScrollTop(): number;
68
+ isStuckToBottom(): boolean;
69
+ }