gridstack 12.3.2 → 12.3.3

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 (85) hide show
  1. package/dist/angular/esm2020/lib/base-widget.mjs +2 -2
  2. package/dist/angular/esm2020/lib/gridstack-item.component.mjs +2 -2
  3. package/dist/angular/esm2020/lib/gridstack.component.mjs +2 -2
  4. package/dist/angular/esm2020/lib/gridstack.module.mjs +2 -2
  5. package/dist/angular/esm2020/lib/types.mjs +2 -2
  6. package/dist/angular/fesm2015/gridstack-angular.mjs +4 -4
  7. package/dist/angular/fesm2015/gridstack-angular.mjs.map +1 -1
  8. package/dist/angular/fesm2020/gridstack-angular.mjs +5 -5
  9. package/dist/angular/fesm2020/gridstack-angular.mjs.map +1 -1
  10. package/dist/angular/lib/gridstack-item.component.d.ts +1 -1
  11. package/dist/angular/lib/gridstack.component.d.ts +1 -1
  12. package/dist/angular/lib/types.d.ts +1 -1
  13. package/dist/angular/package.json +1 -1
  14. package/dist/angular/src/base-widget.ts +1 -1
  15. package/dist/angular/src/gridstack-item.component.ts +1 -1
  16. package/dist/angular/src/gridstack.component.ts +1 -1
  17. package/dist/angular/src/gridstack.module.ts +1 -1
  18. package/dist/angular/src/types.ts +1 -1
  19. package/dist/gridstack-all.js +1 -1
  20. package/dist/gridstack-all.js.LICENSE.txt +1 -1
  21. package/dist/gridstack-all.js.map +1 -1
  22. package/dist/gridstack.css +1 -1
  23. package/dist/spec/gridstack-engine-spec.d.ts +1 -0
  24. package/dist/spec/gridstack-engine-spec.js +358 -0
  25. package/dist/spec/gridstack-engine-spec.js.map +1 -0
  26. package/dist/spec/gridstack-spec.d.ts +1 -0
  27. package/dist/spec/gridstack-spec.js +1780 -0
  28. package/dist/spec/gridstack-spec.js.map +1 -0
  29. package/dist/spec/integration/gridstack-integration.spec.d.ts +1 -0
  30. package/dist/spec/integration/gridstack-integration.spec.js +171 -0
  31. package/dist/spec/integration/gridstack-integration.spec.js.map +1 -0
  32. package/dist/spec/regression-spec.d.ts +1 -0
  33. package/dist/spec/regression-spec.js +100 -0
  34. package/dist/spec/regression-spec.js.map +1 -0
  35. package/dist/spec/utils-spec.d.ts +1 -0
  36. package/dist/spec/utils-spec.js +243 -0
  37. package/dist/spec/utils-spec.js.map +1 -0
  38. package/dist/src/dd-base-impl.d.ts +69 -0
  39. package/dist/src/dd-base-impl.js +70 -0
  40. package/dist/src/dd-base-impl.js.map +1 -0
  41. package/dist/src/dd-draggable.d.ts +20 -0
  42. package/dist/src/dd-draggable.js +364 -0
  43. package/dist/src/dd-draggable.js.map +1 -0
  44. package/dist/src/dd-droppable.d.ts +26 -0
  45. package/dist/src/dd-droppable.js +149 -0
  46. package/dist/src/dd-droppable.js.map +1 -0
  47. package/dist/src/dd-element.d.ts +27 -0
  48. package/dist/src/dd-element.js +91 -0
  49. package/dist/src/dd-element.js.map +1 -0
  50. package/dist/src/dd-gridstack.d.ts +82 -0
  51. package/dist/src/dd-gridstack.js +165 -0
  52. package/dist/src/dd-gridstack.js.map +1 -0
  53. package/dist/src/dd-manager.d.ts +43 -0
  54. package/dist/src/dd-manager.js +14 -0
  55. package/dist/src/dd-manager.js.map +1 -0
  56. package/dist/src/dd-resizable-handle.d.ts +18 -0
  57. package/dist/src/dd-resizable-handle.js +113 -0
  58. package/dist/src/dd-resizable-handle.js.map +1 -0
  59. package/dist/src/dd-resizable.d.ts +30 -0
  60. package/dist/src/dd-resizable.js +304 -0
  61. package/dist/src/dd-resizable.js.map +1 -0
  62. package/dist/src/dd-touch.d.ts +33 -0
  63. package/dist/src/dd-touch.js +145 -0
  64. package/dist/src/dd-touch.js.map +1 -0
  65. package/dist/src/gridstack-engine.d.ts +321 -0
  66. package/dist/src/gridstack-engine.js +1272 -0
  67. package/dist/src/gridstack-engine.js.map +1 -0
  68. package/dist/src/gridstack.d.ts +802 -0
  69. package/dist/src/gridstack.js +2872 -0
  70. package/dist/src/gridstack.js.map +1 -0
  71. package/dist/src/gridstack.scss +1 -1
  72. package/dist/src/types.d.ts +427 -0
  73. package/dist/src/types.js +38 -0
  74. package/dist/src/types.js.map +1 -0
  75. package/dist/src/utils.d.ts +283 -0
  76. package/dist/src/utils.js +790 -0
  77. package/dist/src/utils.js.map +1 -0
  78. package/dist/vitest.config.d.ts +2 -0
  79. package/dist/vitest.config.js +74 -0
  80. package/dist/vitest.config.js.map +1 -0
  81. package/dist/vitest.setup.d.ts +1 -0
  82. package/dist/vitest.setup.js +90 -0
  83. package/dist/vitest.setup.js.map +1 -0
  84. package/doc/API.md +22 -22
  85. package/package.json +21 -9
@@ -0,0 +1,1272 @@
1
+ /**
2
+ * gridstack-engine.ts 12.3.3
3
+ * Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
4
+ */
5
+ import { Utils } from './utils';
6
+ /**
7
+ * Defines the GridStack engine that handles all grid layout calculations and node positioning.
8
+ * This is the core engine that performs grid manipulation without any DOM operations.
9
+ *
10
+ * The engine manages:
11
+ * - Node positioning and collision detection
12
+ * - Layout algorithms (compact, float, etc.)
13
+ * - Grid resizing and column changes
14
+ * - Widget movement and resizing logic
15
+ *
16
+ * NOTE: Values should not be modified directly - use the main GridStack API instead
17
+ * to ensure proper DOM updates and event triggers.
18
+ */
19
+ class GridStackEngine {
20
+ constructor(opts = {}) {
21
+ this.addedNodes = [];
22
+ this.removedNodes = [];
23
+ this.defaultColumn = 12;
24
+ this.column = opts.column || this.defaultColumn;
25
+ if (this.column > this.defaultColumn)
26
+ this.defaultColumn = this.column;
27
+ this.maxRow = opts.maxRow;
28
+ this._float = opts.float;
29
+ this.nodes = opts.nodes || [];
30
+ this.onChange = opts.onChange;
31
+ }
32
+ /**
33
+ * Enable/disable batch mode for multiple operations to optimize performance.
34
+ * When enabled, layout updates are deferred until batch mode is disabled.
35
+ *
36
+ * @param flag true to enable batch mode, false to disable and apply changes
37
+ * @param doPack if true (default), pack/compact nodes when disabling batch mode
38
+ * @returns the engine instance for chaining
39
+ *
40
+ * @example
41
+ * // Start batch mode for multiple operations
42
+ * engine.batchUpdate(true);
43
+ * engine.addNode(node1);
44
+ * engine.addNode(node2);
45
+ * engine.batchUpdate(false); // Apply all changes at once
46
+ */
47
+ batchUpdate(flag = true, doPack = true) {
48
+ if (!!this.batchMode === flag)
49
+ return this;
50
+ this.batchMode = flag;
51
+ if (flag) {
52
+ this._prevFloat = this._float;
53
+ this._float = true; // let things go anywhere for now... will restore and possibly reposition later
54
+ this.cleanNodes();
55
+ this.saveInitial(); // since begin update (which is called multiple times) won't do this
56
+ }
57
+ else {
58
+ this._float = this._prevFloat;
59
+ delete this._prevFloat;
60
+ if (doPack)
61
+ this._packNodes();
62
+ this._notify();
63
+ }
64
+ return this;
65
+ }
66
+ // use entire row for hitting area (will use bottom reverse sorted first) if we not actively moving DOWN and didn't already skip
67
+ _useEntireRowArea(node, nn) {
68
+ return (!this.float || this.batchMode && !this._prevFloat) && !this._hasLocked && (!node._moving || node._skipDown || nn.y <= node.y);
69
+ }
70
+ /** @internal fix collision on given 'node', going to given new location 'nn', with optional 'collide' node already found.
71
+ * return true if we moved. */
72
+ _fixCollisions(node, nn = node, collide, opt = {}) {
73
+ this.sortNodes(-1); // from last to first, so recursive collision move items in the right order
74
+ collide = collide || this.collide(node, nn); // REAL area collide for swap and skip if none...
75
+ if (!collide)
76
+ return false;
77
+ // swap check: if we're actively moving in gravity mode, see if we collide with an object the same size
78
+ if (node._moving && !opt.nested && !this.float) {
79
+ if (this.swap(node, collide))
80
+ return true;
81
+ }
82
+ // during while() collisions MAKE SURE to check entire row so larger items don't leap frog small ones (push them all down starting last in grid)
83
+ let area = nn;
84
+ if (!this._loading && this._useEntireRowArea(node, nn)) {
85
+ area = { x: 0, w: this.column, y: nn.y, h: nn.h };
86
+ collide = this.collide(node, area, opt.skip); // force new hit
87
+ }
88
+ let didMove = false;
89
+ const newOpt = { nested: true, pack: false };
90
+ let counter = 0;
91
+ while (collide = collide || this.collide(node, area, opt.skip)) { // could collide with more than 1 item... so repeat for each
92
+ if (counter++ > this.nodes.length * 2) {
93
+ throw new Error("Infinite collide check");
94
+ }
95
+ let moved;
96
+ // if colliding with a locked item OR loading (move after) OR moving down with top gravity (and collide could move up) -> skip past the collide,
97
+ // but remember that skip down so we only do this once (and push others otherwise).
98
+ if (collide.locked || this._loading || node._moving && !node._skipDown && nn.y > node.y && !this.float &&
99
+ // can take space we had, or before where we're going
100
+ (!this.collide(collide, { ...collide, y: node.y }, node) || !this.collide(collide, { ...collide, y: nn.y - collide.h }, node))) {
101
+ node._skipDown = (node._skipDown || nn.y > node.y);
102
+ const newNN = { ...nn, y: collide.y + collide.h, ...newOpt };
103
+ // pretent we moved to where we are now so we can continue any collision checks #2492
104
+ moved = this._loading && Utils.samePos(node, newNN) ? true : this.moveNode(node, newNN);
105
+ if ((collide.locked || this._loading) && moved) {
106
+ Utils.copyPos(nn, node); // moving after lock become our new desired location
107
+ }
108
+ else if (!collide.locked && moved && opt.pack) {
109
+ // we moved after and will pack: do it now and keep the original drop location, but past the old collide to see what else we might push way
110
+ this._packNodes();
111
+ nn.y = collide.y + collide.h;
112
+ Utils.copyPos(node, nn);
113
+ }
114
+ didMove = didMove || moved;
115
+ }
116
+ else {
117
+ // move collide down *after* where we will be, ignoring where we are now (don't collide with us)
118
+ moved = this.moveNode(collide, { ...collide, y: nn.y + nn.h, skip: node, ...newOpt });
119
+ }
120
+ if (!moved)
121
+ return didMove; // break inf loop if we couldn't move after all (ex: maxRow, fixed)
122
+ collide = undefined;
123
+ }
124
+ return didMove;
125
+ }
126
+ /**
127
+ * Return the first node that intercepts/collides with the given node or area.
128
+ * Used for collision detection during drag and drop operations.
129
+ *
130
+ * @param skip the node to skip in collision detection (usually the node being moved)
131
+ * @param area the area to check for collisions (defaults to skip node's area)
132
+ * @param skip2 optional second node to skip in collision detection
133
+ * @returns the first colliding node, or undefined if no collision
134
+ *
135
+ * @example
136
+ * const colliding = engine.collide(draggedNode, {x: 2, y: 1, w: 2, h: 1});
137
+ * if (colliding) {
138
+ * console.log('Would collide with:', colliding.id);
139
+ * }
140
+ */
141
+ collide(skip, area = skip, skip2) {
142
+ const skipId = skip._id;
143
+ const skip2Id = skip2?._id;
144
+ return this.nodes.find(n => n._id !== skipId && n._id !== skip2Id && Utils.isIntercepted(n, area));
145
+ }
146
+ /**
147
+ * Return all nodes that intercept/collide with the given node or area.
148
+ * Similar to collide() but returns all colliding nodes instead of just the first.
149
+ *
150
+ * @param skip the node to skip in collision detection
151
+ * @param area the area to check for collisions (defaults to skip node's area)
152
+ * @param skip2 optional second node to skip in collision detection
153
+ * @returns array of all colliding nodes
154
+ *
155
+ * @example
156
+ * const allCollisions = engine.collideAll(draggedNode);
157
+ * console.log('Colliding with', allCollisions.length, 'nodes');
158
+ */
159
+ collideAll(skip, area = skip, skip2) {
160
+ const skipId = skip._id;
161
+ const skip2Id = skip2?._id;
162
+ return this.nodes.filter(n => n._id !== skipId && n._id !== skip2Id && Utils.isIntercepted(n, area));
163
+ }
164
+ /** does a pixel coverage collision based on where we started, returning the node that has the most coverage that is >50% mid line */
165
+ directionCollideCoverage(node, o, collides) {
166
+ if (!o.rect || !node._rect)
167
+ return;
168
+ const r0 = node._rect; // where started
169
+ const r = { ...o.rect }; // where we are
170
+ // update dragged rect to show where it's coming from (above or below, etc...)
171
+ if (r.y > r0.y) {
172
+ r.h += r.y - r0.y;
173
+ r.y = r0.y;
174
+ }
175
+ else {
176
+ r.h += r0.y - r.y;
177
+ }
178
+ if (r.x > r0.x) {
179
+ r.w += r.x - r0.x;
180
+ r.x = r0.x;
181
+ }
182
+ else {
183
+ r.w += r0.x - r.x;
184
+ }
185
+ let collide;
186
+ let overMax = 0.5; // need >50%
187
+ for (let n of collides) {
188
+ if (n.locked || !n._rect) {
189
+ break;
190
+ }
191
+ const r2 = n._rect; // overlapping target
192
+ let yOver = Number.MAX_VALUE, xOver = Number.MAX_VALUE;
193
+ // depending on which side we started from, compute the overlap % of coverage
194
+ // (ex: from above/below we only compute the max horizontal line coverage)
195
+ if (r0.y < r2.y) { // from above
196
+ yOver = ((r.y + r.h) - r2.y) / r2.h;
197
+ }
198
+ else if (r0.y + r0.h > r2.y + r2.h) { // from below
199
+ yOver = ((r2.y + r2.h) - r.y) / r2.h;
200
+ }
201
+ if (r0.x < r2.x) { // from the left
202
+ xOver = ((r.x + r.w) - r2.x) / r2.w;
203
+ }
204
+ else if (r0.x + r0.w > r2.x + r2.w) { // from the right
205
+ xOver = ((r2.x + r2.w) - r.x) / r2.w;
206
+ }
207
+ const over = Math.min(xOver, yOver);
208
+ if (over > overMax) {
209
+ overMax = over;
210
+ collide = n;
211
+ }
212
+ }
213
+ o.collide = collide; // save it so we don't have to find it again
214
+ return collide;
215
+ }
216
+ /** does a pixel coverage returning the node that has the most coverage by area */
217
+ /*
218
+ protected collideCoverage(r: GridStackPosition, collides: GridStackNode[]): {collide: GridStackNode, over: number} {
219
+ const collide: GridStackNode;
220
+ const overMax = 0;
221
+ collides.forEach(n => {
222
+ if (n.locked || !n._rect) return;
223
+ const over = Utils.areaIntercept(r, n._rect);
224
+ if (over > overMax) {
225
+ overMax = over;
226
+ collide = n;
227
+ }
228
+ });
229
+ return {collide, over: overMax};
230
+ }
231
+ */
232
+ /**
233
+ * Cache the pixel rectangles for all nodes used for collision detection during drag operations.
234
+ * This optimization converts grid coordinates to pixel coordinates for faster collision detection.
235
+ *
236
+ * @param w width of a single grid cell in pixels
237
+ * @param h height of a single grid cell in pixels
238
+ * @param top top margin/padding in pixels
239
+ * @param right right margin/padding in pixels
240
+ * @param bottom bottom margin/padding in pixels
241
+ * @param left left margin/padding in pixels
242
+ * @returns the engine instance for chaining
243
+ *
244
+ * @internal This is typically called by GridStack during resize events
245
+ */
246
+ cacheRects(w, h, top, right, bottom, left) {
247
+ this.nodes.forEach(n => n._rect = {
248
+ y: n.y * h + top,
249
+ x: n.x * w + left,
250
+ w: n.w * w - left - right,
251
+ h: n.h * h - top - bottom
252
+ });
253
+ return this;
254
+ }
255
+ /**
256
+ * Attempt to swap the positions of two nodes if they meet swapping criteria.
257
+ * Nodes can swap if they are the same size or in the same column/row, not locked, and touching.
258
+ *
259
+ * @param a first node to swap
260
+ * @param b second node to swap
261
+ * @returns true if swap was successful, false if not possible, undefined if not applicable
262
+ *
263
+ * @example
264
+ * const swapped = engine.swap(nodeA, nodeB);
265
+ * if (swapped) {
266
+ * console.log('Nodes swapped successfully');
267
+ * }
268
+ */
269
+ swap(a, b) {
270
+ if (!b || b.locked || !a || a.locked)
271
+ return false;
272
+ function _doSwap() {
273
+ const x = b.x, y = b.y;
274
+ b.x = a.x;
275
+ b.y = a.y; // b -> a position
276
+ if (a.h != b.h) {
277
+ a.x = x;
278
+ a.y = b.y + b.h; // a -> goes after b
279
+ }
280
+ else if (a.w != b.w) {
281
+ a.x = b.x + b.w;
282
+ a.y = y; // a -> goes after b
283
+ }
284
+ else {
285
+ a.x = x;
286
+ a.y = y; // a -> old b position
287
+ }
288
+ a._dirty = b._dirty = true;
289
+ return true;
290
+ }
291
+ let touching; // remember if we called it (vs undefined)
292
+ // same size and same row or column, and touching
293
+ if (a.w === b.w && a.h === b.h && (a.x === b.x || a.y === b.y) && (touching = Utils.isTouching(a, b)))
294
+ return _doSwap();
295
+ if (touching === false)
296
+ return; // IFF ran test and fail, bail out
297
+ // check for taking same columns (but different height) and touching
298
+ if (a.w === b.w && a.x === b.x && (touching || (touching = Utils.isTouching(a, b)))) {
299
+ if (b.y < a.y) {
300
+ const t = a;
301
+ a = b;
302
+ b = t;
303
+ } // swap a <-> b vars so a is first
304
+ return _doSwap();
305
+ }
306
+ if (touching === false)
307
+ return;
308
+ // check if taking same row (but different width) and touching
309
+ if (a.h === b.h && a.y === b.y && (touching || (touching = Utils.isTouching(a, b)))) {
310
+ if (b.x < a.x) {
311
+ const t = a;
312
+ a = b;
313
+ b = t;
314
+ } // swap a <-> b vars so a is first
315
+ return _doSwap();
316
+ }
317
+ return false;
318
+ }
319
+ /**
320
+ * Check if the specified rectangular area is empty (no nodes occupy any part of it).
321
+ *
322
+ * @param x the x coordinate (column) of the area to check
323
+ * @param y the y coordinate (row) of the area to check
324
+ * @param w the width in columns of the area to check
325
+ * @param h the height in rows of the area to check
326
+ * @returns true if the area is completely empty, false if any node overlaps
327
+ *
328
+ * @example
329
+ * if (engine.isAreaEmpty(2, 1, 3, 2)) {
330
+ * console.log('Area is available for placement');
331
+ * }
332
+ */
333
+ isAreaEmpty(x, y, w, h) {
334
+ const nn = { x: x || 0, y: y || 0, w: w || 1, h: h || 1 };
335
+ return !this.collide(nn);
336
+ }
337
+ /**
338
+ * Re-layout grid items to reclaim any empty space.
339
+ * This optimizes the grid layout by moving items to fill gaps.
340
+ *
341
+ * @param layout layout algorithm to use:
342
+ * - 'compact' (default): find truly empty spaces, may reorder items
343
+ * - 'list': keep the sort order exactly the same, move items up sequentially
344
+ * @param doSort if true (default), sort nodes by position before compacting
345
+ * @returns the engine instance for chaining
346
+ *
347
+ * @example
348
+ * // Compact to fill empty spaces
349
+ * engine.compact();
350
+ *
351
+ * // Compact preserving item order
352
+ * engine.compact('list');
353
+ */
354
+ compact(layout = 'compact', doSort = true) {
355
+ if (this.nodes.length === 0)
356
+ return this;
357
+ if (doSort)
358
+ this.sortNodes();
359
+ const wasBatch = this.batchMode;
360
+ if (!wasBatch)
361
+ this.batchUpdate();
362
+ const wasColumnResize = this._inColumnResize;
363
+ if (!wasColumnResize)
364
+ this._inColumnResize = true; // faster addNode()
365
+ const copyNodes = this.nodes;
366
+ this.nodes = []; // pretend we have no nodes to conflict layout to start with...
367
+ copyNodes.forEach((n, index, list) => {
368
+ let after;
369
+ if (!n.locked) {
370
+ n.autoPosition = true;
371
+ if (layout === 'list' && index)
372
+ after = list[index - 1];
373
+ }
374
+ this.addNode(n, false, after); // 'false' for add event trigger
375
+ });
376
+ if (!wasColumnResize)
377
+ delete this._inColumnResize;
378
+ if (!wasBatch)
379
+ this.batchUpdate(false);
380
+ return this;
381
+ }
382
+ /**
383
+ * Enable/disable floating widgets (default: `false`).
384
+ * When floating is enabled, widgets can move up to fill empty spaces.
385
+ * See [example](http://gridstackjs.com/demo/float.html)
386
+ *
387
+ * @param val true to enable floating, false to disable
388
+ *
389
+ * @example
390
+ * engine.float = true; // Enable floating
391
+ * engine.float = false; // Disable floating (default)
392
+ */
393
+ set float(val) {
394
+ if (this._float === val)
395
+ return;
396
+ this._float = val || false;
397
+ if (!val) {
398
+ this._packNodes()._notify();
399
+ }
400
+ }
401
+ /**
402
+ * Get the current floating mode setting.
403
+ *
404
+ * @returns true if floating is enabled, false otherwise
405
+ *
406
+ * @example
407
+ * const isFloating = engine.float;
408
+ * console.log('Floating enabled:', isFloating);
409
+ */
410
+ get float() { return this._float || false; }
411
+ /**
412
+ * Sort the nodes array from first to last, or reverse.
413
+ * This is called during collision/placement operations to enforce a specific order.
414
+ *
415
+ * @param dir sort direction: 1 for ascending (first to last), -1 for descending (last to first)
416
+ * @returns the engine instance for chaining
417
+ *
418
+ * @example
419
+ * engine.sortNodes(); // Sort ascending (default)
420
+ * engine.sortNodes(-1); // Sort descending
421
+ */
422
+ sortNodes(dir = 1) {
423
+ this.nodes = Utils.sort(this.nodes, dir);
424
+ return this;
425
+ }
426
+ /** @internal called to top gravity pack the items back OR revert back to original Y positions when floating */
427
+ _packNodes() {
428
+ if (this.batchMode) {
429
+ return this;
430
+ }
431
+ this.sortNodes(); // first to last
432
+ if (this.float) {
433
+ // restore original Y pos
434
+ this.nodes.forEach(n => {
435
+ if (n._updating || n._orig === undefined || n.y === n._orig.y)
436
+ return;
437
+ let newY = n.y;
438
+ while (newY > n._orig.y) {
439
+ --newY;
440
+ const collide = this.collide(n, { x: n.x, y: newY, w: n.w, h: n.h });
441
+ if (!collide) {
442
+ n._dirty = true;
443
+ n.y = newY;
444
+ }
445
+ }
446
+ });
447
+ }
448
+ else {
449
+ // top gravity pack
450
+ this.nodes.forEach((n, i) => {
451
+ if (n.locked)
452
+ return;
453
+ while (n.y > 0) {
454
+ const newY = i === 0 ? 0 : n.y - 1;
455
+ const canBeMoved = i === 0 || !this.collide(n, { x: n.x, y: newY, w: n.w, h: n.h });
456
+ if (!canBeMoved)
457
+ break;
458
+ // Note: must be dirty (from last position) for GridStack::OnChange CB to update positions
459
+ // and move items back. The user 'change' CB should detect changes from the original
460
+ // starting position instead.
461
+ n._dirty = (n.y !== newY);
462
+ n.y = newY;
463
+ }
464
+ });
465
+ }
466
+ return this;
467
+ }
468
+ /**
469
+ * Prepare and validate a node's coordinates and values for the current grid.
470
+ * This ensures the node has valid position, size, and properties before being added to the grid.
471
+ *
472
+ * @param node the node to prepare and validate
473
+ * @param resizing if true, resize the node down if it's out of bounds; if false, move it to fit
474
+ * @returns the prepared node with valid coordinates
475
+ *
476
+ * @example
477
+ * const node = { w: 3, h: 2, content: 'Hello' };
478
+ * const prepared = engine.prepareNode(node);
479
+ * console.log('Node prepared at:', prepared.x, prepared.y);
480
+ */
481
+ prepareNode(node, resizing) {
482
+ node._id = node._id ?? GridStackEngine._idSeq++;
483
+ // make sure USER supplied id are unique in our list, else assign a new one as it will create issues during load/update/etc...
484
+ const id = node.id;
485
+ if (id) {
486
+ let count = 1; // append nice _n rather than some random number
487
+ while (this.nodes.find(n => n.id === node.id && n !== node)) {
488
+ node.id = id + '_' + (count++);
489
+ }
490
+ }
491
+ // if we're missing position, have the grid position us automatically (before we set them to 0,0)
492
+ if (node.x === undefined || node.y === undefined || node.x === null || node.y === null) {
493
+ node.autoPosition = true;
494
+ }
495
+ // assign defaults for missing required fields
496
+ const defaults = { x: 0, y: 0, w: 1, h: 1 };
497
+ Utils.defaults(node, defaults);
498
+ if (!node.autoPosition) {
499
+ delete node.autoPosition;
500
+ }
501
+ if (!node.noResize) {
502
+ delete node.noResize;
503
+ }
504
+ if (!node.noMove) {
505
+ delete node.noMove;
506
+ }
507
+ Utils.sanitizeMinMax(node);
508
+ // check for NaN (in case messed up strings were passed. can't do parseInt() || defaults.x above as 0 is valid #)
509
+ if (typeof node.x == 'string') {
510
+ node.x = Number(node.x);
511
+ }
512
+ if (typeof node.y == 'string') {
513
+ node.y = Number(node.y);
514
+ }
515
+ if (typeof node.w == 'string') {
516
+ node.w = Number(node.w);
517
+ }
518
+ if (typeof node.h == 'string') {
519
+ node.h = Number(node.h);
520
+ }
521
+ if (isNaN(node.x)) {
522
+ node.x = defaults.x;
523
+ node.autoPosition = true;
524
+ }
525
+ if (isNaN(node.y)) {
526
+ node.y = defaults.y;
527
+ node.autoPosition = true;
528
+ }
529
+ if (isNaN(node.w)) {
530
+ node.w = defaults.w;
531
+ }
532
+ if (isNaN(node.h)) {
533
+ node.h = defaults.h;
534
+ }
535
+ this.nodeBoundFix(node, resizing);
536
+ return node;
537
+ }
538
+ /**
539
+ * Part 2 of preparing a node to fit inside the grid - validates and fixes coordinates and dimensions.
540
+ * This ensures the node fits within grid boundaries and respects min/max constraints.
541
+ *
542
+ * @param node the node to validate and fix
543
+ * @param resizing if true, resize the node to fit; if false, move the node to fit
544
+ * @returns the engine instance for chaining
545
+ *
546
+ * @example
547
+ * // Fix a node that might be out of bounds
548
+ * engine.nodeBoundFix(node, true); // Resize to fit
549
+ * engine.nodeBoundFix(node, false); // Move to fit
550
+ */
551
+ nodeBoundFix(node, resizing) {
552
+ const before = node._orig || Utils.copyPos({}, node);
553
+ if (node.maxW) {
554
+ node.w = Math.min(node.w || 1, node.maxW);
555
+ }
556
+ if (node.maxH) {
557
+ node.h = Math.min(node.h || 1, node.maxH);
558
+ }
559
+ if (node.minW) {
560
+ node.w = Math.max(node.w || 1, node.minW);
561
+ }
562
+ if (node.minH) {
563
+ node.h = Math.max(node.h || 1, node.minH);
564
+ }
565
+ // if user loaded a larger than allowed widget for current # of columns,
566
+ // remember it's position & width so we can restore back (1 -> 12 column) #1655 #1985
567
+ // IFF we're not in the middle of column resizing!
568
+ const saveOrig = (node.x || 0) + (node.w || 1) > this.column;
569
+ if (saveOrig && this.column < this.defaultColumn && !this._inColumnResize && !this.skipCacheUpdate && node._id != null && this.findCacheLayout(node, this.defaultColumn) === -1) {
570
+ const copy = { ...node }; // need _id + positions
571
+ if (copy.autoPosition || copy.x === undefined) {
572
+ delete copy.x;
573
+ delete copy.y;
574
+ }
575
+ else
576
+ copy.x = Math.min(this.defaultColumn - 1, copy.x);
577
+ copy.w = Math.min(this.defaultColumn, copy.w || 1);
578
+ this.cacheOneLayout(copy, this.defaultColumn);
579
+ }
580
+ if (node.w > this.column) {
581
+ node.w = this.column;
582
+ }
583
+ else if (node.w < 1) {
584
+ node.w = 1;
585
+ }
586
+ if (this.maxRow && node.h > this.maxRow) {
587
+ node.h = this.maxRow;
588
+ }
589
+ else if (node.h < 1) {
590
+ node.h = 1;
591
+ }
592
+ if (node.x < 0) {
593
+ node.x = 0;
594
+ }
595
+ if (node.y < 0) {
596
+ node.y = 0;
597
+ }
598
+ if (node.x + node.w > this.column) {
599
+ if (resizing) {
600
+ node.w = this.column - node.x;
601
+ }
602
+ else {
603
+ node.x = this.column - node.w;
604
+ }
605
+ }
606
+ if (this.maxRow && node.y + node.h > this.maxRow) {
607
+ if (resizing) {
608
+ node.h = this.maxRow - node.y;
609
+ }
610
+ else {
611
+ node.y = this.maxRow - node.h;
612
+ }
613
+ }
614
+ if (!Utils.samePos(node, before)) {
615
+ node._dirty = true;
616
+ }
617
+ return this;
618
+ }
619
+ /**
620
+ * Returns a list of nodes that have been modified from their original values.
621
+ * This is used to track which nodes need DOM updates.
622
+ *
623
+ * @param verify if true, performs additional verification by comparing current vs original positions
624
+ * @returns array of nodes that have been modified
625
+ *
626
+ * @example
627
+ * const changed = engine.getDirtyNodes();
628
+ * console.log('Modified nodes:', changed.length);
629
+ *
630
+ * // Get verified dirty nodes
631
+ * const verified = engine.getDirtyNodes(true);
632
+ */
633
+ getDirtyNodes(verify) {
634
+ // compare original x,y,w,h instead as _dirty can be a temporary state
635
+ if (verify) {
636
+ return this.nodes.filter(n => n._dirty && !Utils.samePos(n, n._orig));
637
+ }
638
+ return this.nodes.filter(n => n._dirty);
639
+ }
640
+ /** @internal call this to call onChange callback with dirty nodes so DOM can be updated */
641
+ _notify(removedNodes) {
642
+ if (this.batchMode || !this.onChange)
643
+ return this;
644
+ const dirtyNodes = (removedNodes || []).concat(this.getDirtyNodes());
645
+ this.onChange(dirtyNodes);
646
+ return this;
647
+ }
648
+ /**
649
+ * Clean all dirty and last tried information from nodes.
650
+ * This resets the dirty state tracking for all nodes.
651
+ *
652
+ * @returns the engine instance for chaining
653
+ *
654
+ * @internal
655
+ */
656
+ cleanNodes() {
657
+ if (this.batchMode)
658
+ return this;
659
+ this.nodes.forEach(n => {
660
+ delete n._dirty;
661
+ delete n._lastTried;
662
+ });
663
+ return this;
664
+ }
665
+ /**
666
+ * Save the initial position/size of all nodes to track real dirty state.
667
+ * This creates a snapshot of current positions that can be restored later.
668
+ *
669
+ * Note: Should be called right after change events and before move/resize operations.
670
+ *
671
+ * @returns the engine instance for chaining
672
+ *
673
+ * @internal
674
+ */
675
+ saveInitial() {
676
+ this.nodes.forEach(n => {
677
+ n._orig = Utils.copyPos({}, n);
678
+ delete n._dirty;
679
+ });
680
+ this._hasLocked = this.nodes.some(n => n.locked);
681
+ return this;
682
+ }
683
+ /**
684
+ * Restore all nodes back to their initial values.
685
+ * This is typically called when canceling an operation (e.g., Esc key during drag).
686
+ *
687
+ * @returns the engine instance for chaining
688
+ *
689
+ * @internal
690
+ */
691
+ restoreInitial() {
692
+ this.nodes.forEach(n => {
693
+ if (!n._orig || Utils.samePos(n, n._orig))
694
+ return;
695
+ Utils.copyPos(n, n._orig);
696
+ n._dirty = true;
697
+ });
698
+ this._notify();
699
+ return this;
700
+ }
701
+ /**
702
+ * Find the first available empty spot for the given node dimensions.
703
+ * Updates the node's x,y attributes with the found position.
704
+ *
705
+ * @param node the node to find a position for (w,h must be set)
706
+ * @param nodeList optional list of nodes to check against (defaults to engine nodes)
707
+ * @param column optional column count (defaults to engine column count)
708
+ * @param after optional node to start search after (maintains order)
709
+ * @returns true if an empty position was found and node was updated
710
+ *
711
+ * @example
712
+ * const node = { w: 2, h: 1 };
713
+ * if (engine.findEmptyPosition(node)) {
714
+ * console.log('Found position at:', node.x, node.y);
715
+ * }
716
+ */
717
+ findEmptyPosition(node, nodeList = this.nodes, column = this.column, after) {
718
+ const start = after ? after.y * column + (after.x + after.w) : 0;
719
+ let found = false;
720
+ for (let i = start; !found; ++i) {
721
+ const x = i % column;
722
+ const y = Math.floor(i / column);
723
+ if (x + node.w > column) {
724
+ continue;
725
+ }
726
+ const box = { x, y, w: node.w, h: node.h };
727
+ if (!nodeList.find(n => Utils.isIntercepted(box, n))) {
728
+ if (node.x !== x || node.y !== y)
729
+ node._dirty = true;
730
+ node.x = x;
731
+ node.y = y;
732
+ delete node.autoPosition;
733
+ found = true;
734
+ }
735
+ }
736
+ return found;
737
+ }
738
+ /**
739
+ * Add the given node to the grid, handling collision detection and re-packing.
740
+ * This is the main method for adding new widgets to the engine.
741
+ *
742
+ * @param node the node to add to the grid
743
+ * @param triggerAddEvent if true, adds node to addedNodes list for event triggering
744
+ * @param after optional node to place this node after (for ordering)
745
+ * @returns the added node (or existing node if duplicate)
746
+ *
747
+ * @example
748
+ * const node = { x: 0, y: 0, w: 2, h: 1, content: 'Hello' };
749
+ * const added = engine.addNode(node, true);
750
+ */
751
+ addNode(node, triggerAddEvent = false, after) {
752
+ const dup = this.nodes.find(n => n._id === node._id);
753
+ if (dup)
754
+ return dup; // prevent inserting twice! return it instead.
755
+ // skip prepareNode if we're in middle of column resize (not new) but do check for bounds!
756
+ this._inColumnResize ? this.nodeBoundFix(node) : this.prepareNode(node);
757
+ delete node._temporaryRemoved;
758
+ delete node._removeDOM;
759
+ let skipCollision;
760
+ if (node.autoPosition && this.findEmptyPosition(node, this.nodes, this.column, after)) {
761
+ delete node.autoPosition; // found our slot
762
+ skipCollision = true;
763
+ }
764
+ this.nodes.push(node);
765
+ if (triggerAddEvent) {
766
+ this.addedNodes.push(node);
767
+ }
768
+ if (!skipCollision)
769
+ this._fixCollisions(node);
770
+ if (!this.batchMode) {
771
+ this._packNodes()._notify();
772
+ }
773
+ return node;
774
+ }
775
+ /**
776
+ * Remove the given node from the grid.
777
+ *
778
+ * @param node the node to remove
779
+ * @param removeDOM if true (default), marks node for DOM removal
780
+ * @param triggerEvent if true, adds node to removedNodes list for event triggering
781
+ * @returns the engine instance for chaining
782
+ *
783
+ * @example
784
+ * engine.removeNode(node, true, true);
785
+ */
786
+ removeNode(node, removeDOM = true, triggerEvent = false) {
787
+ if (!this.nodes.find(n => n._id === node._id)) {
788
+ // TEST console.log(`Error: GridStackEngine.removeNode() node._id=${node._id} not found!`)
789
+ return this;
790
+ }
791
+ if (triggerEvent) { // we wait until final drop to manually track removed items (rather than during drag)
792
+ this.removedNodes.push(node);
793
+ }
794
+ if (removeDOM)
795
+ node._removeDOM = true; // let CB remove actual HTML (used to set _id to null, but then we loose layout info)
796
+ // don't use 'faster' .splice(findIndex(),1) in case node isn't in our list, or in multiple times.
797
+ this.nodes = this.nodes.filter(n => n._id !== node._id);
798
+ if (!node._isAboutToRemove)
799
+ this._packNodes(); // if dragged out, no need to relayout as already done...
800
+ this._notify([node]);
801
+ return this;
802
+ }
803
+ /**
804
+ * Remove all nodes from the grid.
805
+ *
806
+ * @param removeDOM if true (default), marks all nodes for DOM removal
807
+ * @param triggerEvent if true (default), triggers removal events
808
+ * @returns the engine instance for chaining
809
+ *
810
+ * @example
811
+ * engine.removeAll(); // Remove all nodes
812
+ */
813
+ removeAll(removeDOM = true, triggerEvent = true) {
814
+ delete this._layouts;
815
+ if (!this.nodes.length)
816
+ return this;
817
+ removeDOM && this.nodes.forEach(n => n._removeDOM = true); // let CB remove actual HTML (used to set _id to null, but then we loose layout info)
818
+ const removedNodes = this.nodes;
819
+ this.removedNodes = triggerEvent ? removedNodes : [];
820
+ this.nodes = [];
821
+ return this._notify(removedNodes);
822
+ }
823
+ /**
824
+ * Check if a node can be moved to a new position, considering layout constraints.
825
+ * This is a safer version of moveNode() that validates the move first.
826
+ *
827
+ * For complex cases (like maxRow constraints), it simulates the move in a clone first,
828
+ * then applies the changes only if they meet all specifications.
829
+ *
830
+ * @param node the node to move
831
+ * @param o move options including target position
832
+ * @returns true if the node was successfully moved
833
+ *
834
+ * @example
835
+ * const canMove = engine.moveNodeCheck(node, { x: 2, y: 1 });
836
+ * if (canMove) {
837
+ * console.log('Node moved successfully');
838
+ * }
839
+ */
840
+ moveNodeCheck(node, o) {
841
+ // if (node.locked) return false;
842
+ if (!this.changedPosConstrain(node, o))
843
+ return false;
844
+ o.pack = true;
845
+ // simpler case: move item directly...
846
+ if (!this.maxRow) {
847
+ return this.moveNode(node, o);
848
+ }
849
+ // complex case: create a clone with NO maxRow (will check for out of bounds at the end)
850
+ let clonedNode;
851
+ const clone = new GridStackEngine({
852
+ column: this.column,
853
+ float: this.float,
854
+ nodes: this.nodes.map(n => {
855
+ if (n._id === node._id) {
856
+ clonedNode = { ...n };
857
+ return clonedNode;
858
+ }
859
+ return { ...n };
860
+ })
861
+ });
862
+ if (!clonedNode)
863
+ return false;
864
+ // check if we're covering 50% collision and could move, while still being under maxRow or at least not making it worse
865
+ // (case where widget was somehow added past our max #2449)
866
+ const canMove = clone.moveNode(clonedNode, o) && clone.getRow() <= Math.max(this.getRow(), this.maxRow);
867
+ // else check if we can force a swap (float=true, or different shapes) on non-resize
868
+ if (!canMove && !o.resizing && o.collide) {
869
+ const collide = o.collide.el.gridstackNode; // find the source node the clone collided with at 50%
870
+ if (this.swap(node, collide)) { // swaps and mark dirty
871
+ this._notify();
872
+ return true;
873
+ }
874
+ }
875
+ if (!canMove)
876
+ return false;
877
+ // if clone was able to move, copy those mods over to us now instead of caller trying to do this all over!
878
+ // Note: we can't use the list directly as elements and other parts point to actual node, so copy content
879
+ clone.nodes.filter(n => n._dirty).forEach(c => {
880
+ const n = this.nodes.find(a => a._id === c._id);
881
+ if (!n)
882
+ return;
883
+ Utils.copyPos(n, c);
884
+ n._dirty = true;
885
+ });
886
+ this._notify();
887
+ return true;
888
+ }
889
+ /** return true if can fit in grid height constrain only (always true if no maxRow) */
890
+ willItFit(node) {
891
+ delete node._willFitPos;
892
+ if (!this.maxRow)
893
+ return true;
894
+ // create a clone with NO maxRow and check if still within size
895
+ const clone = new GridStackEngine({
896
+ column: this.column,
897
+ float: this.float,
898
+ nodes: this.nodes.map(n => { return { ...n }; })
899
+ });
900
+ const n = { ...node }; // clone node so we don't mod any settings on it but have full autoPosition and min/max as well! #1687
901
+ this.cleanupNode(n);
902
+ delete n.el;
903
+ delete n._id;
904
+ delete n.content;
905
+ delete n.grid;
906
+ clone.addNode(n);
907
+ if (clone.getRow() <= this.maxRow) {
908
+ node._willFitPos = Utils.copyPos({}, n);
909
+ return true;
910
+ }
911
+ return false;
912
+ }
913
+ /** true if x,y or w,h are different after clamping to min/max */
914
+ changedPosConstrain(node, p) {
915
+ // first make sure w,h are set for caller
916
+ p.w = p.w || node.w;
917
+ p.h = p.h || node.h;
918
+ if (node.x !== p.x || node.y !== p.y)
919
+ return true;
920
+ // check constrained w,h
921
+ if (node.maxW) {
922
+ p.w = Math.min(p.w, node.maxW);
923
+ }
924
+ if (node.maxH) {
925
+ p.h = Math.min(p.h, node.maxH);
926
+ }
927
+ if (node.minW) {
928
+ p.w = Math.max(p.w, node.minW);
929
+ }
930
+ if (node.minH) {
931
+ p.h = Math.max(p.h, node.minH);
932
+ }
933
+ return (node.w !== p.w || node.h !== p.h);
934
+ }
935
+ /** return true if the passed in node was actually moved (checks for no-op and locked) */
936
+ moveNode(node, o) {
937
+ if (!node || /*node.locked ||*/ !o)
938
+ return false;
939
+ let wasUndefinedPack;
940
+ if (o.pack === undefined && !this.batchMode) {
941
+ wasUndefinedPack = o.pack = true;
942
+ }
943
+ // constrain the passed in values and check if we're still changing our node
944
+ if (typeof o.x !== 'number') {
945
+ o.x = node.x;
946
+ }
947
+ if (typeof o.y !== 'number') {
948
+ o.y = node.y;
949
+ }
950
+ if (typeof o.w !== 'number') {
951
+ o.w = node.w;
952
+ }
953
+ if (typeof o.h !== 'number') {
954
+ o.h = node.h;
955
+ }
956
+ const resizing = (node.w !== o.w || node.h !== o.h);
957
+ const nn = Utils.copyPos({}, node, true); // get min/max out first, then opt positions next
958
+ Utils.copyPos(nn, o);
959
+ this.nodeBoundFix(nn, resizing);
960
+ Utils.copyPos(o, nn);
961
+ if (!o.forceCollide && Utils.samePos(node, o))
962
+ return false;
963
+ const prevPos = Utils.copyPos({}, node);
964
+ // check if we will need to fix collision at our new location
965
+ const collides = this.collideAll(node, nn, o.skip);
966
+ let needToMove = true;
967
+ if (collides.length) {
968
+ const activeDrag = node._moving && !o.nested;
969
+ // check to make sure we actually collided over 50% surface area while dragging
970
+ let collide = activeDrag ? this.directionCollideCoverage(node, o, collides) : collides[0];
971
+ // if we're enabling creation of sub-grids on the fly, see if we're covering 80% of either one, if we didn't already do that
972
+ if (activeDrag && collide && node.grid?.opts?.subGridDynamic && !node.grid._isTemp) {
973
+ const over = Utils.areaIntercept(o.rect, collide._rect);
974
+ const a1 = Utils.area(o.rect);
975
+ const a2 = Utils.area(collide._rect);
976
+ const perc = over / (a1 < a2 ? a1 : a2);
977
+ if (perc > .8) {
978
+ collide.grid.makeSubGrid(collide.el, undefined, node);
979
+ collide = undefined;
980
+ }
981
+ }
982
+ if (collide) {
983
+ needToMove = !this._fixCollisions(node, nn, collide, o); // check if already moved...
984
+ }
985
+ else {
986
+ needToMove = false; // we didn't cover >50% for a move, skip...
987
+ if (wasUndefinedPack)
988
+ delete o.pack;
989
+ }
990
+ }
991
+ // now move (to the original ask vs the collision version which might differ) and repack things
992
+ if (needToMove && !Utils.samePos(node, nn)) {
993
+ node._dirty = true;
994
+ Utils.copyPos(node, nn);
995
+ }
996
+ if (o.pack) {
997
+ this._packNodes()
998
+ ._notify();
999
+ }
1000
+ return !Utils.samePos(node, prevPos); // pack might have moved things back
1001
+ }
1002
+ getRow() {
1003
+ return this.nodes.reduce((row, n) => Math.max(row, n.y + n.h), 0);
1004
+ }
1005
+ beginUpdate(node) {
1006
+ if (!node._updating) {
1007
+ node._updating = true;
1008
+ delete node._skipDown;
1009
+ if (!this.batchMode)
1010
+ this.saveInitial();
1011
+ }
1012
+ return this;
1013
+ }
1014
+ endUpdate() {
1015
+ const n = this.nodes.find(n => n._updating);
1016
+ if (n) {
1017
+ delete n._updating;
1018
+ delete n._skipDown;
1019
+ }
1020
+ return this;
1021
+ }
1022
+ /** saves a copy of the largest column layout (eg 12 even when rendering 1 column) so we don't loose orig layout, unless explicity column
1023
+ * count to use is given. returning a list of widgets for serialization
1024
+ * @param saveElement if true (default), the element will be saved to GridStackWidget.el field, else it will be removed.
1025
+ * @param saveCB callback for each node -> widget, so application can insert additional data to be saved into the widget data structure.
1026
+ * @param column if provided, the grid will be saved for the given column count (IFF we have matching internal saved layout, or current layout).
1027
+ * Note: nested grids will ALWAYS save the container w to match overall layouts (parent + child) to be consistent.
1028
+ */
1029
+ save(saveElement = true, saveCB, column) {
1030
+ // use the highest layout for any saved info so we can have full detail on reload #1849
1031
+ // unless we're given a column to match (always set for nested grids)
1032
+ const len = this._layouts?.length || 0;
1033
+ let layout;
1034
+ if (len) {
1035
+ if (column) {
1036
+ if (column !== this.column)
1037
+ layout = this._layouts[column];
1038
+ }
1039
+ else if (this.column !== len - 1) {
1040
+ layout = this._layouts[len - 1];
1041
+ }
1042
+ }
1043
+ const list = [];
1044
+ this.sortNodes();
1045
+ this.nodes.forEach(n => {
1046
+ const wl = layout?.find(l => l._id === n._id);
1047
+ // use layout info fields instead if set
1048
+ const w = { ...n, ...(wl || {}) };
1049
+ Utils.removeInternalForSave(w, !saveElement);
1050
+ if (saveCB)
1051
+ saveCB(n, w);
1052
+ list.push(w);
1053
+ });
1054
+ return list;
1055
+ }
1056
+ /** @internal called whenever a node is added or moved - updates the cached layouts */
1057
+ layoutsNodesChange(nodes) {
1058
+ if (!this._layouts || this._inColumnResize)
1059
+ return this;
1060
+ // remove smaller layouts - we will re-generate those on the fly... larger ones need to update
1061
+ this._layouts.forEach((layout, column) => {
1062
+ if (!layout || column === this.column)
1063
+ return this;
1064
+ if (column < this.column) {
1065
+ this._layouts[column] = undefined;
1066
+ }
1067
+ else {
1068
+ // we save the original x,y,w (h isn't cached) to see what actually changed to propagate better.
1069
+ // NOTE: we don't need to check against out of bound scaling/moving as that will be done when using those cache values. #1785
1070
+ const ratio = column / this.column;
1071
+ nodes.forEach(node => {
1072
+ if (!node._orig)
1073
+ return; // didn't change (newly added ?)
1074
+ const n = layout.find(l => l._id === node._id);
1075
+ if (!n)
1076
+ return; // no cache for new nodes. Will use those values.
1077
+ // Y changed, push down same amount
1078
+ // TODO: detect doing item 'swaps' will help instead of move (especially in 1 column mode)
1079
+ if (n.y >= 0 && node.y !== node._orig.y) {
1080
+ n.y += (node.y - node._orig.y);
1081
+ }
1082
+ // X changed, scale from new position
1083
+ if (node.x !== node._orig.x) {
1084
+ n.x = Math.round(node.x * ratio);
1085
+ }
1086
+ // width changed, scale from new width
1087
+ if (node.w !== node._orig.w) {
1088
+ n.w = Math.round(node.w * ratio);
1089
+ }
1090
+ // ...height always carries over from cache
1091
+ });
1092
+ }
1093
+ });
1094
+ return this;
1095
+ }
1096
+ /**
1097
+ * @internal Called to scale the widget width & position up/down based on the column change.
1098
+ * Note we store previous layouts (especially original ones) to make it possible to go
1099
+ * from say 12 -> 1 -> 12 and get back to where we were.
1100
+ *
1101
+ * @param prevColumn previous number of columns
1102
+ * @param column new column number
1103
+ * @param layout specify the type of re-layout that will happen (position, size, etc...).
1104
+ * Note: items will never be outside of the current column boundaries. default (moveScale). Ignored for 1 column
1105
+ */
1106
+ columnChanged(prevColumn, column, layout = 'moveScale') {
1107
+ if (!this.nodes.length || !column || prevColumn === column)
1108
+ return this;
1109
+ // simpler shortcuts layouts
1110
+ const doCompact = layout === 'compact' || layout === 'list';
1111
+ if (doCompact) {
1112
+ this.sortNodes(1); // sort with original layout once and only once (new column will affect order otherwise)
1113
+ }
1114
+ // cache the current layout in case they want to go back (like 12 -> 1 -> 12) as it requires original data IFF we're sizing down (see below)
1115
+ if (column < prevColumn)
1116
+ this.cacheLayout(this.nodes, prevColumn);
1117
+ this.batchUpdate(); // do this EARLY as it will call saveInitial() so we can detect where we started for _dirty and collision
1118
+ let newNodes = [];
1119
+ let nodes = doCompact ? this.nodes : Utils.sort(this.nodes, -1); // current column reverse sorting so we can insert last to front (limit collision)
1120
+ // see if we have cached previous layout IFF we are going up in size (restore) otherwise always
1121
+ // generate next size down from where we are (looks more natural as you gradually size down).
1122
+ if (column > prevColumn && this._layouts) {
1123
+ const cacheNodes = this._layouts[column] || [];
1124
+ // ...if not, start with the largest layout (if not already there) as down-scaling is more accurate
1125
+ // by pretending we came from that larger column by assigning those values as starting point
1126
+ const lastIndex = this._layouts.length - 1;
1127
+ if (!cacheNodes.length && prevColumn !== lastIndex && this._layouts[lastIndex]?.length) {
1128
+ prevColumn = lastIndex;
1129
+ this._layouts[lastIndex].forEach(cacheNode => {
1130
+ const n = nodes.find(n => n._id === cacheNode._id);
1131
+ if (n) {
1132
+ // still current, use cache info positions
1133
+ if (!doCompact && !cacheNode.autoPosition) {
1134
+ n.x = cacheNode.x ?? n.x;
1135
+ n.y = cacheNode.y ?? n.y;
1136
+ }
1137
+ n.w = cacheNode.w ?? n.w;
1138
+ if (cacheNode.x == undefined || cacheNode.y === undefined)
1139
+ n.autoPosition = true;
1140
+ }
1141
+ });
1142
+ }
1143
+ // if we found cache re-use those nodes that are still current
1144
+ cacheNodes.forEach(cacheNode => {
1145
+ const j = nodes.findIndex(n => n._id === cacheNode._id);
1146
+ if (j !== -1) {
1147
+ const n = nodes[j];
1148
+ // still current, use cache info positions
1149
+ if (doCompact) {
1150
+ n.w = cacheNode.w; // only w is used, and don't trim the list
1151
+ return;
1152
+ }
1153
+ if (cacheNode.autoPosition || isNaN(cacheNode.x) || isNaN(cacheNode.y)) {
1154
+ this.findEmptyPosition(cacheNode, newNodes);
1155
+ }
1156
+ if (!cacheNode.autoPosition) {
1157
+ n.x = cacheNode.x ?? n.x;
1158
+ n.y = cacheNode.y ?? n.y;
1159
+ n.w = cacheNode.w ?? n.w;
1160
+ newNodes.push(n);
1161
+ }
1162
+ nodes.splice(j, 1);
1163
+ }
1164
+ });
1165
+ }
1166
+ // much simpler layout that just compacts
1167
+ if (doCompact) {
1168
+ this.compact(layout, false);
1169
+ }
1170
+ else {
1171
+ // ...and add any extra non-cached ones
1172
+ if (nodes.length) {
1173
+ if (typeof layout === 'function') {
1174
+ layout(column, prevColumn, newNodes, nodes);
1175
+ }
1176
+ else {
1177
+ const ratio = (doCompact || layout === 'none') ? 1 : column / prevColumn;
1178
+ const move = (layout === 'move' || layout === 'moveScale');
1179
+ const scale = (layout === 'scale' || layout === 'moveScale');
1180
+ nodes.forEach(node => {
1181
+ // NOTE: x + w could be outside of the grid, but addNode() below will handle that
1182
+ node.x = (column === 1 ? 0 : (move ? Math.round(node.x * ratio) : Math.min(node.x, column - 1)));
1183
+ node.w = ((column === 1 || prevColumn === 1) ? 1 : scale ? (Math.round(node.w * ratio) || 1) : (Math.min(node.w, column)));
1184
+ newNodes.push(node);
1185
+ });
1186
+ nodes = [];
1187
+ }
1188
+ }
1189
+ // finally re-layout them in reverse order (to get correct placement)
1190
+ newNodes = Utils.sort(newNodes, -1);
1191
+ this._inColumnResize = true; // prevent cache update
1192
+ this.nodes = []; // pretend we have no nodes to start with (add() will use same structures) to simplify layout
1193
+ newNodes.forEach(node => {
1194
+ this.addNode(node, false); // 'false' for add event trigger
1195
+ delete node._orig; // make sure the commit doesn't try to restore things back to original
1196
+ });
1197
+ }
1198
+ this.nodes.forEach(n => delete n._orig); // clear _orig before batch=false so it doesn't handle float=true restore
1199
+ this.batchUpdate(false, !doCompact);
1200
+ delete this._inColumnResize;
1201
+ return this;
1202
+ }
1203
+ /**
1204
+ * call to cache the given layout internally to the given location so we can restore back when column changes size
1205
+ * @param nodes list of nodes
1206
+ * @param column corresponding column index to save it under
1207
+ * @param clear if true, will force other caches to be removed (default false)
1208
+ */
1209
+ cacheLayout(nodes, column, clear = false) {
1210
+ const copy = [];
1211
+ nodes.forEach((n, i) => {
1212
+ // make sure we have an id in case this is new layout, else re-use id already set
1213
+ if (n._id === undefined) {
1214
+ const existing = n.id ? this.nodes.find(n2 => n2.id === n.id) : undefined; // find existing node using users id
1215
+ n._id = existing?._id ?? GridStackEngine._idSeq++;
1216
+ }
1217
+ copy[i] = { x: n.x, y: n.y, w: n.w, _id: n._id }; // only thing we change is x,y,w and id to find it back
1218
+ });
1219
+ this._layouts = clear ? [] : this._layouts || []; // use array to find larger quick
1220
+ this._layouts[column] = copy;
1221
+ return this;
1222
+ }
1223
+ /**
1224
+ * call to cache the given node layout internally to the given location so we can restore back when column changes size
1225
+ * @param node single node to cache
1226
+ * @param column corresponding column index to save it under
1227
+ */
1228
+ cacheOneLayout(n, column) {
1229
+ n._id = n._id ?? GridStackEngine._idSeq++;
1230
+ const l = { x: n.x, y: n.y, w: n.w, _id: n._id };
1231
+ if (n.autoPosition || n.x === undefined) {
1232
+ delete l.x;
1233
+ delete l.y;
1234
+ if (n.autoPosition)
1235
+ l.autoPosition = true;
1236
+ }
1237
+ this._layouts = this._layouts || [];
1238
+ this._layouts[column] = this._layouts[column] || [];
1239
+ const index = this.findCacheLayout(n, column);
1240
+ if (index === -1)
1241
+ this._layouts[column].push(l);
1242
+ else
1243
+ this._layouts[column][index] = l;
1244
+ return this;
1245
+ }
1246
+ findCacheLayout(n, column) {
1247
+ return this._layouts?.[column]?.findIndex(l => l._id === n._id) ?? -1;
1248
+ }
1249
+ removeNodeFromLayoutCache(n) {
1250
+ if (!this._layouts) {
1251
+ return;
1252
+ }
1253
+ for (let i = 0; i < this._layouts.length; i++) {
1254
+ const index = this.findCacheLayout(n, i);
1255
+ if (index !== -1) {
1256
+ this._layouts[i].splice(index, 1);
1257
+ }
1258
+ }
1259
+ }
1260
+ /** called to remove all internal values but the _id */
1261
+ cleanupNode(node) {
1262
+ for (const prop in node) {
1263
+ if (prop[0] === '_' && prop !== '_id')
1264
+ delete node[prop];
1265
+ }
1266
+ return this;
1267
+ }
1268
+ }
1269
+ /** @internal unique global internal _id counter */
1270
+ GridStackEngine._idSeq = 0;
1271
+ export { GridStackEngine };
1272
+ //# sourceMappingURL=gridstack-engine.js.map