Skip to content

Commit e76255e

Browse files
authored
refactor(ui5-list, ui5-tree, ui5-list-item-group): extract drag-and-drop logic (#11928)
- Create reusable DragAndDropHandler delegate class - Refactor List, Tree, and ListItemGroup to use shared handler - Remove duplicated drag and drop logic across components - Improve maintainability and consistency of drag/drop behavior - Support configurable drag validation and placement filtering This consolidates ~200 lines of duplicated code into a single, configurable handler that can be reused across all drag-enabled components. Fixes #11120
1 parent 7460a76 commit e76255e

File tree

4 files changed

+232
-147
lines changed

4 files changed

+232
-147
lines changed

packages/main/src/List.ts

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,10 @@ import {
2222
isDown,
2323
isUp,
2424
} from "@ui5/webcomponents-base/dist/Keys.js";
25-
import handleDragOver from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDragOver.js";
26-
import handleDrop from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDrop.js";
27-
import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js";
2825
import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js";
26+
import DragAndDropHandler from "./delegate/DragAndDropHandler.js";
2927
import type { MoveEventDetail } from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js";
30-
import { findClosestPosition, findClosestPositionsByKey } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js";
28+
import { findClosestPositionsByKey } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js";
3129
import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js";
3230
import {
3331
getAllAccessibleDescriptionRefTexts,
@@ -543,6 +541,8 @@ class List extends UI5Element {
543541
onForwardBeforeBound: (e: CustomEvent) => void;
544542
onItemTabIndexChangeBound: (e: CustomEvent) => void;
545543

544+
_dragAndDropHandler: DragAndDropHandler;
545+
546546
constructor() {
547547
super();
548548

@@ -572,6 +572,14 @@ class List extends UI5Element {
572572
this.onForwardAfterBound = this.onForwardAfter.bind(this);
573573
this.onForwardBeforeBound = this.onForwardBefore.bind(this);
574574
this.onItemTabIndexChangeBound = this.onItemTabIndexChange.bind(this);
575+
576+
// Initialize the DragAndDropHandler with the necessary configurations
577+
// The handler will manage the drag and drop operations for the list items.
578+
this._dragAndDropHandler = new DragAndDropHandler(this, {
579+
getItems: () => this.items,
580+
getDropIndicator: () => this.dropIndicatorDOM,
581+
useOriginalEvent: true,
582+
});
575583
}
576584

577585
/**
@@ -1193,46 +1201,19 @@ class List extends UI5Element {
11931201
}
11941202

11951203
_ondragenter(e: DragEvent) {
1196-
e.preventDefault();
1204+
this._dragAndDropHandler.ondragenter(e);
11971205
}
11981206

11991207
_ondragleave(e: DragEvent) {
1200-
if (e.relatedTarget instanceof Node && this.shadowRoot!.contains(e.relatedTarget)) {
1201-
return;
1202-
}
1203-
1204-
this.dropIndicatorDOM!.targetReference = null;
1208+
this._dragAndDropHandler.ondragleave(e);
12051209
}
12061210

12071211
_ondragover(e: DragEvent) {
1208-
if (!(e.target instanceof HTMLElement)) {
1209-
return;
1210-
}
1211-
1212-
const closestPosition = findClosestPosition(
1213-
this.items,
1214-
e.clientY,
1215-
Orientation.Vertical,
1216-
);
1217-
1218-
if (!closestPosition) {
1219-
this.dropIndicatorDOM!.targetReference = null;
1220-
return;
1221-
}
1222-
1223-
const { targetReference, placement } = handleDragOver(e, this, closestPosition, closestPosition.element, { originalEvent: true });
1224-
this.dropIndicatorDOM!.targetReference = targetReference;
1225-
this.dropIndicatorDOM!.placement = placement;
1212+
this._dragAndDropHandler.ondragover(e);
12261213
}
12271214

12281215
_ondrop(e: DragEvent) {
1229-
if (!this.dropIndicatorDOM?.targetReference || !this.dropIndicatorDOM?.placement) {
1230-
e.preventDefault();
1231-
return;
1232-
}
1233-
1234-
handleDrop(e, this, this.dropIndicatorDOM.targetReference, this.dropIndicatorDOM.placement, { originalEvent: true });
1235-
this.dropIndicatorDOM.targetReference = null;
1216+
this._dragAndDropHandler.ondrop(e);
12361217
}
12371218

12381219
isForwardElement(element: HTMLElement) {

packages/main/src/ListItemGroup.ts

Lines changed: 26 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
55
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
66
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
77
import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js";
8-
import { findClosestPosition } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js";
9-
import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js";
8+
import DragAndDropHandler from "./delegate/DragAndDropHandler.js";
109
import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js";
1110
import type DropIndicator from "./DropIndicator.js";
1211
import type ListItemBase from "./ListItemBase.js";
@@ -148,6 +147,20 @@ class ListItemGroup extends UI5Element {
148147
@slot({ type: HTMLElement })
149148
header!: Array<ListItemBase>;
150149

150+
_dragAndDropHandler: DragAndDropHandler;
151+
152+
constructor() {
153+
super();
154+
155+
// Initialize the DragAndDropHandler with the necessary configurations
156+
// The handler will manage the drag and drop operations for the list items.
157+
this._dragAndDropHandler = new DragAndDropHandler(this, {
158+
getItems: () => this.items,
159+
getDropIndicator: () => this.dropIndicatorDOM,
160+
filterPlacements: this._filterPlacements.bind(this),
161+
});
162+
}
163+
151164
onEnterDOM() {
152165
DragRegistry.subscribe(this);
153166
}
@@ -177,81 +190,27 @@ class ListItemGroup extends UI5Element {
177190
}
178191

179192
_ondragenter(e: DragEvent) {
180-
e.preventDefault();
193+
this._dragAndDropHandler.ondragenter(e);
181194
}
182195

183196
_ondragleave(e: DragEvent) {
184-
if (e.relatedTarget instanceof Node && this.shadowRoot!.contains(e.relatedTarget)) {
185-
return;
186-
}
187-
188-
this.dropIndicatorDOM!.targetReference = null;
197+
this._dragAndDropHandler.ondragleave(e);
189198
}
190199

191200
_ondragover(e: DragEvent) {
192-
const draggedElement = DragRegistry.getDraggedElement();
193-
194-
if (!(e.target instanceof HTMLElement) || !draggedElement) {
195-
return;
196-
}
197-
198-
const closestPosition = findClosestPosition(
199-
this.items,
200-
e.clientY,
201-
Orientation.Vertical,
202-
);
203-
204-
if (!closestPosition) {
205-
this.dropIndicatorDOM!.targetReference = null;
206-
return;
207-
}
208-
209-
let placements = closestPosition.placements;
210-
211-
if (closestPosition.element === draggedElement) {
212-
placements = placements.filter(placement => placement !== MovePlacement.On);
213-
}
214-
215-
const placementAccepted = placements.some(placement => {
216-
const beforeItemMovePrevented = !this.fireDecoratorEvent("move-over", {
217-
source: {
218-
element: draggedElement,
219-
},
220-
destination: {
221-
element: closestPosition.element,
222-
placement,
223-
},
224-
});
225-
226-
if (beforeItemMovePrevented) {
227-
e.preventDefault();
228-
this.dropIndicatorDOM!.targetReference = closestPosition.element;
229-
this.dropIndicatorDOM!.placement = placement;
230-
return true;
231-
}
232-
233-
return false;
234-
});
235-
236-
if (!placementAccepted) {
237-
this.dropIndicatorDOM!.targetReference = null;
238-
}
201+
this._dragAndDropHandler.ondragover(e);
239202
}
240203

241204
_ondrop(e: DragEvent) {
242-
e.preventDefault();
243-
244-
this.fireDecoratorEvent("move", {
245-
source: {
246-
element: DragRegistry.getDraggedElement()!,
247-
},
248-
destination: {
249-
element: this.dropIndicatorDOM!.targetReference!,
250-
placement: this.dropIndicatorDOM!.placement,
251-
},
252-
});
205+
this._dragAndDropHandler.ondrop(e);
206+
}
253207

254-
this.dropIndicatorDOM!.targetReference = null;
208+
_filterPlacements(placements: MovePlacement[], draggedElement: HTMLElement, targetElement: HTMLElement): MovePlacement[] {
209+
// Filter out MovePlacement.On when dragged element is the same as target
210+
if (targetElement === draggedElement) {
211+
return placements.filter(placement => placement !== MovePlacement.On);
212+
}
213+
return placements;
255214
}
256215
}
257216

packages/main/src/Tree.ts

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
33
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
44
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
55
import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js";
6-
import handleDragOver from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDragOver.js";
7-
import handleDrop from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDrop.js";
8-
import { findClosestPosition } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js";
9-
import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js";
6+
import DragAndDropHandler from "./delegate/DragAndDropHandler.js";
107
import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js";
118
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
129
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
@@ -311,6 +308,22 @@ class Tree extends UI5Element {
311308
@slot()
312309
header!: Array<HTMLElement>;
313310

311+
_dragAndDropHandler: DragAndDropHandler;
312+
313+
constructor() {
314+
super();
315+
316+
// Initialize the DragAndDropHandler with the necessary configurations
317+
// The handler will manage the drag and drop operations for the tree items.
318+
this._dragAndDropHandler = new DragAndDropHandler(this, {
319+
getItems: this._getItems.bind(this),
320+
getDropIndicator: () => this.dropIndicatorDOM,
321+
transformElement: this._transformElement.bind(this),
322+
validateDraggedElement: this._validateDraggedElement.bind(this),
323+
filterPlacements: this._filterPlacements.bind(this),
324+
});
325+
}
326+
314327
onEnterDOM() {
315328
DragRegistry.subscribe(this);
316329
}
@@ -346,56 +359,19 @@ class Tree extends UI5Element {
346359
}
347360

348361
_ondragenter(e: DragEvent) {
349-
e.preventDefault();
362+
this._dragAndDropHandler.ondragenter(e);
350363
}
351364

352365
_ondragleave(e: DragEvent) {
353-
if (e.relatedTarget instanceof Node && this.shadowRoot!.contains(e.relatedTarget)) {
354-
return;
355-
}
356-
357-
this.dropIndicatorDOM!.targetReference = null;
366+
this._dragAndDropHandler.ondragleave(e);
358367
}
359368

360369
_ondragover(e: DragEvent) {
361-
const draggedElement = DragRegistry.getDraggedElement();
362-
const allLiNodesTraversed: Array<HTMLElement> = []; // use the only <li> nodes to determine positioning
363-
if (!(e.target instanceof HTMLElement) || !draggedElement) {
364-
return;
365-
}
366-
367-
this.walk(item => {
368-
allLiNodesTraversed.push(item.shadowRoot!.querySelector("li")!);
369-
});
370-
371-
const closestPosition = findClosestPosition(
372-
allLiNodesTraversed,
373-
e.clientY,
374-
Orientation.Vertical,
375-
);
376-
377-
if (!closestPosition) {
378-
this.dropIndicatorDOM!.targetReference = null;
379-
return;
380-
}
381-
382-
closestPosition.element = <HTMLElement>(<ShadowRoot>closestPosition.element.getRootNode()).host;
383-
if (draggedElement.contains(closestPosition.element)) { return; }
384-
if (closestPosition.element === draggedElement) {
385-
closestPosition.placements = closestPosition.placements.filter(placement => placement !== MovePlacement.On);
386-
}
387-
388-
const { targetReference, placement } = handleDragOver(e, this, closestPosition, closestPosition.element);
389-
this.dropIndicatorDOM!.targetReference = targetReference;
390-
this.dropIndicatorDOM!.placement = placement;
370+
this._dragAndDropHandler.ondragover(e);
391371
}
392372

393373
_ondrop(e: DragEvent) {
394-
if (!this.dropIndicatorDOM?.targetReference || !this.dropIndicatorDOM?.placement) {
395-
return;
396-
}
397-
handleDrop(e, this, this.dropIndicatorDOM.targetReference, this.dropIndicatorDOM.placement);
398-
this.dropIndicatorDOM.targetReference = null;
374+
this._dragAndDropHandler.ondrop(e);
399375
}
400376

401377
_onListItemStepIn(e: CustomEvent<TreeItemBaseStepInEventDetail>) {
@@ -531,6 +507,32 @@ class Tree extends UI5Element {
531507
walkTree(this, 1, callback);
532508
}
533509

510+
_getItems(): Array<HTMLElement> {
511+
const allLiNodesTraversed: Array<HTMLElement> = [];
512+
this.walk(item => {
513+
allLiNodesTraversed.push(item.shadowRoot!.querySelector("li")!);
514+
});
515+
return allLiNodesTraversed;
516+
}
517+
518+
_transformElement(element: HTMLElement): HTMLElement {
519+
// Get the host element from shadow DOM
520+
return <HTMLElement>(<ShadowRoot>element.getRootNode()).host;
521+
}
522+
523+
_validateDraggedElement(draggedElement: HTMLElement, targetElement: HTMLElement): boolean {
524+
// Don't allow dropping on itself or its children
525+
return !draggedElement.contains(targetElement);
526+
}
527+
528+
_filterPlacements(placements: MovePlacement[], draggedElement: HTMLElement, targetElement: HTMLElement): MovePlacement[] {
529+
// Filter out MovePlacement.On when dragged element is the same as target
530+
if (targetElement === draggedElement) {
531+
return placements.filter(placement => placement !== MovePlacement.On);
532+
}
533+
return placements;
534+
}
535+
534536
_isInstanceOfTreeItemBase(object: any): object is TreeItemBase {
535537
return "isTreeItem" in object;
536538
}

0 commit comments

Comments
 (0)