Skip to content

Commit 9a7d24f

Browse files
committed
Decouple and Configure Window Detachment Thresholds in SortZone #8160 wip
1 parent 711a59b commit 9a7d24f

File tree

4 files changed

+204
-17
lines changed

4 files changed

+204
-17
lines changed

src/draggable/container/SortZone.mjs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ class SortZone extends DragZone {
5555
* @protected
5656
*/
5757
currentIndex: -1,
58+
/**
59+
* The intersection ratio (0-1) required to detach an item into a new window.
60+
* Lower values mean the item must be dragged further out.
61+
* @member {Number} detachThreshold=0.8
62+
*/
63+
detachThreshold: 0.8,
5864
/**
5965
* A CSS selector to identify the drag handle within a component.
6066
* If specified, the drag is initiated on this element, but the owning component is dragged.
@@ -80,6 +86,11 @@ class SortZone extends DragZone {
8086
* @protected
8187
*/
8288
itemStyles: null,
89+
/**
90+
* @member {Number} lastIntersectionRatio=1
91+
* @protected
92+
*/
93+
lastIntersectionRatio: 1,
8394
/**
8495
* @member {Object} ownerRect=null
8596
* @protected
@@ -90,6 +101,12 @@ class SortZone extends DragZone {
90101
* @protected
91102
*/
92103
ownerStyle: null,
104+
/**
105+
* The intersection ratio (0-1) required to re-attach a window-dragged item back into the container.
106+
* Higher values mean the item must be dragged further in.
107+
* @member {Number} reattachThreshold=0.6
108+
*/
109+
reattachThreshold: 0.6,
93110
/**
94111
* @member {Boolean} alwaysFireDragMove=false
95112
* @protected
@@ -312,17 +329,22 @@ class SortZone extends DragZone {
312329
// console.log('SortZone onDragMove', me.dragProxy);
313330

314331
if (!me.isRemoteDragging && me.dragProxy && me.enableProxyToPopup) {
315-
const {proxyRect} = data;
332+
let {proxyRect} = data;
316333

317334
if (proxyRect && me.boundaryContainerRect) {
318335
const
319-
boundaryRect = me.boundaryContainerRect,
320-
intersection = Rectangle.getIntersection(proxyRect, boundaryRect),
321-
proxyArea = proxyRect.width * proxyRect.height,
322-
intersectionArea = intersection ? intersection.width * intersection.height : 0;
336+
boundaryRect = me.boundaryContainerRect,
337+
intersection = Rectangle.getIntersection(proxyRect, boundaryRect),
338+
proxyArea = proxyRect.width * proxyRect.height,
339+
intersectionArea = intersection ? intersection.width * intersection.height : 0,
340+
intersectionRatio = proxyArea > 0 ? intersectionArea / proxyArea : 0,
341+
isMovingIn = intersectionRatio > me.lastIntersectionRatio,
342+
isMovingOut = intersectionRatio < me.lastIntersectionRatio;
343+
344+
me.lastIntersectionRatio = intersectionRatio;
323345

324346
if (!me.isWindowDragging) {
325-
if (proxyArea > 0 && (intersectionArea / proxyArea) < 0.5) {
347+
if (isMovingOut && intersectionRatio < me.detachThreshold) {
326348
me.isWindowDragging = true; // Set flag to prevent re-entry
327349

328350
me.fire('dragBoundaryExit', {
@@ -333,7 +355,7 @@ class SortZone extends DragZone {
333355
return // Stop further processing in onDragMove
334356
}
335357
} else if (me.isWindowDragging) {
336-
if (proxyArea > 0 && (intersectionArea / proxyArea) > 0.51) {
358+
if (isMovingIn && intersectionRatio > me.reattachThreshold) {
337359
// Restore layout
338360
me.dragPlaceholder.wrapperStyle = {
339361
...me.dragPlaceholder.wrapperStyle,
@@ -504,6 +526,7 @@ class SortZone extends DragZone {
504526
dragElement : VDomUtil.find(owner.vdom, draggedItem.id).vdom,
505527
dragProxyConfig : me.getDragProxyConfig(),
506528
indexMap,
529+
lastIntersectionRatio : 1,
507530
ownerStyle : {height: ownerStyle.height, minWidth: ownerStyle.minWidth, width: ownerStyle.width},
508531
reversedLayoutDirection: layout.direction === 'column-reverse' || layout.direction === 'row-reverse',
509532
sortableItems,

src/draggable/dashboard/SortZone.mjs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,33 @@ class DashboardSortZone extends SortZone {
5252
DragCoordinator.register(this)
5353
}
5454

55+
/**
56+
* Checks if the remote drag coordinates intersect with the sort zone.
57+
* Triggers an async fetch of ownerRect if not currently cached.
58+
* @param {Number} x
59+
* @param {Number} y
60+
* @returns {Boolean}
61+
*/
62+
acceptsRemoteDrag(x, y) {
63+
let me = this;
64+
65+
if (!me.ownerRect) {
66+
if (!me.isFetchingRect) {
67+
me.isFetchingRect = true;
68+
me.owner.getDomRect([me.owner.id]).then(rects => {
69+
me.ownerRect = rects[0];
70+
me.isFetchingRect = false
71+
})
72+
}
73+
return false
74+
}
75+
76+
return x >= me.ownerRect.x &&
77+
x <= me.ownerRect.x + me.ownerRect.width &&
78+
y >= me.ownerRect.y &&
79+
y <= me.ownerRect.y + me.ownerRect.height
80+
}
81+
5582
/**
5683
*
5784
*/
@@ -270,17 +297,21 @@ class DashboardSortZone extends SortZone {
270297
}
271298

272299
if (!me.isRemoteDragging && me.dragProxy && me.enableProxyToPopup) {
273-
const {proxyRect} = data;
300+
let {proxyRect} = data;
274301

275302
if (proxyRect && me.boundaryContainerRect) {
276303
const
277-
boundaryRect = me.boundaryContainerRect,
278-
intersection = Rectangle.getIntersection(proxyRect, boundaryRect),
279-
proxyArea = proxyRect.width * proxyRect.height,
280-
intersectionArea = intersection ? intersection.width * intersection.height : 0;
304+
boundaryRect = me.boundaryContainerRect,
305+
intersection = Rectangle.getIntersection(proxyRect, boundaryRect),
306+
proxyArea = proxyRect.width * proxyRect.height,
307+
intersectionArea = intersection ? intersection.width * intersection.height : 0,
308+
intersectionRatio = proxyArea > 0 ? intersectionArea / proxyArea : 0,
309+
isMovingIn = intersectionRatio > me.lastIntersectionRatio;
281310

282311
if (me.isWindowDragging) {
283-
if (!(proxyArea > 0 && (intersectionArea / proxyArea) > 0.51)) {
312+
if (!(isMovingIn && intersectionRatio > me.reattachThreshold)) {
313+
me.lastIntersectionRatio = intersectionRatio;
314+
284315
// Signal Coordinator
285316
DragCoordinator.onDragMove({
286317
draggedItem : me.dragComponent,

src/manager/DragCoordinator.mjs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import Manager from './Base.mjs';
2-
import Window from './Window.mjs';
1+
import Manager from './Base.mjs';
2+
import Rectangle from '../util/Rectangle.mjs';
3+
import Window from './Window.mjs';
34

45
/**
56
* @class Neo.manager.DragCoordinator
@@ -70,7 +71,13 @@ class DragCoordinator extends Manager {
7071
if (targetSortZone) {
7172
let targetWindow = Window.get(targetWindowId),
7273
localX = screenX - targetWindow.innerRect.x,
73-
localY = screenY - targetWindow.innerRect.y;
74+
localY = screenY - targetWindow.innerRect.y,
75+
targetProxyRect = new Rectangle(
76+
localX - offsetX,
77+
localY - offsetY,
78+
proxyRect.width,
79+
proxyRect.height
80+
);
7481

7582
if (targetSortZone.acceptsRemoteDrag(localX, localY)) {
7683
// console.log('DragCoordinator target found', {targetWindowId, localX, localY});
@@ -95,7 +102,7 @@ class DragCoordinator extends Manager {
95102
localY,
96103
offsetX,
97104
offsetY,
98-
proxyRect
105+
proxyRect: targetProxyRect
99106
});
100107

101108
return
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {setup} from '../../../setup.mjs';
2+
3+
const appName = 'DashboardSortZoneTest';
4+
5+
setup({
6+
appConfig: {
7+
name: appName
8+
}
9+
});
10+
11+
import {test, expect} from '@playwright/test';
12+
import Neo from '../../../../../src/Neo.mjs';
13+
import * as core from '../../../../../src/core/_export.mjs';
14+
import InstanceManager from '../../../../../src/manager/Instance.mjs';
15+
16+
/**
17+
* @summary Tests for Neo.draggable.dashboard.SortZone directional thresholds
18+
*/
19+
test.describe.serial('Neo.draggable.dashboard.SortZone Directional Logic', () => {
20+
let DashboardSortZone, Rectangle, sortZone;
21+
22+
test.beforeAll(async () => {
23+
Neo.currentWorker = {
24+
on : () => {},
25+
sendMessage: () => {},
26+
isSharedWorker: false
27+
};
28+
29+
const sortZoneModule = await import('../../../../../src/draggable/dashboard/SortZone.mjs');
30+
DashboardSortZone = sortZoneModule.default;
31+
32+
const rectModule = await import('../../../../../src/util/Rectangle.mjs');
33+
Rectangle = rectModule.default;
34+
});
35+
36+
test.beforeEach(() => {
37+
Neo.ns('Neo.main.addon.DragDrop', true);
38+
Neo.main.addon.DragDrop = {
39+
setConfigs : () => Promise.resolve({boundaryContainerRect: {}}),
40+
setDragProxyElement: () => Promise.resolve(),
41+
startWindowDrag : () => Promise.resolve()
42+
};
43+
44+
const DragCoordinator = Neo.manager?.DragCoordinator;
45+
if (DragCoordinator) {
46+
DragCoordinator.onDragMove = () => {};
47+
DragCoordinator.register = () => {};
48+
DragCoordinator.unregister = () => {};
49+
}
50+
});
51+
52+
test.afterEach(() => {
53+
sortZone?.destroy();
54+
});
55+
56+
test('Validates Directional Logic with Colliding Thresholds (0.8 / 0.6)', async () => {
57+
const mockOwner = {
58+
id: 'mockOwner',
59+
items: [{
60+
id: 'item1',
61+
vdom: {cls: ['neo-draggable']},
62+
wrapperStyle: {}
63+
}],
64+
vdom: {},
65+
addDomListeners: () => {},
66+
getDomRect: () => Promise.resolve([{x:0, y:0, width:100, height:100}]),
67+
on: () => {}
68+
};
69+
70+
sortZone = Neo.create(DashboardSortZone, {
71+
owner: mockOwner,
72+
detachThreshold : 0.8,
73+
reattachThreshold: 0.6,
74+
enableProxyToPopup: true
75+
});
76+
77+
sortZone.boundaryContainerRect = new Rectangle(0, 0, 100, 100);
78+
sortZone.dragProxy = { id: 'proxy', destroy: () => {} };
79+
sortZone.dragPlaceholder = { id: 'placeholder', wrapperStyle: {}, destroy: () => {} };
80+
81+
sortZone.lastIntersectionRatio = 1;
82+
sortZone.isWindowDragging = false;
83+
84+
const simulateMove = async (x, y) => {
85+
const proxyRect = new Rectangle(x, y, 100, 100);
86+
await sortZone.onDragMove({
87+
clientX: x,
88+
clientY: y,
89+
proxyRect,
90+
screenX: x,
91+
screenY: y,
92+
path: []
93+
});
94+
};
95+
96+
sortZone.itemRects = [{left:0, top:0, width:100, height:100}];
97+
sortZone.indexMap = {0: 0};
98+
sortZone.currentIndex = 0;
99+
sortZone.isScrolling = false;
100+
101+
sortZone.fire = (event) => {
102+
if (event === 'dragBoundaryExit') sortZone.isWindowDragging = true;
103+
if (event === 'dragBoundaryEntry') sortZone.isWindowDragging = false;
104+
};
105+
106+
await simulateMove(10, 0);
107+
expect(sortZone.lastIntersectionRatio).toBe(0.9);
108+
expect(sortZone.isWindowDragging).toBe(false);
109+
110+
await simulateMove(25, 0);
111+
expect(sortZone.lastIntersectionRatio).toBe(0.75);
112+
expect(sortZone.isWindowDragging).toBe(true);
113+
114+
await simulateMove(30, 0);
115+
expect(sortZone.lastIntersectionRatio).toBeCloseTo(0.70);
116+
expect(sortZone.isWindowDragging).toBe(true);
117+
118+
await simulateMove(25, 0);
119+
expect(sortZone.lastIntersectionRatio).toBe(0.75);
120+
expect(sortZone.isWindowDragging).toBe(false);
121+
122+
await simulateMove(30, 0);
123+
expect(sortZone.lastIntersectionRatio).toBeCloseTo(0.70);
124+
expect(sortZone.isWindowDragging).toBe(true);
125+
});
126+
});

0 commit comments

Comments
 (0)