diff --git a/packages/base/src/util/dragAndDrop/DragRegistry.ts b/packages/base/src/util/dragAndDrop/DragRegistry.ts index 8a63cc0d1af4..8c960411b9d2 100644 --- a/packages/base/src/util/dragAndDrop/DragRegistry.ts +++ b/packages/base/src/util/dragAndDrop/DragRegistry.ts @@ -1,4 +1,5 @@ import type UI5Element from "../../UI5Element.js"; +import type MovePlacement from "../../types/MovePlacement.js"; let draggedElement: HTMLElement | null = null; let globalHandlersAttached = false; @@ -75,6 +76,28 @@ const removeSelfManagedArea = (area: HTMLElement | ShadowRoot) => { selfManagedDragAreas.delete(area); }; +type DragAndDropSettings = { + /** + * Allow cross-browser and file drag and drop. + */ + crossDnD?: boolean; + /** + * Pass the original event in the event parameters. + */ + originalEvent?: boolean; +}; + +type MoveEventDetail = { + originalEvent: Event, + source: { + element: HTMLElement, + }, + destination: { + element: HTMLElement, + placement: `${MovePlacement}`, + } +}; + const DragRegistry = { subscribe, unsubscribe, @@ -86,4 +109,6 @@ const DragRegistry = { export default DragRegistry; export type { SetDraggedElementFunction, + DragAndDropSettings, + MoveEventDetail, }; diff --git a/packages/base/src/util/dragAndDrop/handleDragOver.ts b/packages/base/src/util/dragAndDrop/handleDragOver.ts new file mode 100644 index 000000000000..ff36314cfafa --- /dev/null +++ b/packages/base/src/util/dragAndDrop/handleDragOver.ts @@ -0,0 +1,63 @@ +import type UI5Element from "../../UI5Element.js"; +import type MovePlacement from "../../types/MovePlacement.js"; +import type { DragAndDropSettings } from "./DragRegistry.js"; +import DragRegistry from "./DragRegistry.js"; + +type DragOverResult = { + targetReference: HTMLElement | null; + placement: any; +} + +type DragPosition = { + element: HTMLElement; + placements: MovePlacement[]; +}; + +/** + * Handles the dragover event. + */ +function handleDragOver(e: DragEvent, component: T, position: DragPosition, target: HTMLElement, settings: DragAndDropSettings = {}): DragOverResult { + const draggedElement = DragRegistry.getDraggedElement(); + const dragOverResult: DragOverResult = { + targetReference: null, + placement: null, + }; + + if (!draggedElement && !settings?.crossDnD) { + return dragOverResult; + } + + const placements = position.placements; + dragOverResult.targetReference = e.target as HTMLElement; + + const placementAccepted = placements.some(placement => { + const originalEvent = settings.originalEvent ? { originalEvent: e } : {}; + const beforeItemMovePrevented = !component.fireDecoratorEvent("move-over" as keyof T["eventDetails"], { + ...originalEvent, + source: { + element: draggedElement, + }, + destination: { + element: target, + placement, + }, + } as T["eventDetails"][keyof T["eventDetails"]]); + + if (beforeItemMovePrevented) { + e.preventDefault(); + dragOverResult.targetReference = position.element; + dragOverResult.placement = placement; + return true; + } + + return false; + }); + + if (!placementAccepted) { + dragOverResult.targetReference = null; + } + + return dragOverResult; +} + +export default handleDragOver; diff --git a/packages/base/src/util/dragAndDrop/handleDrop.ts b/packages/base/src/util/dragAndDrop/handleDrop.ts new file mode 100644 index 000000000000..b7bc8bdc8ba2 --- /dev/null +++ b/packages/base/src/util/dragAndDrop/handleDrop.ts @@ -0,0 +1,29 @@ +import type UI5Element from "../../UI5Element.js"; +import type MovePlacement from "../../types/MovePlacement.js"; +import type { DragAndDropSettings } from "./DragRegistry.js"; +import DragRegistry from "./DragRegistry.js"; + +function handleDrop(e: DragEvent, component: T, target: HTMLElement, placement: `${MovePlacement}`, settings: DragAndDropSettings = {}): void { + e.preventDefault(); + const draggedElement = DragRegistry.getDraggedElement(); + + if (!draggedElement && settings?.crossDnD) { + return; + } + + const originalEvent = settings.originalEvent ? { originalEvent: e } : {}; + component.fireDecoratorEvent("move" as keyof T["eventDetails"], { + ...originalEvent, + source: { + element: draggedElement, + }, + destination: { + element: target, + placement, + }, + } as T["eventDetails"][keyof T["eventDetails"]]); + + draggedElement?.focus(); +} + +export default handleDrop; diff --git a/packages/main/cypress/specs/TableDragAndDrop.cy.ts b/packages/main/cypress/specs/TableDragAndDrop.cy.ts new file mode 100644 index 000000000000..c9f75d608bbb --- /dev/null +++ b/packages/main/cypress/specs/TableDragAndDrop.cy.ts @@ -0,0 +1,233 @@ +import { html } from "lit"; + +import "../../src/Table.js"; +import "../../src/TableHeaderRow.js"; +import "../../src/TableCell.js"; +import "../../src/TableRow.js"; +import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; + +describe("API & Events", () => { + function dragTo(selectors: { source: string, destination: string }, position: MovePlacement, expectMove = true, onPrevented = false) { + const source = Cypress.$(selectors.source)[0]; + const destination = Cypress.$(selectors.destination)[0]; + const destinationRect = destination.getBoundingClientRect(); + + const dataTransfer = new DataTransfer(); + + cy.get(selectors.source) + .then(row => { + row.get(0).dispatchEvent(new DragEvent("dragstart", { + dataTransfer, + bubbles: true, + })); + }); + + let delta = 10; + if (position === MovePlacement.On) { + delta = destinationRect.height / 2; + } else if (position === MovePlacement.After) { + delta = destinationRect.height; + } + + cy.get("ui5-table") + .trigger("dragover", { + dataTransfer, + clientX: destinationRect.left, + clientY: destinationRect.top + delta, + }); + + cy.get("ui5-table") + .trigger("drop", { + dataTransfer, + }); + + cy.get(selectors.source) + .trigger("dragend"); + + cy.get("@moveOver") + .should("be.called") + .should("be.calledWithMatch", { + detail: { + source: { element: source }, + destination: { element: destination, placement: position }, + }, + }); + + if (onPrevented) { + position = MovePlacement.After; + } + + if (expectMove) { + cy.get("@move") + .should("be.called") + .should("be.calledWithMatch", { + detail: { + source: { element: source }, + destination: { element: destination, placement: position }, + }, + }); + } + } + + beforeEach(() => { + cy.viewport(1920, 1080); + cy.mount(html` + + + ColumnA + ColumnB + + ${Array.from({ length: 10 }).map((_, index) => html` + + Cell A + Cell B + + `)} + + Cell A + Cell B + + + `); + }); + + it("tests if draggable=true is set", () => { + cy.get("[ui5-table-row]") + .should("have.length", 11) + .each(($row, index) => { + if (index === 10) { + cy.wrap($row).should("not.have.attr", "draggable"); + } else { + cy.wrap($row).should("have.attr", "draggable", "true"); + } + }); + }); + + it("tests if events are fired and paramters are as expected", () => { + cy.get("[ui5-table]") + .then(table => { + table.get(0).addEventListener("move-over", e => e.preventDefault()); + table.get(0).addEventListener("move-over", cy.stub().as("moveOver")); + table.get(0).addEventListener("move", cy.stub().as("move")); + }); + + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='1']", + }, MovePlacement.Before); + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='1']", + }, MovePlacement.On); + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='1']", + }, MovePlacement.After); + }); + + it("tests if drop with Before placement does not occur when not preventing move-over for it", () => { + cy.get("ui5-table") + .then(table => { + table.get(0).addEventListener("move-over", (e: Event) => { + const evt = e as CustomEvent; // needed to satisfy TS + if (evt.detail.destination.placement === MovePlacement.Before) { + return; + } + e.preventDefault(); + }); + table.get(0).addEventListener("move-over", cy.stub().as("moveOver")); + table.get(0).addEventListener("move", cy.stub().as("move")); + }); + + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='5']", + }, MovePlacement.Before, false); + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='5']", + }, MovePlacement.On); + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='5']", + }, MovePlacement.After); + }); + + it("tests if drop with After placement does not occur when not preventing move-over for it", () => { + cy.get("ui5-table") + .then(table => { + table.get(0).addEventListener("move-over", (e: Event) => { + const evt = e as CustomEvent; // needed to satisfy TS + if (evt.detail.destination.placement === MovePlacement.After) { + return; + } + e.preventDefault(); + }); + table.get(0).addEventListener("move-over", cy.stub().as("moveOver")); + table.get(0).addEventListener("move", cy.stub().as("move")); + }); + + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='5']", + }, MovePlacement.Before); + + cy.get("@move") + .should("have.callCount", 1); + + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='5']", + }, MovePlacement.On); + + cy.get("@move") + .should("have.callCount", 2); + + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='5']", + }, MovePlacement.After, false); + + cy.get("@move") + .should("have.callCount", 2); + }); + + it("tests if drop with On placement does occur (because Before/After still applies) when not preventing move-over for it", () => { + cy.get("ui5-table") + .then(table => { + table.get(0).addEventListener("move-over", (e: Event) => { + const evt = e as CustomEvent; // needed to satisfy TS + if (evt.detail.destination.placement === MovePlacement.On) { + return; + } + e.preventDefault(); + }); + table.get(0).addEventListener("move-over", cy.stub().as("moveOver")); + table.get(0).addEventListener("move", cy.stub().as("move")); + }); + + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='5']", + }, MovePlacement.Before); + + cy.get("@move") + .should("have.callCount", 1); + + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='5']", + }, MovePlacement.On, true, true); + + cy.get("@move") + .should("have.callCount", 2); + + dragTo({ + source: "ui5-table-row[row-key='0']", + destination: "ui5-table-row[row-key='5']", + }, MovePlacement.After); + + cy.get("@move") + .should("have.callCount", 3); + }); +}); diff --git a/packages/main/src/List.ts b/packages/main/src/List.ts index 054d1b672700..745f6c98322a 100644 --- a/packages/main/src/List.ts +++ b/packages/main/src/List.ts @@ -21,7 +21,10 @@ import { isDown, isUp, } from "@ui5/webcomponents-base/dist/Keys.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 Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js"; +import DragRegistry, { type MoveEventDetail as ListMoveEventDetail } from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; import { findClosestPosition, findClosestPositionsByKey } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js"; import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js"; import { @@ -37,8 +40,6 @@ import getEffectiveScrollbarStyle from "@ui5/webcomponents-base/dist/util/getEff import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import debounce from "@ui5/webcomponents-base/dist/util/debounce.js"; import isElementInView from "@ui5/webcomponents-base/dist/util/isElementInView.js"; -import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.js"; -import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; import ListSelectionMode from "./types/ListSelectionMode.js"; import ListGrowingMode from "./types/ListGrowingMode.js"; import type ListAccessibleRole from "./types/ListAccessibleRole.js"; @@ -91,17 +92,6 @@ type ListItemDeleteEventDetail = { item: ListItemBase, } -type ListMoveEventDetail = { - originalEvent: Event, - source: { - element: HTMLElement, - }, - destination: { - element: HTMLElement, - placement: `${MovePlacement}`, - } -} - // ListItem-based events type ListItemCloseEventDetail = { item: ListItemBase, @@ -1128,9 +1118,7 @@ class List extends UI5Element { } _ondragover(e: DragEvent) { - const draggedElement = DragRegistry.getDraggedElement(); - - if (!(e.target instanceof HTMLElement) || !draggedElement) { + if (!(e.target instanceof HTMLElement)) { return; } @@ -1145,56 +1133,19 @@ class List extends UI5Element { 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", { - originalEvent: e, - 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; - } + const { targetReference, placement } = handleDragOver(e, this, closestPosition, closestPosition.element, { originalEvent: true }); + this.dropIndicatorDOM!.targetReference = targetReference; + this.dropIndicatorDOM!.placement = placement; } _ondrop(e: DragEvent) { - e.preventDefault(); - const draggedElement = DragRegistry.getDraggedElement()!; - - this.fireDecoratorEvent("move", { - originalEvent: e, - source: { - element: draggedElement, - }, - destination: { - element: this.dropIndicatorDOM!.targetReference!, - placement: this.dropIndicatorDOM!.placement, - }, - }); + if (!this.dropIndicatorDOM?.targetReference || !this.dropIndicatorDOM?.placement) { + e.preventDefault(); + return; + } - this.dropIndicatorDOM!.targetReference = null; - draggedElement.focus(); + handleDrop(e, this, this.dropIndicatorDOM.targetReference, this.dropIndicatorDOM.placement, { originalEvent: true }); + this.dropIndicatorDOM.targetReference = null; } isForwardElement(element: HTMLElement) { diff --git a/packages/main/src/TabContainer.ts b/packages/main/src/TabContainer.ts index 0e77d4409ac9..4d708f6affa8 100644 --- a/packages/main/src/TabContainer.ts +++ b/packages/main/src/TabContainer.ts @@ -32,6 +32,8 @@ import arraysAreEqual from "@ui5/webcomponents-base/dist/util/arraysAreEqual.js" import { findClosestPosition, findClosestPositionsByKey } from "@ui5/webcomponents-base/dist/util/dragAndDrop/findClosestPosition.js"; import Orientation from "@ui5/webcomponents-base/dist/types/Orientation.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 type { SetDraggedElementFunction } from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; import longDragOverHandler from "@ui5/webcomponents-base/dist/util/dragAndDrop/longDragOverHandler.js"; import MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; @@ -525,36 +527,17 @@ class TabContainer extends UI5Element { e.preventDefault(); } else if (closestPosition) { const dropTarget = (closestPosition.element as TabInStrip).realTabReference; - let placements = closestPosition.placements; if (dropTarget === draggedElement) { - placements = placements.filter(placement => placement !== MovePlacement.On); + closestPosition.placements = closestPosition.placements.filter(placement => placement !== MovePlacement.On); } - const acceptedPlacement = placements.find(placement => { - const dragOverPrevented = !this.fireDecoratorEvent("move-over", { - source: { - element: draggedElement!, - }, - destination: { - element: dropTarget, - placement, - }, - }); - - if (dragOverPrevented) { - e.preventDefault(); - this.dropIndicatorDOM!.targetReference = closestPosition.element; - this.dropIndicatorDOM!.placement = placement; - return true; - } - - return false; - }); - - if (acceptedPlacement === MovePlacement.On && (closestPosition.element as TabInStrip).realTabReference.items.length) { + const { targetReference, placement } = handleDragOver(e, this, closestPosition, dropTarget); + this.dropIndicatorDOM!.targetReference = targetReference; + this.dropIndicatorDOM!.placement = placement; + if (placement === MovePlacement.On && (closestPosition.element as TabInStrip).realTabReference.items.length) { popoverTarget = closestPosition.element; - } else if (!acceptedPlacement) { + } else if (!placement) { this.dropIndicatorDOM!.targetReference = null; } } @@ -571,21 +554,8 @@ class TabContainer extends UI5Element { return; } - e.preventDefault(); - const draggedElement = DragRegistry.getDraggedElement()!; - - this.fireDecoratorEvent("move", { - source: { - element: draggedElement, - }, - destination: { - element: (this.dropIndicatorDOM!.targetReference as TabInStrip).realTabReference, - placement: this.dropIndicatorDOM!.placement, - }, - }); - + handleDrop(e, this, (this.dropIndicatorDOM!.targetReference as TabInStrip).realTabReference, this.dropIndicatorDOM!.placement); this.dropIndicatorDOM!.targetReference = null; - draggedElement.focus(); } _moveHeaderItem(tab: Tab, e: KeyboardEvent) { diff --git a/packages/main/src/Table.hbs b/packages/main/src/Table.hbs index 4688ba22e56c..e0bca47d9b0a 100644 --- a/packages/main/src/Table.hbs +++ b/packages/main/src/Table.hbs @@ -30,6 +30,11 @@ {{/if}} + + {{> tableEndRow}} diff --git a/packages/main/src/Table.ts b/packages/main/src/Table.ts index e291132cd451..7287d96682d3 100644 --- a/packages/main/src/Table.ts +++ b/packages/main/src/Table.ts @@ -10,6 +10,7 @@ import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delega import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; +import type { MoveEventDetail as TableMoveEventDetail } from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js"; import TableTemplate from "./generated/templates/TableTemplate.lit.js"; import TableStyles from "./generated/themes/Table.css.js"; import TableRow from "./TableRow.js"; @@ -19,12 +20,14 @@ import TableExtension from "./TableExtension.js"; import type TableSelection from "./TableSelection.js"; import TableOverflowMode from "./types/TableOverflowMode.js"; import TableNavigation from "./TableNavigation.js"; +import DropIndicator from "./DropIndicator.js"; import { TABLE_NO_DATA, } from "./generated/i18n/i18n-defaults.js"; import BusyIndicator from "./BusyIndicator.js"; import TableCell from "./TableCell.js"; import { findVerticalScrollContainer, scrollElementIntoView, isFeature } from "./TableUtils.js"; +import TableDragAndDrop from "./TableDragAndDrop.js"; import type TableVirtualizer from "./TableVirtualizer.js"; /** @@ -165,6 +168,7 @@ type TableRowClickEventDetail = { TableHeaderRow, TableCell, TableRow, + DropIndicator, ], }) @@ -178,9 +182,48 @@ type TableRowClickEventDetail = { bubbles: true, }) +/** + * Fired when a movable item is moved over a potential drop target during a dragging operation. + * + * If the new position is valid, prevent the default action of the event using `preventDefault()`. + * + * **Note:** If the dragging operation is a cross-browser operation or files are moved to a potential drop target, + * the `source` parameter will be `null`. + * + * @param {Event} originalEvent The original `dragover` event + * @param {object} source The source object + * @param {object} destination The destination object + * @public + */ +@event("move-over", { + cancelable: true, + bubbles: true, +}) + +/** + * Fired when a movable list item is dropped onto a drop target. + * + * **Notes:** + * + * The `move` event is fired only if there was a preceding `move-over` with prevented default action. + * + * If the dragging operation is a cross-browser operation or files are moved to a potential drop target, + * the `source` parameter will be `null`. + * + * @param {Event} originalEvent The original `drop` event + * @param {object} source The source object + * @param {object} destination The destination object + * @public + */ +@event("move", { + bubbles: true, +}) + class Table extends UI5Element { eventDetails!: { "row-click": TableRowClickEventDetail; + "move-over": TableMoveEventDetail; + "move": TableMoveEventDetail; } /** * Defines the rows of the component. @@ -299,10 +342,11 @@ class Table extends UI5Element { @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; - _events = ["keydown", "keyup", "click", "focusin", "focusout"]; + _events = ["keydown", "keyup", "click", "focusin", "focusout", "dragenter", "dragleave", "dragover", "drop"]; _onEventBound: (e: Event) => void; _onResizeBound: ResizeObserverCallback; _tableNavigation?: TableNavigation; + _tableDragAndDrop?: TableDragAndDrop; _poppedIn: Array<{col: TableHeaderCell, width: float}>; _containerWidth: number; @@ -321,11 +365,13 @@ class Table extends UI5Element { this._events.forEach(eventType => this.addEventListener(eventType, this._onEventBound)); this.features.forEach(feature => feature.onTableActivate(this)); this._tableNavigation = new TableNavigation(this); + this._tableDragAndDrop = new TableDragAndDrop(this); } onExitDOM() { this._tableNavigation = undefined; - this._events.forEach(eventType => this.addEventListener(eventType, this._onEventBound)); + this._tableDragAndDrop = undefined; + this._events.forEach(eventType => this.removeEventListener(eventType, this._onEventBound)); if (this.overflowMode === TableOverflowMode.Popin) { ResizeHandler.deregister(this, this._onResizeBound); } @@ -359,7 +405,7 @@ class Table extends UI5Element { _onEvent(e: Event) { const composedPath = e.composedPath(); const eventOrigin = composedPath[0] as HTMLElement; - const elements = [this._tableNavigation, ...composedPath, ...this.features]; + const elements = [this._tableNavigation, this._tableDragAndDrop, ...composedPath, ...this.features]; elements.forEach(element => { if (element instanceof TableExtension || (element instanceof HTMLElement && element.localName.includes("ui5-table"))) { const eventHandlerName = `_on${e.type}` as keyof typeof element; @@ -580,6 +626,10 @@ class Table extends UI5Element { get isTable() { return true; } + + get dropIndicatorDOM(): DropIndicator | null { + return this.shadowRoot!.querySelector("[ui5-drop-indicator]"); + } } Table.define(); @@ -590,4 +640,5 @@ export type { ITableFeature, ITableGrowing, TableRowClickEventDetail, + TableMoveEventDetail as TableTableMoveEventDetail, }; diff --git a/packages/main/src/TableDragAndDrop.ts b/packages/main/src/TableDragAndDrop.ts new file mode 100644 index 000000000000..63bcb6fd2b0d --- /dev/null +++ b/packages/main/src/TableDragAndDrop.ts @@ -0,0 +1,60 @@ +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 handleDragOver from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDragOver.js"; +import handleDrop from "@ui5/webcomponents-base/dist/util/dragAndDrop/handleDrop.js"; + +import type Table from "./Table.js"; +import TableExtension from "./TableExtension.js"; + +export default class TableDragAndDrop extends TableExtension { + _table: Table; + + constructor(table: Table) { + super(); + this._table = table; + DragRegistry.subscribe(this._table); // TODO: Where unsubscribe? + } + + _ondragenter(e: DragEvent) { + e.preventDefault(); + } + + _ondragleave(e: DragEvent) { + if (e.relatedTarget instanceof Node && this._table.shadowRoot!.contains(e.relatedTarget)) { + return; + } + + this._table.dropIndicatorDOM!.targetReference = null; + } + + _ondragover(e: DragEvent) { + if (!(e.target instanceof HTMLElement)) { + return; + } + + const closestPosition = findClosestPosition( + this._table.rows, + e.clientY, + Orientation.Vertical, + ); + + if (!closestPosition) { + this._table.dropIndicatorDOM!.targetReference = null; + return; + } + + const { targetReference, placement } = handleDragOver(e, this._table, closestPosition, closestPosition.element, { crossDnD: true, originalEvent: true }); + this._table.dropIndicatorDOM!.targetReference = targetReference; + this._table.dropIndicatorDOM!.placement = placement; + } + + _ondrop(e: DragEvent) { + if (!this._table.dropIndicatorDOM?.targetReference || !this._table.dropIndicatorDOM?.placement) { + return; + } + + handleDrop(e, this._table, this._table.dropIndicatorDOM.targetReference, this._table.dropIndicatorDOM.placement); + this._table.dropIndicatorDOM.targetReference = null; + } +} diff --git a/packages/main/src/TableRow.ts b/packages/main/src/TableRow.ts index cfafe418a377..1b06d01d2a36 100644 --- a/packages/main/src/TableRow.ts +++ b/packages/main/src/TableRow.ts @@ -87,6 +87,15 @@ class TableRow extends TableRowBase { @property({ type: Boolean }) navigated = false; + /** + * Defines whether the row is movable. + * + * @default false + * @public + */ + @property({ type: Boolean }) + movable = false; + @property({ type: Boolean, noAttribute: true }) _renderNavigated = false; @@ -101,6 +110,11 @@ class TableRow extends TableRowBase { } else { this.removeAttribute("aria-current"); } + if (this.movable) { + this.setAttribute("draggable", "true"); + } else { + this.removeAttribute("draggable"); + } } async focus(focusOptions?: FocusOptions | undefined): Promise { diff --git a/packages/main/src/Tree.ts b/packages/main/src/Tree.ts index 8d547ab03b8a..ec1c9165ba21 100644 --- a/packages/main/src/Tree.ts +++ b/packages/main/src/Tree.ts @@ -3,6 +3,8 @@ 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 MovePlacement from "@ui5/webcomponents-base/dist/types/MovePlacement.js"; @@ -364,58 +366,23 @@ class Tree extends UI5Element { return; } - let placements = closestPosition.placements; - closestPosition.element = (closestPosition.element.getRootNode()).host; - if (draggedElement.contains(closestPosition.element)) { return; } - if (closestPosition.element === draggedElement) { - placements = placements.filter(placement => placement !== MovePlacement.On); + closestPosition.placements = closestPosition.placements.filter(placement => placement !== MovePlacement.On); } - const placementAccepted = placements.some(placement => { - const closestElement = closestPosition.element; - const beforeItemMovePrevented = !this.fireDecoratorEvent("move-over", { - source: { - element: draggedElement, - }, - destination: { - element: closestElement, - placement, - }, - }); - - if (beforeItemMovePrevented) { - e.preventDefault(); - this.dropIndicatorDOM!.targetReference = closestElement; - this.dropIndicatorDOM!.placement = placement; - return true; - } - - return false; - }); - - if (!placementAccepted) { - this.dropIndicatorDOM!.targetReference = null; - } + const { targetReference, placement } = handleDragOver(e, this, closestPosition, closestPosition.element); + this.dropIndicatorDOM!.targetReference = targetReference; + this.dropIndicatorDOM!.placement = placement; } _ondrop(e: DragEvent) { - e.preventDefault(); - - const draggedElement = DragRegistry.getDraggedElement()!; - this.fireDecoratorEvent("move", { - source: { - element: draggedElement, - }, - destination: { - element: this.dropIndicatorDOM!.targetReference!, - placement: this.dropIndicatorDOM!.placement, - }, - }); - draggedElement.focus(); - this.dropIndicatorDOM!.targetReference = null; + if (!this.dropIndicatorDOM?.targetReference || !this.dropIndicatorDOM?.placement) { + return; + } + handleDrop(e, this, this.dropIndicatorDOM.targetReference, this.dropIndicatorDOM.placement); + this.dropIndicatorDOM.targetReference = null; } _onListItemStepIn(e: CustomEvent) { diff --git a/packages/main/test/pages/TableDragAndDrop.html b/packages/main/test/pages/TableDragAndDrop.html new file mode 100644 index 000000000000..e0f6842e7997 --- /dev/null +++ b/packages/main/test/pages/TableDragAndDrop.html @@ -0,0 +1,138 @@ + + + + + + + Table (in development) + + + + + + + + + + + Item 1 + Item 2 + Item 3 + Item 4 + Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Column A + Column B + Column C + + + Test 1 + Test 2 + Test 3 + + + Test 4 + Test 5 + Test 6 + + + Test 7 + Test 8 + Test 9 + + + + + + diff --git a/packages/website/docs/_components_pages/main/Table/Table.mdx b/packages/website/docs/_components_pages/main/Table/Table.mdx index fd8b6379471f..bb97af299382 100644 --- a/packages/website/docs/_components_pages/main/Table/Table.mdx +++ b/packages/website/docs/_components_pages/main/Table/Table.mdx @@ -10,6 +10,7 @@ import StickyHeader from "../../../_samples/main/Table/StickyHeader/StickyHeader import StickyHeaderContainer from "../../../_samples/main/Table/StickyHeaderContainer/StickyHeaderContainer.md"; import NoDataSlot from "../../../_samples/main/Table/NoDataSlot/NoDataSlot.md"; import Interactive from "../../../_samples/main/Table/Interactive/Interactive.md"; +import DragAndDrop from "../../../_samples/main/Table/DragAndDrop/DragAndDrop.md"; <%COMPONENT_OVERVIEW%> @@ -55,4 +56,11 @@ You can provide an illustration or a custom text, that is shown when the table h Create an interactive table by marking `ui5-table-row` components as `interactive`. Pressing on an interactive row will fire the `row-click` event. - \ No newline at end of file + + +### Drag and Drop + +Enable Drag and Drop by using the `move-over` and `move` event in combination with the `movable` property on the +`ui5-table-row`. + + \ No newline at end of file diff --git a/packages/website/docs/_components_pages/main/Table/TableRow.mdx b/packages/website/docs/_components_pages/main/Table/TableRow.mdx index 42244b75c62b..9721808820c8 100644 --- a/packages/website/docs/_components_pages/main/Table/TableRow.mdx +++ b/packages/website/docs/_components_pages/main/Table/TableRow.mdx @@ -4,6 +4,7 @@ sidebar_class_name: newComponentBadge expComponentBadge --- import Interactive from "../../../_samples/main/Table/Interactive/Interactive.md"; +import DragAndDrop from "../../../_samples/main/Table/DragAndDrop/DragAndDrop.md"; <%COMPONENT_OVERVIEW%> @@ -14,4 +15,10 @@ import Interactive from "../../../_samples/main/Table/Interactive/Interactive.md Create an interactive table by marking `ui5-table-row` components as `interactive`. Pressing on an interactive row will fire the `row-click` event. - \ No newline at end of file + + +## Movable Rows + +Adding the `movable` property enables the `ui5-table-row` for drag and drop operations. + + \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Table/DragAndDrop/DragAndDrop.md b/packages/website/docs/_samples/main/Table/DragAndDrop/DragAndDrop.md new file mode 100644 index 000000000000..17798ecc59ab --- /dev/null +++ b/packages/website/docs/_samples/main/Table/DragAndDrop/DragAndDrop.md @@ -0,0 +1,4 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; + + diff --git a/packages/website/docs/_samples/main/Table/DragAndDrop/main.js b/packages/website/docs/_samples/main/Table/DragAndDrop/main.js new file mode 100644 index 000000000000..66a8717d7913 --- /dev/null +++ b/packages/website/docs/_samples/main/Table/DragAndDrop/main.js @@ -0,0 +1,45 @@ +import "@ui5/webcomponents/dist/Table.js"; +import "@ui5/webcomponents/dist/TableHeaderRow.js"; +import "@ui5/webcomponents/dist/TableHeaderCell.js"; +import "@ui5/webcomponents/dist/Label.js"; + +function tableMoveOver(e) { + const { source, destination } = e.detail; + + const sourceIndex = table.rows.indexOf(source.element); + const destinationIndex = table.rows.indexOf(destination.element); + + if (sourceIndex === -1 || destinationIndex === -1) { + return; + } + + if (source.element.hasAttribute("ui5-table-row") && destination.element.hasAttribute("ui5-table-row") && destination.placement !== "On") { + e.preventDefault(); + } +} + +function tableMove(e) { + const { source, destination } = e.detail; + reorderRow(source.element, destination.element, destination.placement); +} + +function reorderRow(source, destination, placement) { + if (!table) { + return; + } + + switch (placement) { + case "Before": + destination.insertAdjacentElement("beforebegin", source); + break; + case "After": + destination.insertAdjacentElement("afterend", source); + break; + default: + break; + } +} + +const table = document.getElementById("table"); +table.addEventListener('move-over', tableMoveOver); +table.addEventListener('move', tableMove); \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Table/DragAndDrop/sample.html b/packages/website/docs/_samples/main/Table/DragAndDrop/sample.html new file mode 100644 index 000000000000..f197766b00f6 --- /dev/null +++ b/packages/website/docs/_samples/main/Table/DragAndDrop/sample.html @@ -0,0 +1,50 @@ + + + + + + + + Sample + + + +
+ + + + Product + Supplier + Dimensions + Weight + Price + + + Notebook Basic 15
HT-1000
+ Very Best Screens + 30 x 18 x 3 cm + 4.2 KG + 956 EUR +
+ + Notebook Basic 17
HT-1001
+ Smartcards + 29 x 17 x 3.1 cm + 4.5 KG + 1249 EUR +
+ + Notebook Basic 18
HT-1002
+ Technocom + 32 x 21 x 4 cm + 3.7 KG + 29 EUR +
+
+ +
+ + + + +