diff --git a/packages/core/src/canvas/index.ts b/packages/core/src/canvas/index.ts index c3c84fa1fa..d4f3d841c1 100644 --- a/packages/core/src/canvas/index.ts +++ b/packages/core/src/canvas/index.ts @@ -499,7 +499,7 @@ export default class CanvasModule extends Module { * @return {Object} * @private */ - getMouseRelativeCanvas(ev: MouseEvent, opts: any) { + getMouseRelativeCanvas(ev: MouseEvent | { clientX: number; clientY: number }, opts: any) { const zoom = this.getZoomDecimal(); const { top = 0, left = 0 } = this.getCanvasView().getPosition(opts) ?? {}; diff --git a/packages/core/src/utils/sorter/CanvasComponentNode.ts b/packages/core/src/utils/sorter/CanvasComponentNode.ts index 087d302e74..2495e75f24 100644 --- a/packages/core/src/utils/sorter/CanvasComponentNode.ts +++ b/packages/core/src/utils/sorter/CanvasComponentNode.ts @@ -1,6 +1,11 @@ import { BaseComponentNode } from './BaseComponentNode'; export default class CanvasComponentNode extends BaseComponentNode { + protected _dropAreaConfig = { + ratio: 0.8, + minUndroppableDimension: 1, // In px + maxUndroppableDimension: 15, // In px + }; /** * Get the associated view of this component. * @returns The view associated with the component, or undefined if none. diff --git a/packages/core/src/utils/sorter/Dimension.ts b/packages/core/src/utils/sorter/Dimension.ts index 0fbfcf9d35..dc8b7e2845 100644 --- a/packages/core/src/utils/sorter/Dimension.ts +++ b/packages/core/src/utils/sorter/Dimension.ts @@ -1,5 +1,5 @@ import CanvasModule from '../../canvas'; -import { Placement } from './types'; +import { Placement, DroppableZoneConfig } from './types'; /** * A class representing dimensions of an element, including position, size, offsets, and other metadata. @@ -113,4 +113,46 @@ export default class Dimension { dir: this.dir, }); } + + public getDropArea(config: DroppableZoneConfig): Dimension { + const dropZone = this.clone(); + // Adjust width + const { newSize: newWidth, newPosition: newLeft } = this.adjustDropDimension(this.width, this.left, config); + dropZone.width = newWidth; + dropZone.left = newLeft; + + // Adjust height + const { newSize: newHeight, newPosition: newTop } = this.adjustDropDimension(this.height, this.top, config); + dropZone.height = newHeight; + dropZone.top = newTop; + + return dropZone; + } + + private adjustDropDimension( + size: number, + position: number, + config: DroppableZoneConfig, + ): { newSize: number; newPosition: number } { + const { ratio, minUndroppableDimension: minUnDroppableDimension, maxUndroppableDimension } = config; + + let undroppableDimension = (size * (1 - ratio)) / 2; + undroppableDimension = Math.max(undroppableDimension, minUnDroppableDimension); + undroppableDimension = Math.min(undroppableDimension, maxUndroppableDimension); + const newSize = size - undroppableDimension * 2; + const newPosition = position + undroppableDimension; + + return { newSize, newPosition }; + } + + /** + * Checks if the given coordinates are within the bounds of this dimension instance. + * + * @param {number} x - The X coordinate to check. + * @param {number} y - The Y coordinate to check. + * @returns {boolean} - True if the coordinates are within bounds, otherwise false. + */ + public isWithinBounds(x: number, y: number): boolean { + return x >= this.left && x <= this.left + this.width && y >= this.top && y <= this.top + this.height; + } } diff --git a/packages/core/src/utils/sorter/DropLocationDeterminer.ts b/packages/core/src/utils/sorter/DropLocationDeterminer.ts index b8466a6af0..7365737043 100644 --- a/packages/core/src/utils/sorter/DropLocationDeterminer.ts +++ b/packages/core/src/utils/sorter/DropLocationDeterminer.ts @@ -116,8 +116,10 @@ export class DropLocationDeterminer> ext const { targetNode: lastTargetNode } = this.lastMoveData; this.eventHandlers.onMouseMove?.(mouseEvent); - const { mouseXRelativeToContainer: mouseX, mouseYRelativeToContainer: mouseY } = - this.getMousePositionRelativeToContainer(mouseEvent); + const { mouseXRelative: mouseX, mouseYRelative: mouseY } = this.getMousePositionRelativeToContainer( + mouseEvent.clientX, + mouseEvent.clientY, + ); const targetNode = this.getTargetNode(mouseEvent); const targetChanged = !targetNode?.equals(lastTargetNode); if (targetChanged) { @@ -257,6 +259,10 @@ export class DropLocationDeterminer> ext */ private getTargetNode(mouseEvent: MouseEvent): NodeType | undefined { this.cacheContainerPosition(this.containerContext.container); + const { mouseXRelative, mouseYRelative } = this.getMousePositionRelativeToContainer( + mouseEvent.clientX, + mouseEvent.clientY, + ); // Get the element under the mouse const mouseTargetEl = this.getMouseTargetElement(mouseEvent); @@ -268,10 +274,10 @@ export class DropLocationDeterminer> ext let hoveredNode = this.getOrCreateHoveredNode(hoveredModel); // Get the drop position index based on the mouse position - const { index } = this.getDropPosition(hoveredNode, mouseEvent.clientX, mouseEvent.clientY); + const { index } = this.getDropPosition(hoveredNode, mouseXRelative, mouseYRelative); // Determine the valid target node (or its valid parent) - let targetNode = this.getValidParent(hoveredNode, index, mouseEvent.clientX, mouseEvent.clientY); + let targetNode = this.getValidParent(hoveredNode, index, mouseXRelative, mouseYRelative); return this.getOrReuseTargetNode(targetNode); } @@ -389,21 +395,44 @@ export class DropLocationDeterminer> ext private getValidParent(targetNode: NodeType, index: number, mouseX: number, mouseY: number): NodeType | undefined { if (!targetNode) return; - const positionNotChanged = targetNode.equals(this.lastMoveData.targetNode) && index === this.lastMoveData.index; - if (positionNotChanged) return targetNode; + + const lastTargetNode = this.lastMoveData.targetNode; + const targetNotChanged = targetNode.equals(lastTargetNode); + targetNode.nodeDimensions = targetNotChanged ? lastTargetNode.nodeDimensions! : this.getDim(targetNode.element!); + if (!targetNode.isWithinDropBounds(mouseX, mouseY)) { + return this.handleParentTraversal(targetNode, mouseX, mouseY); + } + + const positionNotChanged = targetNotChanged && index === this.lastMoveData.index; + if (positionNotChanged) return lastTargetNode; const canMove = this.sourceNodes.some((node) => targetNode.canMove(node, index)); this.triggerDragValidation(canMove, targetNode); if (canMove) return targetNode; + return this.handleParentTraversal(targetNode, mouseX, mouseY); + } + + private handleParentTraversal(targetNode: NodeType, mouseX: number, mouseY: number): NodeType | undefined { const parent = targetNode.getParent() as NodeType; if (!parent) return; + const indexInParent = this.getIndexInParent(parent, targetNode, targetNode.nodeDimensions!, mouseX, mouseY); + return this.getValidParent(parent, indexInParent, mouseX, mouseY); + } + + private getIndexInParent( + parent: NodeType, + targetNode: NodeType, + nodeDimensions: Dimension, + mouseX: number, + mouseY: number, + ) { let indexInParent = parent?.indexOfChild(targetNode); - const nodeDimensions = this.getDim(targetNode.element!); nodeDimensions.dir = this.getDirection(targetNode.element!, parent.element!); + indexInParent = indexInParent + (nodeDimensions.determinePlacement(mouseX, mouseY) == 'after' ? 1 : 0); - return this.getValidParent(parent, indexInParent, mouseX, mouseY); + return indexInParent; } private triggerDragValidation(canMove: boolean, targetNode: NodeType) { @@ -488,27 +517,26 @@ export class DropLocationDeterminer> ext /** * Gets the mouse position relative to the container, adjusting for scroll and canvas relative options. * - * @param {MouseEvent} mouseEvent - The current mouse event. * @return {{ mouseXRelativeToContainer: number, mouseYRelativeToContainer: number }} - The mouse X and Y positions relative to the container. * @private */ - private getMousePositionRelativeToContainer(mouseEvent: MouseEvent): { - mouseXRelativeToContainer: number; - mouseYRelativeToContainer: number; + private getMousePositionRelativeToContainer( + mouseX: number, + mouseY: number, + ): { + mouseXRelative: number; + mouseYRelative: number; } { - const { em } = this; - let mouseYRelativeToContainer = - mouseEvent.pageY - this.containerOffset.top + this.containerContext.container.scrollTop; - let mouseXRelativeToContainer = - mouseEvent.pageX - this.containerOffset.left + this.containerContext.container.scrollLeft; - - if (this.positionOptions.canvasRelative && !!em) { - const mousePos = em.Canvas.getMouseRelativeCanvas(mouseEvent, { noScroll: 1 }); - mouseXRelativeToContainer = mousePos.x; - mouseYRelativeToContainer = mousePos.y; + let mouseYRelative = mouseY - this.containerOffset.top + this.containerContext.container.scrollTop; + let mouseXRelative = mouseX - this.containerOffset.left + this.containerContext.container.scrollLeft; + + if (this.positionOptions.canvasRelative) { + const mousePos = this.em.Canvas.getMouseRelativeCanvas({ clientX: mouseX, clientY: mouseY }, { noScroll: 1 }); + mouseXRelative = mousePos.x; + mouseYRelative = mousePos.y; } - return { mouseXRelativeToContainer, mouseYRelativeToContainer }; + return { mouseXRelative, mouseYRelative }; } /** diff --git a/packages/core/src/utils/sorter/PlaceholderClass.ts b/packages/core/src/utils/sorter/PlaceholderClass.ts index b87f38ce67..8bb8c70b6d 100644 --- a/packages/core/src/utils/sorter/PlaceholderClass.ts +++ b/packages/core/src/utils/sorter/PlaceholderClass.ts @@ -95,7 +95,7 @@ export class PlaceholderClass extends View { const borderWidth = borderLeftWidth + borderRightWidth; top = elTop + paddingTop + borderTopWidth; left = elLeft + paddingLeft + borderLeftWidth; - width = elWidth - paddingLeft * 2 - borderWidth + 'px'; + width = Math.max(elWidth - paddingLeft * 2 - borderWidth, 1) + 'px'; height = 'auto'; } else { if (!dir) { diff --git a/packages/core/src/utils/sorter/SortableTreeNode.ts b/packages/core/src/utils/sorter/SortableTreeNode.ts index ee18b619bd..a80c6587aa 100644 --- a/packages/core/src/utils/sorter/SortableTreeNode.ts +++ b/packages/core/src/utils/sorter/SortableTreeNode.ts @@ -1,5 +1,6 @@ import { View } from '../../common'; import Dimension from './Dimension'; +import { DroppableZoneConfig } from './types'; import { DragSource } from './types'; /** @@ -10,6 +11,11 @@ import { DragSource } from './types'; export abstract class SortableTreeNode { protected _model: T; protected _dragSource: DragSource; + protected _dropAreaConfig: DroppableZoneConfig = { + ratio: 1, + minUndroppableDimension: 0, // In px + maxUndroppableDimension: 0, // In px + }; /** The dimensions of the node. */ public nodeDimensions?: Dimension; /** The dimensions of the child elements within the target node. */ @@ -92,6 +98,23 @@ export abstract class SortableTreeNode { return this._dragSource; } + get dropArea(): Dimension | undefined { + // If no parent, there's no reason to reduce the drop zone + if (!this.getParent()) return this.nodeDimensions?.clone(); + return this.nodeDimensions?.getDropArea(this._dropAreaConfig); + } + + /** + * Checks if the given coordinates are within the bounds of this node. + * + * @param {number} x - The X coordinate to check. + * @param {number} y - The Y coordinate to check. + * @returns {boolean} - True if the coordinates are within bounds, otherwise false. + */ + public isWithinDropBounds(x: number, y: number): boolean { + return !!this.dropArea && this.dropArea.isWithinBounds(x, y); + } + equals(node?: SortableTreeNode): node is SortableTreeNode { return !!node?._model && this._model === node._model; } diff --git a/packages/core/src/utils/sorter/types.ts b/packages/core/src/utils/sorter/types.ts index 04d33baae9..fc7e7d38db 100644 --- a/packages/core/src/utils/sorter/types.ts +++ b/packages/core/src/utils/sorter/types.ts @@ -26,6 +26,12 @@ export type DragSource = DraggableContent & { export type Placement = 'inside' | 'before' | 'after'; +export type DroppableZoneConfig = { + ratio: number; + minUndroppableDimension: number; // In px + maxUndroppableDimension: number; // In px +}; + export enum DragDirection { Vertical = 'Vertical', Horizontal = 'Horizontal',