Browse Source

Add dropzone (#6200)

* Fix issue with very small components

* Add drop area calculation to tree nodes

* Add helper methods to dimension class

* Add drop zone to the sorter

* Change minimum undroppable dimensions
pull/6204/head
mohamed yahia 2 years ago
committed by GitHub
parent
commit
5bad337f56
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      packages/core/src/canvas/index.ts
  2. 5
      packages/core/src/utils/sorter/CanvasComponentNode.ts
  3. 44
      packages/core/src/utils/sorter/Dimension.ts
  4. 74
      packages/core/src/utils/sorter/DropLocationDeterminer.ts
  5. 2
      packages/core/src/utils/sorter/PlaceholderClass.ts
  6. 23
      packages/core/src/utils/sorter/SortableTreeNode.ts
  7. 6
      packages/core/src/utils/sorter/types.ts

2
packages/core/src/canvas/index.ts

@ -499,7 +499,7 @@ export default class CanvasModule extends Module<CanvasConfig> {
* @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) ?? {};

5
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.

44
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;
}
}

74
packages/core/src/utils/sorter/DropLocationDeterminer.ts

@ -116,8 +116,10 @@ export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> 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<T, NodeType extends SortableTreeNode<T>> 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<T, NodeType extends SortableTreeNode<T>> 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<T, NodeType extends SortableTreeNode<T>> 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<T, NodeType extends SortableTreeNode<T>> 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 };
}
/**

2
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) {

23
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<T> {
protected _model: T;
protected _dragSource: DragSource<T>;
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<T> {
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<T>): node is SortableTreeNode<T> {
return !!node?._model && this._model === node._model;
}

6
packages/core/src/utils/sorter/types.ts

@ -26,6 +26,12 @@ export type DragSource<T> = 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',

Loading…
Cancel
Save