Skip to content

Commit af32ccd

Browse files
authored
Merge pull request #3419 from adumesny/master
item not moved (to alt position) when colliding fails with an object
2 parents a0bb88a + 1db9092 commit af32ccd

3 files changed

Lines changed: 85 additions & 1 deletion

File tree

‎doc/CHANGES.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ Change log
150150

151151
## 14.0.1 (TBD)
152152
* fix: [#2666](https://github.com/gridstack/gridstack.js/issues/2666) iOS auto-scroll uses visualViewport, and un-bind touch handlers
153+
* fix: [#3418](https://github.com/gridstack/gridstack.js/pull/3418) preserve saved layout after cancelling a cross-grid drag- #3418 - thank you [sameerdeolalikar](https://github.com/sameerdeolalikar)
154+
* fix: [#2819](https://github.com/gridstack/gridstack.js/issues/2819) item not moved (to alt position) when colliding fails with an object
153155

154156
## 14.0.0 (2026-09-20)
155157
* feat: [#754](https://github.com/gridstack/gridstack.js/issues/754), [#2866](https://github.com/gridstack/gridstack.js/issues/2866) new `mode?: 'top' | 'float' | 'list' | 'compact'` - items are continuously re-flowed in sequential (row-major) order, like a re-orderable list: dragging, resizing, adding or removing an item re-flows everyone else instead of pushing them down, and dropping an item on another takes its place. See new

‎spec/regression-spec.ts‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,4 +715,69 @@ describe('regression >', () => {
715715
expect(mouseDown(action)).toBe(false); // must NOT start a drag
716716
});
717717
});
718+
719+
describe('2819 diagonal drag stuck on a tall neighbour it barely grazes >', () => {
720+
const CH = 50, CW = 50;
721+
beforeEach(() => {
722+
document.body.insertAdjacentHTML('afterbegin', gridstackEmptyHTML);
723+
});
724+
afterEach(() => {
725+
document.body.removeChild(document.getElementById('gs-cont'));
726+
});
727+
728+
// reporter's video: dragging mostly sideways, with a small amount of vertical mouse creep,
729+
// froze completely against a tall neighbour instead of continuing to slide horizontally -
730+
// even though the row the item started on was completely free the whole way across.
731+
it('still slides sideways when the vertical component fails 50% coverage', () => {
732+
grid = GridStack.init({column: 12, cellHeight: CH, mode: 'float', children: [
733+
{id: 'move', x: 5, y: 0, w: 2, h: 1},
734+
{id: 'big', x: 0, y: 1, w: 2, h: 6},
735+
]});
736+
const move = grid.engine.nodes.find(n => n.id === 'move')!;
737+
const big = grid.engine.nodes.find(n => n.id === 'big')!;
738+
739+
grid.engine.cleanNodes().beginUpdate(move);
740+
move._moving = true;
741+
grid.engine.cacheRects(CW, CH, 0, 0, 0, 0);
742+
743+
// dx=-4 (mostly horizontal), dy=1 (a sliver dipping into 'big' row) - only 1 of 6 rows of
744+
// overlap (~17%), nowhere near the 50% needed to push 'big' out of the way
745+
grid.engine.moveNodeCheck(move, {x: 1, y: 1, w: 2, h: 1, cellWidth: CW, cellHeight: CH,
746+
rect: {x: 1 * CW, y: 1 * CH, w: 2 * CW, h: 1 * CH}});
747+
748+
expect(big.y).toBe(1); // untouched, correctly - coverage really is too small to push it
749+
expect(move.y).toBe(0); // stayed on its original free row...
750+
expect(move.x).toBe(1); // ...but still allowed to slide over sideways, not stuck at x=5
751+
grid.engine.endUpdate();
752+
});
753+
754+
it('does not slide sideways into something actually blocking that row', () => {
755+
grid = GridStack.init({column: 12, cellHeight: CH, mode: 'float', children: [
756+
{id: 'move', x: 5, y: 0, w: 2, h: 1},
757+
{id: 'big', x: 0, y: 1, w: 2, h: 6},
758+
{id: 'blocker', x: 1, y: 0, w: 2, h: 1},
759+
]});
760+
const move = grid.engine.nodes.find(n => n.id === 'move')!;
761+
762+
grid.engine.cleanNodes().beginUpdate(move);
763+
move._moving = true;
764+
grid.engine.cacheRects(CW, CH, 0, 0, 0, 0);
765+
766+
grid.engine.moveNodeCheck(move, {x: 1, y: 1, w: 2, h: 1, cellWidth: CW, cellHeight: CH,
767+
rect: {x: 1 * CW, y: 1 * CH, w: 2 * CW, h: 1 * CH}});
768+
769+
// the horizontal-only fallback still engaged (not frozen at its starting spot)...
770+
expect(move.x === 5 && move.y === 0).toBe(false);
771+
// ...but went through the normal collision/push logic rather than teleporting onto 'blocker'
772+
const ns = grid.engine.nodes;
773+
for (let i = 0; i < ns.length; i++) {
774+
for (let j = i + 1; j < ns.length; j++) {
775+
const a = ns[i], b = ns[j];
776+
const overlap = a.x! < b.x! + b.w! && b.x! < a.x! + a.w! && a.y! < b.y! + b.h! && b.y! < a.y! + a.h!;
777+
expect(overlap).toBe(false);
778+
}
779+
}
780+
grid.engine.endUpdate();
781+
});
782+
});
718783
});

‎src/gridstack-engine.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,13 +1039,30 @@ export class GridStackEngine {
10391039
// check to make sure we actually collided over 50% surface area while dragging
10401040
let collide = activeDrag ? this.directionCollideCoverage(node, o, collides) : collides[0];
10411041
// 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
1042-
if (activeDrag && this._makeDynamicSubGrid(node, o, collide)) collide = undefined;
1042+
const madeSubGrid = activeDrag && this._makeDynamicSubGrid(node, o, collide);
1043+
if (madeSubGrid) collide = undefined;
10431044

10441045
if (collide) {
10451046
needToMove = !this._fixCollisions(node, nn, collide, o); // check if already moved...
10461047
} else {
10471048
needToMove = false; // we didn't cover >50% for a move, skip...
10481049
if (wasUndefinedPack) delete o.pack;
1050+
1051+
// #2819 a diagonal drag can fail coverage purely because of the axis we're only grazing
1052+
// (ex: a sliver of vertical overlap against a much taller neighbor below), even though the
1053+
// axis we're actually traveling along is completely clear. Retry with the other, barely-
1054+
// moving axis locked back to its current value so the drag can keep sliding along the
1055+
// dominant direction instead of freezing solid until 50% coverage is met.
1056+
if (!madeSubGrid && activeDrag && nn.x !== node.x && nn.y !== node.y) {
1057+
const horizontal = Math.abs(nn.x! - node.x!) >= Math.abs(nn.y! - node.y!);
1058+
const nnAxis: GridStackNode = horizontal ? {...nn, y: node.y} : {...nn, x: node.x};
1059+
const collidesAxis = this.collideAll(node, nnAxis, o.skip);
1060+
const collideAxis = collidesAxis.length ? this.directionCollideCoverage(node, o, collidesAxis) : undefined;
1061+
if (!collidesAxis.length || collideAxis) {
1062+
needToMove = collidesAxis.length ? !this._fixCollisions(node, nnAxis, collideAxis, o) : true;
1063+
if (needToMove) Utils.copyPos(nn, nnAxis);
1064+
}
1065+
}
10491066
}
10501067
}
10511068

0 commit comments

Comments
 (0)