@ulu/frontend-vue 0.5.3 → 0.5.5

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 (41) hide show
  1. package/dist/components/elements/UluCounterList.vue.d.ts +32 -0
  2. package/dist/components/elements/UluCounterList.vue.d.ts.map +1 -0
  3. package/dist/components/elements/UluCounterList.vue.js +77 -0
  4. package/dist/components/elements/UluDataTable.vue.d.ts +43 -0
  5. package/dist/components/elements/UluDataTable.vue.d.ts.map +1 -0
  6. package/dist/components/elements/UluDataTable.vue.js +114 -0
  7. package/dist/components/elements/UluDefinitionList.vue.d.ts +2 -2
  8. package/dist/components/elements/UluList.vue.d.ts +4 -0
  9. package/dist/components/elements/UluList.vue.d.ts.map +1 -1
  10. package/dist/components/elements/UluList.vue.js +40 -23
  11. package/dist/components/elements/UluListItem.vue.d.ts +4 -2
  12. package/dist/components/elements/UluListItem.vue.d.ts.map +1 -1
  13. package/dist/components/elements/UluListItem.vue.js +17 -10
  14. package/dist/components/elements/UluTable.vue.d.ts +48 -0
  15. package/dist/components/elements/UluTable.vue.d.ts.map +1 -0
  16. package/dist/components/elements/UluTable.vue.js +211 -0
  17. package/dist/components/index.d.ts +3 -0
  18. package/dist/components/systems/table-sticky/UluTableSticky.vue.d.ts +104 -104
  19. package/dist/components/systems/table-sticky/UluTableSticky.vue.d.ts.map +1 -1
  20. package/dist/components/systems/table-sticky/UluTableSticky.vue.js +218 -256
  21. package/dist/components/systems/table-sticky/UluTableStickyRows.vue.d.ts +4 -4
  22. package/dist/components/systems/table-sticky/UluTableStickyTable.vue.d.ts +12 -12
  23. package/dist/components/utils/UluAction.vue.js +1 -1
  24. package/dist/components/utils/UluPlaceholderImage.vue.d.ts +2 -2
  25. package/dist/composables/index.d.ts +1 -0
  26. package/dist/composables/useTableData.d.ts +30 -0
  27. package/dist/composables/useTableData.d.ts.map +1 -0
  28. package/dist/composables/useTableData.js +68 -0
  29. package/dist/index.js +190 -182
  30. package/lib/components/elements/UluCounterList.vue +77 -0
  31. package/lib/components/elements/UluDataTable.vue +115 -0
  32. package/lib/components/elements/UluDefinitionList.vue +1 -1
  33. package/lib/components/elements/UluList.vue +23 -8
  34. package/lib/components/elements/UluListItem.vue +11 -6
  35. package/lib/components/elements/UluTable.vue +217 -0
  36. package/lib/components/index.js +3 -0
  37. package/lib/components/systems/table-sticky/UluTableSticky.vue +26 -171
  38. package/lib/components/utils/UluAction.vue +1 -1
  39. package/lib/composables/index.js +1 -0
  40. package/lib/composables/useTableData.js +189 -0
  41. package/package.json +3 -3
@@ -181,7 +181,7 @@
181
181
  import UluTableStickyTable from "./UluTableStickyTable.vue";
182
182
  import { debounce } from "@ulu/utils/performance.js";
183
183
  import { runAfterFramePaint } from "@ulu/utils/browser/performance.js";
184
- import cloneDeep from "lodash-es/cloneDeep.js";
184
+ import { useTableData } from "../../../composables/useTableData.js";
185
185
 
186
186
  const SSR = import.meta.env.SSR;
187
187
  const getWindowWidth = () => SSR ? 0 : window.innerWidth;
@@ -300,117 +300,28 @@
300
300
  const display = ref(null);
301
301
  const table = ref(null);
302
302
 
303
- const idCreator = (type) => {
304
- let id = 0;
305
- return () => `${ props.idPrefix }-${ type }-${ ++id }`;
306
- };
307
-
308
- /**
309
- * Creates column array for internal use
310
- * - Avoid mutating user's prop
311
- * - Current columns being used in the display
312
- * - This internal copy has internal properties/structural info (like ID)
313
- * - This is the copy of the users columns to avoid mutating their object
314
- * - Can be used in the future for adding/removing or enabling/disabling
315
- */
316
- const createColumns = () => {
317
- const newId = idCreator("c");
318
- const columns = cloneDeep(props.columns);
319
- const prep = (column, parent) => {
320
- column.id = newId();
321
- column.parent = parent;
322
- column.width = "auto";
323
- column.boxWidth = null;
324
- column.sortApplied = false;
325
- column.sortAscending = false;
326
- column.sortFocused = false;
327
- let headers = [];
328
- // Add the column's headers for output to attribute
329
- if (parent) {
330
- if (parent.headers && parent.headers.length) {
331
- headers = [ ...parent.headers ];
332
- } else {
333
- headers.push(parent.id);
334
- }
335
- }
336
- headers.push(column.id);
337
- column.headers = headers;
338
- // Call the function on this column's children
339
- if (column.columns) {
340
- column.columns.forEach(c => prep(c, column));
341
- // Make sure column has a required properties
342
- } else if (!column.key && !column.value && !column.slot) {
343
- console.warn("UluTableSticky: Missing 'key', 'value' or 'slot' in column configuration for", column);
344
- }
345
- };
346
- columns.forEach(c => prep(c, null));
347
- return columns;
348
- };
349
-
350
- /**
351
- * Recursive function used as a reducer to return the deepest nested columns
352
- */
353
- const maxColumnChildren = (d, c) => {
354
- const m = c.columns ? c.columns.reduce(maxColumnChildren, 1) + 1 : 1;
355
- return d > m ? d : m;
356
- };
357
-
358
303
  /**
359
- * Conversion of the columns (which are nested hierarchy) to a flat list of columns
360
- * sorted by the way they need to be displayed in rows
361
- * - Used for nested headers
362
- * - Transform nested data into row arrays
304
+ * Method to update the table (sizes, etc) when data has changed
363
305
  */
364
- const createHeaderRows = (currentColumnsData) => {
365
- // Create empty row array, each array will hold it's columns
366
- const newId = idCreator("hr");
367
- const count = currentColumnsData.reduce(maxColumnChildren, 1);
368
- const height = "auto";
369
- const rows = new Array(count).fill(null).map(() => ({
370
- height,
371
- boxHeight: null,
372
- columns: [],
373
- id: newId()
374
- }));
375
-
376
- /**
377
- * Function that adds columns to the rows array's based
378
- * on their depth, called recursively.
379
- */
380
- function setInRows(depth, column) {
381
- const columns = column.columns;
382
- // Go to inward to the deepest child
383
- if (columns) columns.forEach(c => setInRows(1 + depth, c));
384
- // Now that the deepest children have been calculated and pushed we have
385
- // all the information we need to determine the parent's colspan by reducing
386
- // the parents children's colspans and children would include their children
387
- column.rowspan = columns ? 1 : count - depth;
388
- column.colspan = columns ? columns.reduce((a, c) => a + c.colspan, 0) : 1;
389
- rows[depth].columns.push(column);
390
- }
391
- currentColumnsData.forEach(c => setInRows(0, c));
392
- return rows;
306
+ const refresh = () => {
307
+ if (SSR) return;
308
+ removeTableSizes();
309
+ nextTick(() => {
310
+ setTableSizes();
311
+ checkOverflowX();
312
+ checkScrollability();
313
+ syncScrollLeft();
314
+ });
393
315
  };
394
316
 
395
- /**
396
- * Creates row array for internal use
397
- * - Avoid mutating user's prop
398
- */
399
- const createRows = (forFooter) => {
400
- const newId = idCreator(forFooter ? "fr" : "br");
401
- const currentRows = forFooter ? props.footerRows : props.rows;
402
- return currentRows ? currentRows.map(row => ({
403
- height: null,
404
- boxHeight: null,
405
- data: row,
406
- id: newId()
407
- })) : [];
408
- };
317
+ const {
318
+ currentColumns,
319
+ currentRows,
320
+ currentFooterRows,
321
+ headerRows,
322
+ rowColumns
323
+ } = useTableData(props, refresh);
409
324
 
410
- const currentColumns = ref(createColumns());
411
- const currentRows = ref(createRows());
412
- const currentFooterRows = ref(createRows(true));
413
- const headerRows = ref(createHeaderRows(currentColumns.value));
414
325
  const sizesCalculated = ref(false);
415
326
  const tableWidth = ref("auto");
416
327
  const resizing = ref(false);
@@ -428,49 +339,19 @@
428
339
  const headerVisibleX = computed(() => sizesCalculated.value && overflownX.value);
429
340
  const headerOpacityX = computed(() => headerVisibleX.value ? "1" : "0");
430
341
 
431
- /**
432
- * Used to output the body rows. This is an array of only the deepest child columns
433
- * parent column information can be accessed by reference
434
- */
435
- const rowColumns = computed(() => {
436
- const columns = currentColumns.value;
437
- const rc = [];
438
- const add = c => {
439
- if (c.columns) c.columns.forEach(add);
440
- else rc.push(c);
441
- };
442
- // Create array of actual
443
- columns.forEach(add);
444
-
445
- // Iterate over all columns checking for rowHeader
446
- // - If a column has row header create an id function passed current row's index
447
- // - Store callbacks in an array to call on each rows cells
448
- let rowHeaders = [];
449
- rc.forEach((c, columnIndex) => {
450
- // Creating copy of array here so it doesn't include it's own ID and also
451
- // so there can be headers of headers going from left to right only
452
- const thisRowsHeader = rowHeaders.slice();
453
- c.getRowHeaders = rowIndex => thisRowsHeader.map(cb => cb(rowIndex)).join(" ");
454
- // Now we add this columns row header function
455
- // Which will be included in all columns after this iteration
456
- if (c.rowHeader) {
457
- c.getRowHeaderId = rowIndex => `${ props.idPrefix }-rh-${ rowIndex }-${ columnIndex }`;
458
- rowHeaders.push(c.getRowHeaderId);
459
- }
460
- });
461
- return rc;
462
- });
463
-
464
342
  const headerHeight = computed(() => {
465
343
  // Offset height would be the combination of all the rows height's
466
- return headerRows.value.reduce((a, r) => a + r.boxHeight, 0);
344
+ return headerRows.value ? headerRows.value.reduce((a, r) => a + r.boxHeight, 0) : 0;
467
345
  });
468
346
 
469
347
  /**
470
348
  * Reduce the array of column header rows to the first row, first column
471
349
  */
472
350
  const headerRowsFirst = computed(() => {
351
+ if (!headerRows.value?.length) return [];
473
352
  const firstRow = headerRows.value[0];
353
+ if (!firstRow?.columns?.length) return [];
354
+
474
355
  const firstColumn = Object.assign({}, firstRow.columns[0], { rowspan: 1, colspan: 1 });
475
356
  const columns = [ firstColumn ];
476
357
  return [{
@@ -485,10 +366,14 @@
485
366
  * Reduce the rowColumn array to only the first column
486
367
  */
487
368
  const rowColumnsFirst = computed(() => {
369
+ if (!rowColumns.value || !rowColumns.value.length) return [];
488
370
  return [ rowColumns.value[0] ];
489
371
  });
490
372
 
491
373
  const firstColumnSize = computed(() => {
374
+ if (!headerRowsFirst.value?.length || !headerRows.value?.[0]?.columns?.length) {
375
+ return { width: "auto", height: "auto" };
376
+ }
492
377
  const height = headerRowsFirst.value[0].height;
493
378
  const width = headerRows.value[0].columns[0].width;
494
379
  return { width, height };
@@ -598,20 +483,6 @@
598
483
 
599
484
  const resizeHandler = debounce(onResize, 500, true);
600
485
 
601
- /**
602
- * Method to update the table (sizes, etc) when data has changed
603
- */
604
- const refresh = () => {
605
- if (SSR) return;
606
- removeTableSizes();
607
- nextTick(() => {
608
- setTableSizes();
609
- checkOverflowX();
610
- checkScrollability();
611
- syncScrollLeft();
612
- });
613
- };
614
-
615
486
  const onScrollX = () => {
616
487
  checkScrollability();
617
488
  syncScrollLeft();
@@ -763,22 +634,6 @@
763
634
  // };
764
635
  // };
765
636
 
766
- watch(() => props.columns, () => {
767
- currentColumns.value = createColumns();
768
- headerRows.value = createHeaderRows(currentColumns.value);
769
- refresh();
770
- }, { deep: true });
771
-
772
- watch(() => props.rows, () => {
773
- currentRows.value = createRows();
774
- refresh();
775
- }, { deep: true });
776
-
777
- watch(() => props.footerRows, () => {
778
- currentFooterRows.value = createRows(true);
779
- refresh();
780
- }, { deep: true });
781
-
782
637
  onMounted(() => {
783
638
  if (!SSR) {
784
639
  attachHandlers();
@@ -69,7 +69,7 @@
69
69
  attrs.href = props.href;
70
70
  if (props.target) attrs.target = props.target;
71
71
  if (props.download) {
72
- attrs.download = typeof props.download === "string" ? props.download : true;
72
+ attrs.download = typeof props.download === "string" ? props.download : "";
73
73
  }
74
74
  } else if (!props.element || props.element === "button") {
75
75
  // It's a button, ensure it doesn't accidentally submit forms unless requested
@@ -12,3 +12,4 @@ export { useBreakpointManager } from './useBreakpointManager.js';
12
12
  export { usePagination } from './usePagination.js';
13
13
  export { useDocumentTitle } from './useDocumentTitle.js';
14
14
  export { useUluFloating } from "./useUluFloating.js";
15
+ export { useTableData } from "./useTableData.js";
@@ -0,0 +1,189 @@
1
+ import { ref, computed, watch } from 'vue';
2
+
3
+ /**
4
+ * Composable for managing and normalizing table data structures.
5
+ * Generates rows, columns, and accessibility attributes for complex tables.
6
+ *
7
+ * @param {Object} props - The component props containing columns, rows, footerRows, and idPrefix.
8
+ * @param {Function} [onChange] - Optional callback triggered when rows or columns data change (useful for layout recalculation).
9
+ */
10
+ export function useTableData(props, onChange = () => {}) {
11
+ const idCreator = (type) => {
12
+ let id = 0;
13
+ return () => `${props.idPrefix || 'table'}-${type}-${++id}`;
14
+ };
15
+
16
+ /**
17
+ * Creates column array for internal use
18
+ * - Avoid mutating user's prop
19
+ * - Current columns being used in the display
20
+ * - This internal copy has internal properties/structural info (like ID)
21
+ * - This is the copy of the users columns to avoid mutating their object
22
+ * - Can be used in the future for adding/removing or enabling/disabling
23
+ */
24
+ const createColumns = () => {
25
+ if (!props.columns) return [];
26
+ const newId = idCreator("c");
27
+
28
+ const prep = (colDef, parent) => {
29
+ // Create a shallow copy of the user's object so we don't mutate the prop
30
+ const column = { ...colDef };
31
+
32
+ column.id = newId();
33
+ column.parent = parent;
34
+ // Sticky table specific default resets (ignored by simple table)
35
+ column.width = "auto";
36
+ column.boxWidth = null;
37
+ column.sortApplied = false;
38
+ column.sortAscending = false;
39
+ column.sortFocused = false;
40
+
41
+ let headers = [];
42
+ // Add the column's headers for output to attribute
43
+ if (parent) {
44
+ if (parent.headers && parent.headers.length) {
45
+ headers = [ ...parent.headers ];
46
+ } else {
47
+ headers.push(parent.id);
48
+ }
49
+ }
50
+ headers.push(column.id);
51
+ column.headers = headers;
52
+
53
+ // Call the function on this column's children, reassigning the new array
54
+ if (column.columns) {
55
+ column.columns = column.columns.map(c => prep(c, column));
56
+ // Make sure column has a required properties
57
+ } else if (!column.key && !column.value && !column.slot) {
58
+ console.warn("useTableData: Missing 'key', 'value' or 'slot' in column configuration for", column);
59
+ }
60
+
61
+ return column;
62
+ };
63
+
64
+ return props.columns.map(c => prep(c, null));
65
+ };
66
+
67
+ /**
68
+ * Recursive function used as a reducer to return the deepest nested columns
69
+ */
70
+ const maxColumnChildren = (d, c) => {
71
+ const m = c.columns ? c.columns.reduce(maxColumnChildren, 1) + 1 : 1;
72
+ return d > m ? d : m;
73
+ };
74
+
75
+ /**
76
+ * Conversion of the columns (which are nested hierarchy) to a flat list of columns
77
+ * sorted by the way they need to be displayed in rows
78
+ * - Used for nested headers
79
+ * - Transform nested data into row arrays
80
+ */
81
+ const createHeaderRows = (currentColumnsData) => {
82
+ if (!currentColumnsData || currentColumnsData.length === 0) return [];
83
+ // Create empty row array, each array will hold it's columns
84
+ const newId = idCreator("hr");
85
+ const count = currentColumnsData.reduce(maxColumnChildren, 1);
86
+ const rows = new Array(count).fill(null).map(() => ({
87
+ height: "auto",
88
+ boxHeight: null,
89
+ columns: [],
90
+ id: newId()
91
+ }));
92
+
93
+ /**
94
+ * Function that adds columns to the rows array's based
95
+ * on their depth, called recursively.
96
+ */
97
+ function setInRows(depth, column) {
98
+ const columns = column.columns;
99
+ // Go to inward to the deepest child
100
+ if (columns) columns.forEach(c => setInRows(1 + depth, c));
101
+ // Now that the deepest children have been calculated and pushed we have
102
+ // all the information we need to determine the parent's colspan by reducing
103
+ // the parents children's colspans and children would include their children
104
+ column.rowspan = columns ? 1 : count - depth;
105
+ column.colspan = columns ? columns.reduce((a, c) => a + c.colspan, 0) : 1;
106
+ rows[depth].columns.push(column);
107
+ }
108
+
109
+ currentColumnsData.forEach(c => setInRows(0, c));
110
+ return rows;
111
+ };
112
+
113
+ /**
114
+ * Creates row array for internal use
115
+ * - Avoid mutating user's prop
116
+ */
117
+ const createRows = (sourceRows, isFooter = false) => {
118
+ if (!sourceRows) return [];
119
+ const newId = idCreator(isFooter ? "fr" : "br");
120
+ return sourceRows.map(row => ({
121
+ height: null,
122
+ boxHeight: null,
123
+ data: row,
124
+ id: newId()
125
+ }));
126
+ };
127
+
128
+ const currentColumns = ref(createColumns());
129
+ const currentRows = ref(createRows(props.rows));
130
+ const currentFooterRows = ref(createRows(props.footerRows, true));
131
+ const headerRows = ref(createHeaderRows(currentColumns.value));
132
+
133
+ /**
134
+ * Used to output the body rows. This is an array of only the deepest child columns
135
+ * parent column information can be accessed by reference
136
+ */
137
+ const rowColumns = computed(() => {
138
+ const columns = currentColumns.value;
139
+ const rc = [];
140
+ const add = c => {
141
+ if (c.columns) c.columns.forEach(add);
142
+ else rc.push(c);
143
+ };
144
+ // Create array of actual
145
+ columns.forEach(add);
146
+
147
+ // Iterate over all columns checking for rowHeader
148
+ // - If a column has row header create an id function passed current row's index
149
+ // - Store callbacks in an array to call on each rows cells
150
+ let rowHeaders = [];
151
+ rc.forEach((c, columnIndex) => {
152
+ // Creating copy of array here so it doesn't include it's own ID and also
153
+ // so there can be headers of headers going from left to right only
154
+ const thisRowsHeader = rowHeaders.slice();
155
+ c.getRowHeaders = rowIndex => thisRowsHeader.map(cb => cb(rowIndex)).join(" ");
156
+ // Now we add this columns row header function
157
+ // Which will be included in all columns after this iteration
158
+ if (c.rowHeader) {
159
+ c.getRowHeaderId = rowIndex => `${props.idPrefix || 'table'}-rh-${rowIndex}-${columnIndex}`;
160
+ rowHeaders.push(c.getRowHeaderId);
161
+ }
162
+ });
163
+ return rc;
164
+ });
165
+
166
+ watch(() => props.columns, () => {
167
+ currentColumns.value = createColumns();
168
+ headerRows.value = createHeaderRows(currentColumns.value);
169
+ onChange();
170
+ }, { deep: true });
171
+
172
+ watch(() => props.rows, () => {
173
+ currentRows.value = createRows(props.rows);
174
+ onChange();
175
+ }, { deep: true });
176
+
177
+ watch(() => props.footerRows, () => {
178
+ currentFooterRows.value = createRows(props.footerRows, true);
179
+ onChange();
180
+ }, { deep: true });
181
+
182
+ return {
183
+ currentColumns,
184
+ currentRows,
185
+ currentFooterRows,
186
+ headerRows,
187
+ rowColumns
188
+ };
189
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulu/frontend-vue",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "A modular, tree-shakeable Vue 3 component library for the Ulu Frontend theming system, plus general utilities for Vue development",
5
5
  "type": "module",
6
6
  "files": [
@@ -65,7 +65,7 @@
65
65
  "@fortawesome/vue-fontawesome": "^3.0.8",
66
66
  "@headlessui/vue": "^1.7.23",
67
67
  "@portabletext/vue": "^1.0.14",
68
- "@ulu/frontend": "^0.4.3",
68
+ "@ulu/frontend": "^0.4.6",
69
69
  "@ulu/utils": "^0.0.34",
70
70
  "@unhead/vue": "^2.0.11",
71
71
  "fuse.js": "^6.6.2",
@@ -87,7 +87,7 @@
87
87
  "@storybook/addon-essentials": "^9.0.0-alpha.12",
88
88
  "@storybook/addon-links": "^9.1.1",
89
89
  "@storybook/vue3-vite": "^9.1.1",
90
- "@ulu/frontend": "^0.4.3",
90
+ "@ulu/frontend": "^0.4.6",
91
91
  "@ulu/utils": "^0.0.34",
92
92
  "@unhead/vue": "^2.0.11",
93
93
  "@vitejs/plugin-vue": "^6.0.0",