diff --git a/packages/main/src/List.ts b/packages/main/src/List.ts index 6baae21e9964..195c18227460 100644 --- a/packages/main/src/List.ts +++ b/packages/main/src/List.ts @@ -22,12 +22,10 @@ import { isDown, isUp, } from "@ui5/webcomponents-base/dist/Keys.js"; -import handleDragOver from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDragOver.js"; -import handleDrop from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDrop.js"; -import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js"; import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; +import DragAndDropHandler from "./delegate/DragAndDropHandler.js"; import type { MoveEventDetail } from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; -import { findClosestPosition, findClosestPositionsByKey } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js"; +import { findClosestPositionsByKey } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js"; import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js"; import { getAllAccessibleDescriptionRefTexts, @@ -543,6 +541,8 @@ class List extends UI5Element { onForwardBeforeBound: (e: CustomEvent) => void; onItemTabIndexChangeBound: (e: CustomEvent) => void; + _dragAndDropHandler: DragAndDropHandler; + constructor() { super(); @@ -572,6 +572,14 @@ class List extends UI5Element { this.onForwardAfterBound = this.onForwardAfter.bind(this); this.onForwardBeforeBound = this.onForwardBefore.bind(this); this.onItemTabIndexChangeBound = this.onItemTabIndexChange.bind(this); + + // Initialize the DragAndDropHandler with the necessary configurations + // The handler will manage the drag and drop operations for the list items. + this._dragAndDropHandler = new DragAndDropHandler(this, { + getItems: () => this.items, + getDropIndicator: () => this.dropIndicatorDOM, + useOriginalEvent: true, + }); } /** @@ -1193,46 +1201,19 @@ class List extends UI5Element { } _ondragenter(e: DragEvent) { - e.preventDefault(); + this._dragAndDropHandler.ondragenter(e); } _ondragleave(e: DragEvent) { - if (e.relatedTarget instanceof Node && this.shadowRoot!.contains(e.relatedTarget)) { - return; - } - - this.dropIndicatorDOM!.targetReference = null; + this._dragAndDropHandler.ondragleave(e); } _ondragover(e: DragEvent) { - if (!(e.target instanceof HTMLElement)) { - return; - } - - const closestPosition = findClosestPosition( - this.items, - e.clientY, - Orientation.Vertical, - ); - - if (!closestPosition) { - this.dropIndicatorDOM!.targetReference = null; - return; - } - - const { targetReference, placement } = handleDragOver(e, this, closestPosition, closestPosition.element, { originalEvent: true }); - this.dropIndicatorDOM!.targetReference = targetReference; - this.dropIndicatorDOM!.placement = placement; + this._dragAndDropHandler.ondragover(e); } _ondrop(e: DragEvent) { - if (!this.dropIndicatorDOM?.targetReference || !this.dropIndicatorDOM?.placement) { - e.preventDefault(); - return; - } - - handleDrop(e, this, this.dropIndicatorDOM.targetReference, this.dropIndicatorDOM.placement, { originalEvent: true }); - this.dropIndicatorDOM.targetReference = null; + this._dragAndDropHandler.ondrop(e); } isForwardElement(element: HTMLElement) { diff --git a/packages/main/src/ListItemGroup.ts b/packages/main/src/ListItemGroup.ts index 9fd3a701d340..b97829abb761 100644 --- a/packages/main/src/ListItemGroup.ts +++ b/packages/main/src/ListItemGroup.ts @@ -5,8 +5,7 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; -import { findClosestPosition } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js"; -import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js"; +import DragAndDropHandler from "./delegate/DragAndDropHandler.js"; import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; import type DropIndicator from "./DropIndicator.js"; import type ListItemBase from "./ListItemBase.js"; @@ -148,6 +147,20 @@ class ListItemGroup extends UI5Element { @slot({ type: HTMLElement }) header!: Array; + _dragAndDropHandler: DragAndDropHandler; + + constructor() { + super(); + + // Initialize the DragAndDropHandler with the necessary configurations + // The handler will manage the drag and drop operations for the list items. + this._dragAndDropHandler = new DragAndDropHandler(this, { + getItems: () => this.items, + getDropIndicator: () => this.dropIndicatorDOM, + filterPlacements: this._filterPlacements.bind(this), + }); + } + onEnterDOM() { DragRegistry.subscribe(this); } @@ -177,81 +190,27 @@ class ListItemGroup extends UI5Element { } _ondragenter(e: DragEvent) { - e.preventDefault(); + this._dragAndDropHandler.ondragenter(e); } _ondragleave(e: DragEvent) { - if (e.relatedTarget instanceof Node && this.shadowRoot!.contains(e.relatedTarget)) { - return; - } - - this.dropIndicatorDOM!.targetReference = null; + this._dragAndDropHandler.ondragleave(e); } _ondragover(e: DragEvent) { - const draggedElement = DragRegistry.getDraggedElement(); - - if (!(e.target instanceof HTMLElement) || !draggedElement) { - return; - } - - const closestPosition = findClosestPosition( - this.items, - e.clientY, - Orientation.Vertical, - ); - - if (!closestPosition) { - this.dropIndicatorDOM!.targetReference = null; - return; - } - - let placements = closestPosition.placements; - - if (closestPosition.element === draggedElement) { - placements = placements.filter(placement => placement !== MovePlacement.On); - } - - const placementAccepted = placements.some(placement => { - const beforeItemMovePrevented = !this.fireDecoratorEvent("move-over", { - source: { - element: draggedElement, - }, - destination: { - element: closestPosition.element, - placement, - }, - }); - - if (beforeItemMovePrevented) { - e.preventDefault(); - this.dropIndicatorDOM!.targetReference = closestPosition.element; - this.dropIndicatorDOM!.placement = placement; - return true; - } - - return false; - }); - - if (!placementAccepted) { - this.dropIndicatorDOM!.targetReference = null; - } + this._dragAndDropHandler.ondragover(e); } _ondrop(e: DragEvent) { - e.preventDefault(); - - this.fireDecoratorEvent("move", { - source: { - element: DragRegistry.getDraggedElement()!, - }, - destination: { - element: this.dropIndicatorDOM!.targetReference!, - placement: this.dropIndicatorDOM!.placement, - }, - }); + this._dragAndDropHandler.ondrop(e); + } - this.dropIndicatorDOM!.targetReference = null; + _filterPlacements(placements: MovePlacement[], draggedElement: HTMLElement, targetElement: HTMLElement): MovePlacement[] { + // Filter out MovePlacement.On when dragged element is the same as target + if (targetElement === draggedElement) { + return placements.filter(placement => placement !== MovePlacement.On); + } + return placements; } } diff --git a/packages/main/src/Tree.ts b/packages/main/src/Tree.ts index 8ad22b8d1543..9d9bed5e4f12 100644 --- a/packages/main/src/Tree.ts +++ b/packages/main/src/Tree.ts @@ -3,10 +3,7 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; -import handleDragOver from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDragOver.js"; -import handleDrop from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDrop.js"; -import { findClosestPosition } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js"; -import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js"; +import DragAndDropHandler from "./delegate/DragAndDropHandler.js"; import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; @@ -311,6 +308,22 @@ class Tree extends UI5Element { @slot() header!: Array; + _dragAndDropHandler: DragAndDropHandler; + + constructor() { + super(); + + // Initialize the DragAndDropHandler with the necessary configurations + // The handler will manage the drag and drop operations for the tree items. + this._dragAndDropHandler = new DragAndDropHandler(this, { + getItems: this._getItems.bind(this), + getDropIndicator: () => this.dropIndicatorDOM, + transformElement: this._transformElement.bind(this), + validateDraggedElement: this._validateDraggedElement.bind(this), + filterPlacements: this._filterPlacements.bind(this), + }); + } + onEnterDOM() { DragRegistry.subscribe(this); } @@ -346,56 +359,19 @@ class Tree extends UI5Element { } _ondragenter(e: DragEvent) { - e.preventDefault(); + this._dragAndDropHandler.ondragenter(e); } _ondragleave(e: DragEvent) { - if (e.relatedTarget instanceof Node && this.shadowRoot!.contains(e.relatedTarget)) { - return; - } - - this.dropIndicatorDOM!.targetReference = null; + this._dragAndDropHandler.ondragleave(e); } _ondragover(e: DragEvent) { - const draggedElement = DragRegistry.getDraggedElement(); - const allLiNodesTraversed: Array = []; // use the only
  • nodes to determine positioning - if (!(e.target instanceof HTMLElement) || !draggedElement) { - return; - } - - this.walk(item => { - allLiNodesTraversed.push(item.shadowRoot!.querySelector("li")!); - }); - - const closestPosition = findClosestPosition( - allLiNodesTraversed, - e.clientY, - Orientation.Vertical, - ); - - if (!closestPosition) { - this.dropIndicatorDOM!.targetReference = null; - return; - } - - closestPosition.element = (closestPosition.element.getRootNode()).host; - if (draggedElement.contains(closestPosition.element)) { return; } - if (closestPosition.element === draggedElement) { - closestPosition.placements = closestPosition.placements.filter(placement => placement !== MovePlacement.On); - } - - const { targetReference, placement } = handleDragOver(e, this, closestPosition, closestPosition.element); - this.dropIndicatorDOM!.targetReference = targetReference; - this.dropIndicatorDOM!.placement = placement; + this._dragAndDropHandler.ondragover(e); } _ondrop(e: DragEvent) { - if (!this.dropIndicatorDOM?.targetReference || !this.dropIndicatorDOM?.placement) { - return; - } - handleDrop(e, this, this.dropIndicatorDOM.targetReference, this.dropIndicatorDOM.placement); - this.dropIndicatorDOM.targetReference = null; + this._dragAndDropHandler.ondrop(e); } _onListItemStepIn(e: CustomEvent) { @@ -531,6 +507,32 @@ class Tree extends UI5Element { walkTree(this, 1, callback); } + _getItems(): Array { + const allLiNodesTraversed: Array = []; + this.walk(item => { + allLiNodesTraversed.push(item.shadowRoot!.querySelector("li")!); + }); + return allLiNodesTraversed; + } + + _transformElement(element: HTMLElement): HTMLElement { + // Get the host element from shadow DOM + return (element.getRootNode()).host; + } + + _validateDraggedElement(draggedElement: HTMLElement, targetElement: HTMLElement): boolean { + // Don't allow dropping on itself or its children + return !draggedElement.contains(targetElement); + } + + _filterPlacements(placements: MovePlacement[], draggedElement: HTMLElement, targetElement: HTMLElement): MovePlacement[] { + // Filter out MovePlacement.On when dragged element is the same as target + if (targetElement === draggedElement) { + return placements.filter(placement => placement !== MovePlacement.On); + } + return placements; + } + _isInstanceOfTreeItemBase(object: any): object is TreeItemBase { return "isTreeItem" in object; } diff --git a/packages/main/src/delegate/DragAndDropHandler.ts b/packages/main/src/delegate/DragAndDropHandler.ts new file mode 100644 index 000000000000..ea776bd6190a --- /dev/null +++ b/packages/main/src/delegate/DragAndDropHandler.ts @@ -0,0 +1,143 @@ +import DragRegistry from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; +import handleDragOver from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDragOver.js"; +import handleDrop from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDrop.js"; +import { findClosestPosition } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js"; +import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js"; +import type UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import type DropIndicator from "../DropIndicator.js"; +import type MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; + +type DragAndDropConfig = { + getItems: () => Array; + getDropIndicator: () => DropIndicator | null; + orientation?: Orientation; + useOriginalEvent?: boolean; + clientCoordinate?: "clientX" | "clientY"; + transformElement?: (element: HTMLElement) => HTMLElement; + validateDraggedElement?: (draggedElement: HTMLElement, targetElement: HTMLElement) => boolean; + filterPlacements?: (placements: MovePlacement[], draggedElement: HTMLElement, targetElement: HTMLElement) => MovePlacement[]; +}; + +class DragAndDropHandler { + component: UI5Element; + config: DragAndDropConfig; + + constructor(component: UI5Element, config: DragAndDropConfig) { + this.component = component; + this.config = { + orientation: Orientation.Vertical, + clientCoordinate: "clientY", + ...config, + }; + } + + ondragenter(e: DragEvent) { + e.preventDefault(); + } + + ondragleave(e: DragEvent) { + if (e.relatedTarget instanceof Node && this.component.shadowRoot?.contains(e.relatedTarget)) { + return; + } + + const dropIndicator = this.config.getDropIndicator(); + if (dropIndicator) { + dropIndicator.targetReference = null; + } + } + + ondragover(e: DragEvent) { + if (!this._validateDragOver(e)) { + return; + } + + const draggedElement = DragRegistry.getDraggedElement()!; + const dropIndicator = this.config.getDropIndicator()!; + const closestPosition = this._findClosestPosition(e); + + if (!closestPosition) { + dropIndicator.targetReference = null; + return; + } + + const targetElement = this._transformTargetElement(closestPosition.element); + + if (!this._isValidDragTarget(draggedElement, targetElement)) { + dropIndicator.targetReference = null; + return; + } + + // Filter placements if needed (e.g., ListItemGroup filtering out MovePlacement.On) + const placements = this._filterPlacements(closestPosition.placements, draggedElement, targetElement); + + const settings = this.config.useOriginalEvent ? { originalEvent: true } : {}; + const { targetReference, placement } = handleDragOver(e, this.component, { + element: targetElement, + placements, + }, targetElement, settings); + + dropIndicator.targetReference = targetReference; + dropIndicator.placement = placement; + } + + ondrop(e: DragEvent) { + const dropIndicator = this.config.getDropIndicator(); + + if (!dropIndicator?.targetReference || !dropIndicator?.placement) { + e.preventDefault(); + return; + } + + const settings = this.config.useOriginalEvent ? { originalEvent: true } : {}; + handleDrop(e, this.component, dropIndicator.targetReference, dropIndicator.placement, settings); + dropIndicator.targetReference = null; + } + + _validateDragOver(e: DragEvent): boolean { + if (!(e.target instanceof HTMLElement)) { + return false; + } + + const draggedElement = DragRegistry.getDraggedElement(); + const dropIndicator = this.config.getDropIndicator(); + + return !!(draggedElement && dropIndicator); + } + + _findClosestPosition(e: DragEvent) { + const items = this.config.getItems(); + const coordinate = this.config.clientCoordinate === "clientX" ? e.clientX : e.clientY; + + return findClosestPosition( + items, + coordinate, + this.config.orientation!, + ); + } + + _transformTargetElement(element: HTMLElement): HTMLElement { + if (this.config.transformElement) { + return this.config.transformElement(element); + } + return element; + } + + _isValidDragTarget(draggedElement: HTMLElement, targetElement: HTMLElement): boolean { + if (this.config.validateDraggedElement) { + return this.config.validateDraggedElement(draggedElement, targetElement); + } + return true; + } + + _filterPlacements(placements: MovePlacement[], draggedElement: HTMLElement, targetElement: HTMLElement): MovePlacement[] { + if (this.config.filterPlacements) { + return this.config.filterPlacements(placements, draggedElement, targetElement); + } + return placements; + } +} + +export default DragAndDropHandler; +export type { + DragAndDropConfig, +};