Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dropzone #6200

Merged
merged 5 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/canvas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) ?? {};

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/utils/sorter/CanvasComponentNode.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
44 changes: 43 additions & 1 deletion packages/core/src/utils/sorter/Dimension.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 changes: 51 additions & 23 deletions packages/core/src/utils/sorter/DropLocationDeterminer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 };
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/sorter/PlaceholderClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/utils/sorter/SortableTreeNode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { View } from '../../common';
import Dimension from './Dimension';
import { DroppableZoneConfig } from './types';
import { DragSource } from './types';

/**
Expand All @@ -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. */
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/utils/sorter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down