@gkucmierz/utils 5.4.0 → 5.4.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gkucmierz/utils",
3
- "version": "5.4.0",
3
+ "version": "5.4.1",
4
4
  "type": "module",
5
5
  "description": "Usefull functions for solving programming tasks",
6
6
  "keywords": [
@@ -2,9 +2,65 @@
2
2
  * @module developer-utils
3
3
  */
4
4
 
5
+ /**
6
+ * Builds a 2D occupancy matrix `rows[r][c] = itemIndex` (or `-1` for empty gap)
7
+ * when `spanLastRow` or `colSpans` is enabled.
8
+ *
9
+ * @param {number} totalItems
10
+ * @param {number} cols
11
+ * @param {Object} opts
12
+ * @returns {{ matrix: number[][], itemPos: Array<{ row: number, cols: number[] }> }}
13
+ */
14
+ const buildSpannedGridMatrix = (totalItems, cols, opts = {}) => {
15
+ const spanLastRow = Boolean(opts.spanLastRow);
16
+ const colSpans = opts.colSpans || null;
17
+ const matrix = [];
18
+ const itemPos = Array.from({ length: totalItems }, () => ({ row: 0, cols: [] }));
19
+
20
+ let curRow = new Array(cols).fill(-1);
21
+ let c = 0;
22
+
23
+ for (let i = 0; i < totalItems; i += 1) {
24
+ let span = 1;
25
+ if (colSpans) {
26
+ const rawSpan = Array.isArray(colSpans) ? colSpans[i] : colSpans[i];
27
+ if (Number.isFinite(Number(rawSpan)) && Number(rawSpan) >= 1) {
28
+ span = Math.min(cols, Math.floor(Number(rawSpan)));
29
+ }
30
+ }
31
+ if (c + span > cols) {
32
+ matrix.push(curRow);
33
+ curRow = new Array(cols).fill(-1);
34
+ c = 0;
35
+ }
36
+ if (spanLastRow && i === totalItems - 1 && c < cols) {
37
+ span = cols - c;
38
+ }
39
+ const rowIdx = matrix.length;
40
+ itemPos[i].row = rowIdx;
41
+ for (let k = 0; k < span && c < cols; k += 1) {
42
+ curRow[c] = i;
43
+ itemPos[i].cols.push(c);
44
+ c += 1;
45
+ }
46
+ if (c >= cols) {
47
+ matrix.push(curRow);
48
+ curRow = new Array(cols).fill(-1);
49
+ c = 0;
50
+ }
51
+ }
52
+
53
+ if (c > 0) {
54
+ matrix.push(curRow);
55
+ }
56
+
57
+ return { matrix, itemPos };
58
+ };
59
+
5
60
  /**
6
61
  * Resolves the next 0-based index when navigating a 2D grid (matrix) that may contain
7
- * trailing gaps in its last row (`totalItems` not divisible by `columns`).
62
+ * trailing gaps in its last row (`totalItems` not divisible by `columns`) or spanned cells
63
+ * (`spanLastRow: true` / `colSpans` / `preferredCol`).
8
64
  *
9
65
  * Pure mathematical/index function — completely independent of the DOM.
10
66
  *
@@ -13,6 +69,9 @@
13
69
  * - `wrapHoriz` / `wrapHorizontal` (boolean, optional): overrides `wrap` for horizontal movement (`left` / `right`).
14
70
  * - `wrapVert` / `wrapVertical` (boolean, optional): overrides `wrap` for vertical movement (`up` / `down`).
15
71
  * - `rowStrict` (boolean, default `false`): when `true`, horizontal movement wraps/clamps strictly within the current row instead of reading order.
72
+ * - `spanLastRow` (boolean, default `false`): when `true`, the final item (`totalItems - 1`) spans all remaining columns in the last row (e.g. a 2x3 grid where row 2 has a full-width card `[4, 4]`).
73
+ * - `colSpans` (number[]|Object, optional): explicit column span per item index (`>= 1`).
74
+ * - `preferredCol` (number, optional): preferred column index (`0 <= preferredCol < columns`) preserved when stepping vertically out of a multi-column spanned item.
16
75
  *
17
76
  * When `columns === 1` (single-column layout), `right` behaves identically to `down`
18
77
  * and `left` behaves identically to `up`.
@@ -60,6 +119,7 @@ export const navigateGrid = (
60
119
  ? opts.wrapVert
61
120
  : baseWrap;
62
121
  const rowStrict = Boolean(opts.rowStrict);
122
+ const hasSpans = Boolean(opts.spanLastRow || opts.colSpans);
63
123
 
64
124
  const row = Math.floor(cur / cols);
65
125
  const col = cur % cols;
@@ -91,6 +151,26 @@ export const navigateGrid = (
91
151
  // Horizontal movement (left / right)
92
152
  if (deltaH !== 0) {
93
153
  if (rowStrict) {
154
+ if (hasSpans && cols > 1) {
155
+ const { matrix, itemPos } = buildSpannedGridMatrix(totalItems, cols, opts);
156
+ const curRowIdx = itemPos[cur]?.row ?? 0;
157
+ const rowItems = [];
158
+ for (const cellItem of matrix[curRowIdx] || []) {
159
+ if (cellItem >= 0 && !rowItems.includes(cellItem)) {
160
+ rowItems.push(cellItem);
161
+ }
162
+ }
163
+ const posInRow = rowItems.indexOf(cur);
164
+ if (posInRow < 0 || rowItems.length <= 0) return cur;
165
+ const nextPos = posInRow + deltaH;
166
+ if (nextPos >= 0 && nextPos < rowItems.length) {
167
+ return rowItems[nextPos];
168
+ }
169
+ if (wrapHoriz) {
170
+ return rowItems[((nextPos % rowItems.length) + rowItems.length) % rowItems.length];
171
+ }
172
+ return cur;
173
+ }
94
174
  const rowStart = row * cols;
95
175
  const rowLen = Math.min(cols, totalItems - rowStart);
96
176
  if (rowLen <= 0) return cur;
@@ -114,6 +194,42 @@ export const navigateGrid = (
114
194
  return cur;
115
195
  }
116
196
 
197
+ // Vertical movement (up / down) when spanned cells are configured
198
+ if (hasSpans && cols > 1) {
199
+ const { matrix, itemPos } = buildSpannedGridMatrix(totalItems, cols, opts);
200
+ const curInfo = itemPos[cur] || { row: 0, cols: [0] };
201
+ const occupiedCols = curInfo.cols.length > 0 ? curInfo.cols : [0];
202
+ let targetCol = occupiedCols[0];
203
+ if (occupiedCols.length > 1 && Number.isFinite(Number(opts.preferredCol))) {
204
+ const pref = Math.max(0, Math.min(cols - 1, Math.floor(Number(opts.preferredCol))));
205
+ if (occupiedCols.includes(pref)) {
206
+ targetCol = pref;
207
+ }
208
+ }
209
+
210
+ const validRowsForCol = [];
211
+ for (let r = 0; r < matrix.length; r += 1) {
212
+ if (matrix[r][targetCol] >= 0) {
213
+ validRowsForCol.push(r);
214
+ }
215
+ }
216
+ if (validRowsForCol.length === 0) return cur;
217
+
218
+ const curRowPos = validRowsForCol.indexOf(curInfo.row);
219
+ const baseRowPos = curRowPos >= 0 ? curRowPos : 0;
220
+ const nextRowPos = baseRowPos + deltaV;
221
+ if (nextRowPos >= 0 && nextRowPos < validRowsForCol.length) {
222
+ return matrix[validRowsForCol[nextRowPos]][targetCol];
223
+ }
224
+ if (wrapVert) {
225
+ const wrappedPos =
226
+ ((nextRowPos % validRowsForCol.length) + validRowsForCol.length) %
227
+ validRowsForCol.length;
228
+ return matrix[validRowsForCol[wrappedPos]][targetCol];
229
+ }
230
+ return cur;
231
+ }
232
+
117
233
  // Vertical movement (up / down) along column `col` (handling incomplete last row gaps)
118
234
  const colRows = col < totalItems ? Math.floor((totalItems - 1 - col) / cols) + 1 : 0;
119
235
  if (colRows <= 0) return cur;