@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,9 +1,9 @@
1
- import { computed, type Ref, ref, watch, UnwrapRef } from 'vue';
1
+ import { computed, type Ref, ref, watch, UnwrapRef, onBeforeUnmount } from 'vue';
2
2
  import get from 'lodash/get';
3
3
  import isEmpty from 'lodash/isEmpty';
4
4
  import size from 'lodash/size';
5
5
  import cloneDeep from 'lodash/cloneDeep';
6
- import {
6
+ import type {
7
7
  Ui3nTableBodyBaseItem,
8
8
  Ui3nTableConfig,
9
9
  Ui3nTableEmits,
@@ -12,7 +12,21 @@ import {
12
12
  } from '@/components/ui3n-table/types';
13
13
  import type { Ui3nCheckboxValue } from '@/components/ui3n-checkbox/types';
14
14
 
15
- export function useTable<T extends Ui3nTableBodyBaseItem>(props: Ui3nTableProps<T>, emits: Ui3nTableEmits<T>) {
15
+ const ABSOLUTE_CSS_SIZE_RE = /^-?\d+(\.\d+)?(px|rem|em|ch|ex|cm|mm|in|pt|pc)$/i;
16
+
17
+ export function isAbsoluteCssSize(value: string): boolean {
18
+ if (!value || typeof value !== 'string') {
19
+ return false;
20
+ }
21
+
22
+ return ABSOLUTE_CSS_SIZE_RE.test(value.trim());
23
+ }
24
+
25
+ export function useTable<T extends Ui3nTableBodyBaseItem>(
26
+ props: Ui3nTableProps<T>,
27
+ emits: Ui3nTableEmits<T>,
28
+ scrollbarContainerRef?: Ref<HTMLDivElement | null>,
29
+ ) {
16
30
  const tableEl = ref<HTMLDivElement | null>(null);
17
31
  const currentConfig = ref<Pick<Ui3nTableConfig<T>, 'sortOrder' | 'fieldAsRowKey'>>({
18
32
  sortOrder: setInitialSortOrder(),
@@ -22,6 +36,9 @@ export function useTable<T extends Ui3nTableBodyBaseItem>(props: Ui3nTableProps<
22
36
  ? ref([])
23
37
  : (ref(new Set()) as Ref<Set<T>>);
24
38
  const hasGroupActionsRow = ref(false);
39
+ const scrollportWidth = ref<number | null>(null);
40
+
41
+ let resizeObserver: ResizeObserver | null = null;
25
42
 
26
43
  const isRowKeyUsed = computed(() => !!currentConfig.value.fieldAsRowKey);
27
44
  const visibleColumns = computed(() => props.head.filter(h => !h.hidden));
@@ -37,6 +54,77 @@ export function useTable<T extends Ui3nTableBodyBaseItem>(props: Ui3nTableProps<
37
54
  );
38
55
  const showGroupActionsRow = computed(() => props.config?.selectable === 'multiple' && hasGroupActionsRow.value);
39
56
 
57
+ function getInvalidStickyColumnWidths(): Array<{ key: string; width: string }> {
58
+ const { columnStyle } = props.config ?? {};
59
+ const invalid: Array<{ key: string; width: string }> = [];
60
+
61
+ for (const h of visibleColumns.value) {
62
+ const width = get(columnStyle, [h.key, 'width'], '') as string;
63
+ if (!isAbsoluteCssSize(width)) {
64
+ invalid.push({ key: String(h.key), width: width || '(missing)' });
65
+ }
66
+ }
67
+
68
+ return invalid;
69
+ }
70
+
71
+ const stickyColumnsRequested = computed(() => {
72
+ const n = Number(props.config?.stickyColumns);
73
+ if (!Number.isFinite(n) || n < 1) {
74
+ return 0;
75
+ }
76
+
77
+ return Math.floor(n);
78
+ });
79
+
80
+ const stickyColumnsCount = computed(() => {
81
+ if (!stickyColumnsRequested.value) {
82
+ return 0;
83
+ }
84
+
85
+ const max = Math.max(0, visibleColumns.value.length - 1);
86
+ if (!max) {
87
+ return 0;
88
+ }
89
+
90
+ if (getInvalidStickyColumnWidths().length) {
91
+ return 0;
92
+ }
93
+
94
+ return Math.min(stickyColumnsRequested.value, max);
95
+ });
96
+
97
+ const stickyColumnsActive = computed(() => stickyColumnsCount.value > 0);
98
+
99
+ const stickyColumnLefts = computed(() => {
100
+ const count = stickyColumnsCount.value;
101
+ if (!count) {
102
+ return [] as string[];
103
+ }
104
+
105
+ const { columnStyle } = props.config ?? {};
106
+ const widths = visibleColumns.value.slice(0, count).map(h => {
107
+ return (get(columnStyle, [h.key, 'width'], '0px') as string).trim();
108
+ });
109
+
110
+ const lefts: string[] = [];
111
+ for (let i = 0; i < count; i++) {
112
+ if (i === 0) {
113
+ lefts.push('0px');
114
+ continue;
115
+ }
116
+
117
+ if (i === 1) {
118
+ lefts.push(widths[0]);
119
+ continue;
120
+ }
121
+
122
+ lefts.push(`calc(${widths.slice(0, i).join(' + ')})`);
123
+ }
124
+
125
+ return lefts;
126
+ });
127
+
40
128
  const tableColumnWidth = computed(() => {
41
129
  if (!tableEl.value) {
42
130
  return '100%';
@@ -45,8 +133,12 @@ export function useTable<T extends Ui3nTableBodyBaseItem>(props: Ui3nTableProps<
45
133
  const { config = {}, head } = props;
46
134
  const { columnStyle } = config;
47
135
  const keys = head.filter(h => !h.hidden).map(h => h.key);
136
+ const useAbsolute = stickyColumnsActive.value;
137
+
48
138
  return keys.reduce((res, key, index) => {
49
- const width = get(columnStyle, [key, 'width'], '1fr');
139
+ const width = useAbsolute
140
+ ? (get(columnStyle, [key, 'width']) as string)
141
+ : (get(columnStyle, [key, 'width'], '1fr') as string);
50
142
  res = index === 0 ? `${width}` : `${res} ${width}`;
51
143
  return res;
52
144
  }, '');
@@ -220,6 +312,29 @@ export function useTable<T extends Ui3nTableBodyBaseItem>(props: Ui3nTableProps<
220
312
  emits('select:row', []);
221
313
  }
222
314
 
315
+ function disconnectScrollportObserver() {
316
+ resizeObserver?.disconnect();
317
+ resizeObserver = null;
318
+ }
319
+
320
+ function syncScrollportObserver() {
321
+ disconnectScrollportObserver();
322
+
323
+ const el = scrollbarContainerRef?.value ?? tableEl.value;
324
+ if (!el || !stickyColumnsActive.value) {
325
+ scrollportWidth.value = null;
326
+ return;
327
+ }
328
+
329
+ const updateWidth = () => {
330
+ scrollportWidth.value = el.clientWidth;
331
+ };
332
+
333
+ updateWidth();
334
+ resizeObserver = new ResizeObserver(updateWidth);
335
+ resizeObserver.observe(el);
336
+ }
337
+
223
338
  watch(
224
339
  () => props.config?.tableName,
225
340
  newTableName => {
@@ -229,18 +344,6 @@ export function useTable<T extends Ui3nTableBodyBaseItem>(props: Ui3nTableProps<
229
344
  },
230
345
  );
231
346
 
232
- // watch(
233
- // tableColumnWidth,
234
- // (val, oVal) => {
235
- // if (val && val !== oVal) {
236
- // tableEl.value && tableEl.value.style.setProperty('--ui3n-table-columns-width', val);
237
- // }
238
- // },
239
- // {
240
- // immediate: true,
241
- // },
242
- // );
243
-
244
347
  watch(
245
348
  () => selectedRowsSize.value,
246
349
  (val, oldVal) => {
@@ -250,6 +353,52 @@ export function useTable<T extends Ui3nTableBodyBaseItem>(props: Ui3nTableProps<
250
353
  },
251
354
  );
252
355
 
356
+ watch(
357
+ () => ({
358
+ requested: stickyColumnsRequested.value,
359
+ tableName: props.config?.tableName,
360
+ visibleCount: visibleColumns.value.length,
361
+ columnKeys: visibleColumns.value.map(h => String(h.key)).join('\0'),
362
+ widths: visibleColumns.value
363
+ .map(h => `${String(h.key)}=${get(props.config?.columnStyle, [h.key, 'width'], '')}`)
364
+ .join('\0'),
365
+ }),
366
+ ({ requested, tableName, visibleCount }) => {
367
+ if (!requested) {
368
+ return;
369
+ }
370
+
371
+ const tableLabel = tableName ? ` tableName="${tableName}"` : '';
372
+ const max = Math.max(0, visibleCount - 1);
373
+
374
+ if (requested > max) {
375
+ console.warn(
376
+ `[Ui3nTable] stickyColumns clamped:${tableLabel} requested=${requested}, max=${max} (visibleColumns.length - 1).`,
377
+ );
378
+ }
379
+
380
+ const invalid = getInvalidStickyColumnWidths();
381
+ if (!invalid.length) {
382
+ return;
383
+ }
384
+
385
+ const details = invalid.map(({ key, width }) => `key="${key}" width="${width}"`).join(', ');
386
+ console.warn(
387
+ `[Ui3nTable] stickyColumns disabled:${tableLabel} column width must be an absolute CSS length.\nInvalid: ${details}`,
388
+ );
389
+ },
390
+ { immediate: true },
391
+ );
392
+
393
+ watch([tableEl, stickyColumnsActive, scrollbarContainerRef], syncScrollportObserver, {
394
+ immediate: true,
395
+ flush: 'post',
396
+ });
397
+
398
+ onBeforeUnmount(() => {
399
+ disconnectScrollportObserver();
400
+ });
401
+
253
402
  return {
254
403
  tableEl,
255
404
  tableColumnWidth,
@@ -261,6 +410,10 @@ export function useTable<T extends Ui3nTableBodyBaseItem>(props: Ui3nTableProps<
261
410
  selectedRows,
262
411
  selectedRowsArray,
263
412
  selectedRowsSize,
413
+ stickyColumnsActive,
414
+ stickyColumnsCount,
415
+ stickyColumnLefts,
416
+ scrollportWidth,
264
417
  closeGroupActionsRow,
265
418
  getRowKey,
266
419
  isRowSelected,
@@ -59,6 +59,48 @@ export type Ui3nTableConfig<T extends Ui3nTableBodyBaseItem, K extends keyof T =
59
59
  * Minimum height for unused place
60
60
  */
61
61
  minHeightUnusedPlace?: number | string;
62
+ /**
63
+ * Number of leading visible columns to pin on horizontal scroll.
64
+ * `0` / omitted — off. Clamped to at most `visibleColumns.length - 1`.
65
+ * Requires absolute widths (e.g. px/rem) on all visible columns when > 0.
66
+ * @default 0
67
+ */
68
+ stickyColumns?: number;
69
+ /**
70
+ * Custom scrollbar settings.
71
+ * When omitted, native browser overflow is used.
72
+ */
73
+ scrollbar?: {
74
+ /**
75
+ * Which overflow axes to enable.
76
+ * @default 'both'
77
+ */
78
+ axes?: 'vertical' | 'horizontal' | 'both';
79
+ vertical?: {
80
+ thumbMinHeight?: number | string;
81
+ thumbHeight?: number | string | 'auto';
82
+ thumbRadius?: number | string;
83
+ thumbColor?: string;
84
+ thumbHoverColor?: string;
85
+ thumbActiveColor?: string;
86
+ trackWidth?: number;
87
+ trackRadius?: number | string;
88
+ trackColor?: string;
89
+ /** Offset of vertical track from top (usually set by table for sticky header). */
90
+ trackOffsetTop?: number | string;
91
+ };
92
+ horizontal?: {
93
+ thumbMinWidth?: number | string;
94
+ thumbWidth?: number | string | 'auto';
95
+ thumbRadius?: number | string;
96
+ thumbColor?: string;
97
+ thumbHoverColor?: string;
98
+ thumbActiveColor?: string;
99
+ trackHeight?: number | string;
100
+ trackRadius?: number | string;
101
+ trackColor?: string;
102
+ };
103
+ };
62
104
  };
63
105
 
64
106
  /**
@@ -201,6 +243,16 @@ export type Ui3nTableBodyRowSlotScope<T extends Ui3nTableBodyBaseItem, K extends
201
243
  * Column styles
202
244
  */
203
245
  columnStyle?: { [P in Omit<K, 'id'> as string | number]: Record<string, string> };
246
+ /**
247
+ * Effective sticky column count after validation/clamp (`0` = off).
248
+ * Custom `#row`: first `stickyColumnsCount` direct children should be sticky cells
249
+ * with `left: stickyColumnLefts[i]`.
250
+ */
251
+ stickyColumnsCount?: number;
252
+ /**
253
+ * CSS `left` values for sticky columns `[0 .. stickyColumnsCount - 1]`.
254
+ */
255
+ stickyColumnLefts?: string[];
204
256
  /**
205
257
  * Events
206
258
  */