@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
|
@@ -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;
|