diff --git a/packages/abstract/src/manager/dragOperation.ts b/packages/abstract/src/manager/dragOperation.ts index 894666bc..96153d4f 100644 --- a/packages/abstract/src/manager/dragOperation.ts +++ b/packages/abstract/src/manager/dragOperation.ts @@ -10,6 +10,7 @@ import type {DragDropMonitor} from './manager'; export enum Status { Idle = 'idle', + Initializing = 'initializing', Dragging = 'dragging', Dropping = 'dropped', } @@ -99,6 +100,8 @@ export function DragOperationManager< targetIdentifier.value = identifier; }, start(coordinates: Coordinates) { + status.value = Status.Initializing; + batch(() => { status.value = Status.Dragging; position.reset(coordinates); diff --git a/packages/abstract/src/manager/registry.ts b/packages/abstract/src/manager/registry.ts index f05a4248..1039a3e8 100644 --- a/packages/abstract/src/manager/registry.ts +++ b/packages/abstract/src/manager/registry.ts @@ -9,7 +9,7 @@ class Registry { private pubSub = new PubSub(); public [Symbol.iterator]() { - return this.map.value.values(); + return this.map.peek().values(); } public get(identifier: UniqueIdentifier): T | undefined { diff --git a/packages/abstract/src/plugins/plugin.ts b/packages/abstract/src/plugins/plugin.ts index 79d9de9e..9c70c9cb 100644 --- a/packages/abstract/src/plugins/plugin.ts +++ b/packages/abstract/src/plugins/plugin.ts @@ -1,11 +1,23 @@ import type {DragDropManager} from '../manager'; -export abstract class Plugin< +export class Plugin< T extends DragDropManager = DragDropManager, > { constructor(protected manager: T) { this.manager = manager; } - public abstract destroy(): void; + public disabled: boolean = false; + + public enable() { + this.disabled = false; + } + + public disable() { + this.disabled = true; + } + + public destroy() { + // no-op + } } diff --git a/packages/dom-utilities/src/index.ts b/packages/dom-utilities/src/index.ts index d50b5478..00b3661d 100644 --- a/packages/dom-utilities/src/index.ts +++ b/packages/dom-utilities/src/index.ts @@ -10,8 +10,9 @@ export {cloneElement} from './element'; export {Listeners} from './event-listeners'; export { + canScroll, + shouldScroll, getScrollableAncestors, - getScrollDirectionAndSpeed, isDocumentScrollingElement, ScrollDirection, } from './scroll'; diff --git a/packages/dom-utilities/src/scroll/canScroll.ts b/packages/dom-utilities/src/scroll/canScroll.ts new file mode 100644 index 00000000..7aebe3f3 --- /dev/null +++ b/packages/dom-utilities/src/scroll/canScroll.ts @@ -0,0 +1,24 @@ +import type {Coordinates} from '@dnd-kit/geometry'; + +import {getScrollPosition} from './getScrollPosition'; + +export function canScroll(scrollableElement: Element, by?: Coordinates) { + const {isTop, isBottom, isLeft, isRight, position} = + getScrollPosition(scrollableElement); + + const {x, y} = by ?? {x: 0, y: 0}; + + const top = !isTop && position.current.y + y > 0; + const bottom = !isBottom && position.current.y + y < position.max.y; + const left = !isLeft && position.current.x + x > 0; + const right = !isRight && position.current.x + x < position.max.x; + + return { + top, + bottom, + left, + right, + x: left || right, + y: top || bottom, + }; +} diff --git a/packages/dom-utilities/src/scroll/getScrollPosition.ts b/packages/dom-utilities/src/scroll/getScrollPosition.ts index be2fff1e..0b3ded48 100644 --- a/packages/dom-utilities/src/scroll/getScrollPosition.ts +++ b/packages/dom-utilities/src/scroll/getScrollPosition.ts @@ -1,35 +1,45 @@ import {isDocumentScrollingElement} from './documentScrollingElement'; +import { + getViewportBoundingRectangle, + getBoundingRectangle, +} from '../bounding-rectangle'; -export function getScrollPosition(scrollingContainer: Element) { - const minScroll = { - x: 0, - y: 0, - }; - const dimensions = isDocumentScrollingElement(scrollingContainer) +export function getScrollPosition(scrollableElement: Element) { + const rect = isDocumentScrollingElement(scrollableElement) + ? getViewportBoundingRectangle(scrollableElement) + : getBoundingRectangle(scrollableElement); + + const dimensions = isDocumentScrollingElement(scrollableElement) ? { height: window.innerHeight, width: window.innerWidth, } : { - height: scrollingContainer.clientHeight, - width: scrollingContainer.clientWidth, + height: scrollableElement.clientHeight, + width: scrollableElement.clientWidth, }; - const maxScroll = { - x: scrollingContainer.scrollWidth - dimensions.width, - y: scrollingContainer.scrollHeight - dimensions.height, + const position = { + current: { + x: scrollableElement.scrollLeft, + y: scrollableElement.scrollTop, + }, + max: { + x: scrollableElement.scrollWidth - dimensions.width, + y: scrollableElement.scrollHeight - dimensions.height, + }, }; - const isTop = scrollingContainer.scrollTop <= minScroll.y; - const isLeft = scrollingContainer.scrollLeft <= minScroll.x; - const isBottom = scrollingContainer.scrollTop >= maxScroll.y; - const isRight = scrollingContainer.scrollLeft >= maxScroll.x; + const isTop = position.current.y <= 0; + const isLeft = position.current.x <= 0; + const isBottom = position.current.y >= position.max.y; + const isRight = position.current.x >= position.max.x; return { + rect, + position, isTop, isLeft, isBottom, isRight, - maxScroll, - minScroll, }; } diff --git a/packages/dom-utilities/src/scroll/index.ts b/packages/dom-utilities/src/scroll/index.ts index b88a012e..b31aba73 100644 --- a/packages/dom-utilities/src/scroll/index.ts +++ b/packages/dom-utilities/src/scroll/index.ts @@ -1,13 +1,11 @@ +export {canScroll} from './canScroll'; export { getFirstScrollableAncestor, getScrollableAncestors, } from './getScrollableAncestors'; export {getScrollableElement} from './getScrollableElement'; export {getScrollCoordinates} from './getScrollCoordinates'; -export { - getScrollDirectionAndSpeed, - ScrollDirection, -} from './getScrollDirectionAndSpeed'; +export {shouldScroll, ScrollDirection} from './shouldScroll'; export { getScrollOffsets, getScrollXOffset, diff --git a/packages/dom-utilities/src/scroll/getScrollDirectionAndSpeed.ts b/packages/dom-utilities/src/scroll/shouldScroll.ts similarity index 54% rename from packages/dom-utilities/src/scroll/getScrollDirectionAndSpeed.ts rename to packages/dom-utilities/src/scroll/shouldScroll.ts index 4fd5af46..bcdb8e11 100644 --- a/packages/dom-utilities/src/scroll/getScrollDirectionAndSpeed.ts +++ b/packages/dom-utilities/src/scroll/shouldScroll.ts @@ -1,4 +1,4 @@ -import type {Axis, BoundingRectangle} from '@dnd-kit/geometry'; +import type {Axis, BoundingRectangle, Coordinates} from '@dnd-kit/geometry'; import {getScrollPosition} from './getScrollPosition'; @@ -13,15 +13,19 @@ const defaultThreshold: Record = { y: 0.2, }; -export function getScrollDirectionAndSpeed( - scrollContainer: Element, - scrollContainerRect: BoundingRectangle, - rect: BoundingRectangle, +export function shouldScroll( + scrollableElement: Element, + coordinates: Coordinates, acceleration = 10, thresholdPercentage = defaultThreshold ) { - const {top, left, bottom, right} = rect; - const {isTop, isBottom, isLeft, isRight} = getScrollPosition(scrollContainer); + const { + rect: scrollContainerRect, + isTop, + isBottom, + isLeft, + isRight, + } = getScrollPosition(scrollableElement); const direction: Record = { x: ScrollDirection.Idle, @@ -36,43 +40,52 @@ export function getScrollDirectionAndSpeed( width: scrollContainerRect.width * thresholdPercentage.x, }; - if (!isTop && top <= scrollContainerRect.top + threshold.height) { + if (!isTop && coordinates.y <= scrollContainerRect.top + threshold.height) { // Scroll Up direction.y = ScrollDirection.Reverse; speed.y = acceleration * Math.abs( - (scrollContainerRect.top + threshold.height - top) / threshold.height + (scrollContainerRect.top + threshold.height - coordinates.y) / + threshold.height ); } else if ( !isBottom && - bottom >= scrollContainerRect.bottom - threshold.height + coordinates.y >= scrollContainerRect.bottom - threshold.height ) { // Scroll Down direction.y = ScrollDirection.Forward; speed.y = acceleration * Math.abs( - (scrollContainerRect.bottom - threshold.height - bottom) / + (scrollContainerRect.bottom - threshold.height - coordinates.y) / threshold.height ); } - if (!isRight && right >= scrollContainerRect.right - threshold.width) { + if ( + !isRight && + coordinates.x >= scrollContainerRect.right - threshold.width + ) { // Scroll Right direction.x = ScrollDirection.Forward; speed.x = acceleration * Math.abs( - (scrollContainerRect.right - threshold.width - right) / threshold.width + (scrollContainerRect.right - threshold.width - coordinates.x) / + threshold.width ); - } else if (!isLeft && left <= scrollContainerRect.left + threshold.width) { + } else if ( + !isLeft && + coordinates.x <= scrollContainerRect.left + threshold.width + ) { // Scroll Left direction.x = ScrollDirection.Reverse; speed.x = acceleration * Math.abs( - (scrollContainerRect.left + threshold.width - left) / threshold.width + (scrollContainerRect.left + threshold.width - coordinates.x) / + threshold.width ); } diff --git a/packages/dom/src/manager/manager.ts b/packages/dom/src/manager/manager.ts index 2cd843d7..43b8a6f4 100644 --- a/packages/dom/src/manager/manager.ts +++ b/packages/dom/src/manager/manager.ts @@ -4,9 +4,15 @@ import { PluginConstructor, SensorConstructor, } from '@dnd-kit/abstract'; +import {batch, effect} from '@dnd-kit/state'; import type {Draggable, Droppable} from '../nodes'; -import {AutoScroller, DraggablePlaceholder, ScrollManager} from '../plugins'; +import { + AutoScroller, + DraggablePlaceholder, + ScrollManager, + Scroller, +} from '../plugins'; import {PointerSensor} from '../sensors'; export interface Input extends DragDropManagerInput {} @@ -22,7 +28,7 @@ export class DragDropManager< T extends Draggable = Draggable, U extends Droppable = Droppable, > extends AbstractDragDropManager { - public scrollManager: ScrollManager; + public scroller: Scroller; constructor({ plugins = defaultPlugins, @@ -31,11 +37,30 @@ export class DragDropManager< }: Input = {}) { super({...input, plugins, sensors}); - this.scrollManager = new ScrollManager(this); + const scrollManager = new ScrollManager(this); + this.scroller = new Scroller(this); + + const effectCleanup = effect(() => { + if (this.dragOperation.status === 'initializing') { + batch(() => { + for (const droppable of this.registry.droppable) { + droppable.updateShape(); + } + }); + } + }); + + const {destroy} = this; + + this.destroy = () => { + effectCleanup(); + scrollManager.destroy(); + destroy(); + }; } - public destroy() { + public destroy = () => { super.destroy(); - this.scrollManager.destroy(); - } + this.scroller.destroy(); + }; } diff --git a/packages/dom/src/nodes/droppable/droppable.ts b/packages/dom/src/nodes/droppable/droppable.ts index 69debc45..4e8f53f2 100644 --- a/packages/dom/src/nodes/droppable/droppable.ts +++ b/packages/dom/src/nodes/droppable/droppable.ts @@ -6,6 +6,7 @@ import type { Data, DroppableInput as AbstractDroppableInput, } from '@dnd-kit/abstract'; +import {Shape} from '@dnd-kit/geometry'; import {defaultCollisionDetection} from '@dnd-kit/collision'; import type {CollisionDetector} from '@dnd-kit/collision'; import {effect, reactive} from '@dnd-kit/state'; @@ -17,6 +18,7 @@ type OptionalInput = 'collisionDetector'; export interface Input extends Omit, OptionalInput> { collisionDetector?: CollisionDetector; + shape?: Shape; } export class Droppable extends AbstractDroppable { @@ -29,12 +31,23 @@ export class Droppable extends AbstractDroppable { }: Input) { super({...input, collisionDetector}); - this.destroy = effect(this.update); + this.destroy = effect(this.updateShape); } - public update = () => { + public updateShape = () => { const {disabled, element} = this; - this.shape = element && !disabled ? new DOMRectangle(element) : null; + if (!element || disabled) { + this.shape = null; + return; + } + + const updatedShape = new DOMRectangle(element); + + if (this.shape?.equals(updatedShape)) { + return; + } + + this.shape = updatedShape; }; } diff --git a/packages/dom/src/plugins/index.ts b/packages/dom/src/plugins/index.ts index 933c2cb9..c33991e9 100644 --- a/packages/dom/src/plugins/index.ts +++ b/packages/dom/src/plugins/index.ts @@ -1,2 +1,2 @@ export {DraggableClone, DraggablePlaceholder} from './feedback'; -export {AutoScroller, ScrollManager} from './scrolling'; +export {AutoScroller, Scroller, ScrollManager} from './scrolling'; diff --git a/packages/dom/src/plugins/scrolling/AutoScroller.ts b/packages/dom/src/plugins/scrolling/AutoScroller.ts index e0548563..c4afb80f 100644 --- a/packages/dom/src/plugins/scrolling/AutoScroller.ts +++ b/packages/dom/src/plugins/scrolling/AutoScroller.ts @@ -1,16 +1,8 @@ import {Plugin} from '@dnd-kit/abstract'; import type {CleanupFunction} from '@dnd-kit/types'; -import { - getBoundingRectangle, - getScrollableAncestors, - getScrollDirectionAndSpeed, - getViewportBoundingRectangle, - isDocumentScrollingElement, - ScrollDirection, -} from '@dnd-kit/dom-utilities'; +import {shouldScroll} from '@dnd-kit/dom-utilities'; import {Axes} from '@dnd-kit/geometry'; -import {computed, effect} from '@dnd-kit/state'; -import {isEqual} from '@dnd-kit/utilities'; +import {effect} from '@dnd-kit/state'; import type {DragDropManager} from '../../manager'; @@ -23,75 +15,34 @@ const AUTOSCROLL_INTERVAL = 5; export class AutoScroller extends Plugin { public destroy: CleanupFunction; - private disabled = false; - - public disable() { - this.disabled = true; - } - - public enable() { - this.disabled = false; - } - constructor(manager: DragDropManager, _options?: Options) { super(manager); const {dragOperation} = manager; - const elementFromPoint = computed(() => { - const {position} = dragOperation; - - if (!position) { - return null; - } - - const {x, y} = position.current; - - return document.elementFromPoint(x, y); - }); - const scrollableElements = computed(() => { - const element = elementFromPoint.value; - - return element - ? getScrollableAncestors(element, {excludeElement: false}) - : null; - }, isEqual); let interval: NodeJS.Timer | null = null; const scrollIntentTracker = ScrollIntentTracker(manager); this.destroy = effect(() => { - const elements = scrollableElements.value; + const {position, status} = manager.dragOperation; - if (elements) { - const {position} = dragOperation; - const currentPosition = position?.current; + if (!this.disabled && status === 'dragging') { + const scrollIntent = scrollIntentTracker.peek(); + const scrollableElements = + this.manager.scroller.getScrollableElements(); - if (!this.disabled && currentPosition) { - const scrollIntent = scrollIntentTracker.peek(); - - for (const scrollableElement of elements) { - const rect = isDocumentScrollingElement(scrollableElement) - ? getViewportBoundingRectangle(scrollableElement) - : getBoundingRectangle(scrollableElement); - - const {direction, speed} = getScrollDirectionAndSpeed( + if (scrollableElements) { + for (const scrollableElement of scrollableElements) { + const {speed, direction} = shouldScroll( scrollableElement, - rect, - { - width: 0, - height: 0, - top: currentPosition.y, - bottom: currentPosition.y, - left: currentPosition.x, - right: currentPosition.x, - } + position.current ); if (scrollIntent) { for (const axis of Axes) { if (scrollIntent[axis].isLocked(direction[axis])) { speed[axis] = 0; - direction[axis] = ScrollDirection.Idle; + direction[axis] = 0; } } } @@ -105,6 +56,7 @@ export class AutoScroller extends Plugin { }; interval = setInterval(autoScroll, AUTOSCROLL_INTERVAL); + break; } } } diff --git a/packages/dom/src/plugins/scrolling/ScrollManager.ts b/packages/dom/src/plugins/scrolling/ScrollManager.ts index d59427bc..366f234e 100644 --- a/packages/dom/src/plugins/scrolling/ScrollManager.ts +++ b/packages/dom/src/plugins/scrolling/ScrollManager.ts @@ -1,6 +1,6 @@ import {DragOperationStatus, Plugin} from '@dnd-kit/abstract'; import {PubSub} from '@dnd-kit/utilities'; -import {effect} from '@dnd-kit/state'; +import {batch, effect} from '@dnd-kit/state'; import type {DragDropManager} from '../../manager'; import {CleanupFunction} from '@dnd-kit/types'; @@ -50,9 +50,11 @@ export class ScrollManager extends Plugin { this.animationFrame = requestAnimationFrame(() => { this.pubSub.notify(); - for (const droppable of this.manager.registry.droppable) { - droppable.update(); - } + batch(() => { + for (const droppable of this.manager.registry.droppable) { + droppable.updateShape(); + } + }); this.animationFrame = null; }); diff --git a/packages/dom/src/plugins/scrolling/Scroller.ts b/packages/dom/src/plugins/scrolling/Scroller.ts new file mode 100644 index 00000000..60086473 --- /dev/null +++ b/packages/dom/src/plugins/scrolling/Scroller.ts @@ -0,0 +1,78 @@ +import {Plugin} from '@dnd-kit/abstract'; +import { + canScroll, + shouldScroll, + getScrollableAncestors, +} from '@dnd-kit/dom-utilities'; +import {computed} from '@dnd-kit/state'; +import {isEqual} from '@dnd-kit/utilities'; + +import type {DragDropManager} from '../../manager'; + +interface Options {} + +export class Scroller extends Plugin { + public getScrollableElements: () => Element[] | null; + + constructor(manager: DragDropManager, _options?: Options) { + super(manager); + + const elementFromPoint = computed(() => { + const {position} = manager.dragOperation; + + if (!position) { + return null; + } + + const {x, y} = position.current; + + return document.elementFromPoint(x, y); + }); + const scrollableElements = computed(() => { + const element = elementFromPoint.value; + + return element + ? getScrollableAncestors(element, {excludeElement: false}) + : null; + }, isEqual); + + this.getScrollableElements = () => scrollableElements.value; + } + + public scrollBy(x: number, y: number): boolean { + const elements = this.getScrollableElements(); + + if (!elements || this.disabled) { + return false; + } + + const {position} = this.manager.dragOperation; + const currentPosition = position?.current; + + if (currentPosition) { + for (const scrollableElement of elements) { + const elementCanScroll = canScroll(scrollableElement, {x, y}); + + if (elementCanScroll.x || elementCanScroll.y) { + const shouldScrollElement = shouldScroll( + scrollableElement, + currentPosition + ); + + if ( + shouldScrollElement.direction.x || + shouldScrollElement.direction.y + ) { + const scrollLeft = x * shouldScrollElement.speed.x; + const scrollTop = y * shouldScrollElement.speed.y; + + scrollableElement.scrollBy(scrollLeft, scrollTop); + return true; + } + } + } + } + + return false; + } +} diff --git a/packages/dom/src/plugins/scrolling/index.ts b/packages/dom/src/plugins/scrolling/index.ts index aa9e8a33..77bb3358 100644 --- a/packages/dom/src/plugins/scrolling/index.ts +++ b/packages/dom/src/plugins/scrolling/index.ts @@ -1,2 +1,3 @@ +export {Scroller} from './Scroller'; export {AutoScroller} from './AutoScroller'; export {ScrollManager} from './ScrollManager'; diff --git a/packages/dom/src/sensors/keyboard/KeyboardSensor.ts b/packages/dom/src/sensors/keyboard/KeyboardSensor.ts index d5f6f92f..f1c5b205 100644 --- a/packages/dom/src/sensors/keyboard/KeyboardSensor.ts +++ b/packages/dom/src/sensors/keyboard/KeyboardSensor.ts @@ -48,15 +48,15 @@ export class KeyboardSensor extends Sensor< const target = source.activator ?? source.element; const listener: EventListener = (event: Event) => { if (event instanceof KeyboardEvent) { - this.handleKeyUp(event, source, options); + this.handleKeyDown(event, source, options); } }; if (target) { - target.addEventListener('keyup', listener); + target.addEventListener('keydown', listener); return () => { - target.removeEventListener('keyup', listener); + target.removeEventListener('keydown', listener); }; } }); @@ -64,7 +64,7 @@ export class KeyboardSensor extends Sensor< return unbind; } - private handleKeyUp = ( + private handleKeyDown = ( event: KeyboardEvent, source: Draggable, options: KeyboardSensorOptions @@ -83,13 +83,13 @@ export class KeyboardSensor extends Sensor< return; } - const {center} = new DOMRectangle(source.element); + if (this.manager.dragOperation.status !== 'idle') { + return; + } - const autoScroller = this.manager.plugins.get(AutoScroller); + const {center} = new DOMRectangle(source.element); - if (autoScroller) { - autoScroller.disable(); - } + const cleanupSideEffects = this.sideEffects(); this.manager.actions.setDragSource(source.id); this.manager.actions.start({ @@ -97,6 +97,7 @@ export class KeyboardSensor extends Sensor< y: center.y, }); + event.preventDefault(); event.stopImmediatePropagation(); const ownerDocument = getOwnerDocument(source.element); @@ -106,6 +107,7 @@ export class KeyboardSensor extends Sensor< this.manager.actions.stop(); this.cleanup?.(); + cleanupSideEffects(); setTimeout(() => { const draggable = this.manager.registry.draggable.get(source.id); @@ -122,44 +124,64 @@ export class KeyboardSensor extends Sensor< } const {center} = new DOMRectangle(source.element); + const factor = event.shiftKey ? 5 : 1; + const offset = { + x: 0, + y: 0, + }; switch (event.code) { - case 'ArrowUp': - event.preventDefault(); - this.manager.actions.move({ - x: center.x, - y: center.y - DEFAULT_OFFSET, - }); - return; - case 'ArrowDown': - event.preventDefault(); - this.manager.actions.move({ - x: center.x, - y: center.y + DEFAULT_OFFSET, - }); - return; - case 'ArrowLeft': - event.preventDefault(); - this.manager.actions.move({ - x: center.x - DEFAULT_OFFSET, - y: center.y, - }); - return; - case 'ArrowRight': - event.preventDefault(); + case 'ArrowUp': { + offset.y = -DEFAULT_OFFSET * factor; + break; + } + case 'ArrowDown': { + offset.y = DEFAULT_OFFSET * factor; + break; + } + case 'ArrowLeft': { + offset.x = -DEFAULT_OFFSET * factor; + break; + } + case 'ArrowRight': { + offset.x = DEFAULT_OFFSET * factor; + break; + } + } + + if (offset.x || offset.y) { + event.preventDefault(); + + if (!this.manager.scroller.scrollBy(offset.x, offset.y)) { this.manager.actions.move({ - x: center.x + DEFAULT_OFFSET, - y: center.y, + x: center.x + offset.x, + y: center.y + offset.y, }); - return; + } } }; this.cleanup = this.listeners.bind(ownerDocument, [ - {type: 'keydown', listener: onKeyUp, options: {capture: true}}, + {type: 'keydown', listener: onKeyUp}, ]); }; + private sideEffects(): CleanupFunction { + const effectCleanupFns: CleanupFunction[] = []; + + const autoScroller = this.manager.plugins.get(AutoScroller); + + if (autoScroller?.disabled === false) { + autoScroller.disable(); + + effectCleanupFns.push(() => { + autoScroller.enable(); + }); + } + + return () => effectCleanupFns.forEach((cleanup) => cleanup()); + } + public destroy() { // Remove all event listeners this.listeners.clear(); diff --git a/packages/dom/src/sensors/pointer/PointerSensor.ts b/packages/dom/src/sensors/pointer/PointerSensor.ts index 28275408..52c0b9d3 100644 --- a/packages/dom/src/sensors/pointer/PointerSensor.ts +++ b/packages/dom/src/sensors/pointer/PointerSensor.ts @@ -80,6 +80,13 @@ export class PointerSensor extends Sensor< this.manager.actions.setDragSource(source.id); + if (this.manager.dragOperation.status === 'idle') { + this.manager.actions.start({ + x: event.clientX, + y: event.clientY, + }); + } + event.stopImmediatePropagation(); const ownerDocument = getOwnerDocument(event.target); @@ -105,14 +112,6 @@ export class PointerSensor extends Sensor< event.preventDefault(); event.stopPropagation(); - if (this.manager.dragOperation.status === 'idle') { - this.manager.actions.start({ - x: event.clientX, - y: event.clientY, - }); - return; - } - this.manager.actions.move({ x: event.clientX, y: event.clientY, diff --git a/packages/geometry/src/shapes/Rectangle.ts b/packages/geometry/src/shapes/Rectangle.ts index a288f431..21b0c628 100644 --- a/packages/geometry/src/shapes/Rectangle.ts +++ b/packages/geometry/src/shapes/Rectangle.ts @@ -51,6 +51,21 @@ export class Rectangle implements Shape { return width * height; } + public equals(shape: Shape): boolean { + if (!(shape instanceof Rectangle)) { + return false; + } + + const {left, top, width, height} = this; + + return ( + left === shape.left && + top === shape.top && + width === shape.width && + height === shape.height + ); + } + public containsPoint(point: Point): boolean { const {top, left, bottom, right} = this; diff --git a/packages/geometry/src/shapes/Shape.ts b/packages/geometry/src/shapes/Shape.ts index 3a5bd94e..a2e0e585 100644 --- a/packages/geometry/src/shapes/Shape.ts +++ b/packages/geometry/src/shapes/Shape.ts @@ -2,19 +2,43 @@ import type {Point} from '../point'; import type {BoundingRectangle} from '../types'; /** - * An abstract class representing a 2 dimensional geometric shape, - * such as a polygon or circle. Shapes are used for collision detection - * during drag and drop operations + * An abstract class representing a 2D geometric shape, such as + * a polygon or circle. Shapes are used for collision detection + * during drag and drop operations. */ export abstract class Shape { + /** + * Get the bounding rectangle of the 2D shape. + * @returns The bounding rectangle of the shape. + */ abstract get boundingRectangle(): BoundingRectangle; + /** + * Get the center point of the 2D shape. + * @returns The center point of the shape. + */ abstract get center(): Point; + /** + * Get the total space taken up by the 2D shape. + * @returns The area of the shape. + */ abstract get area(): number; + /** + * Get the scale transformation of the shape on the 2D plane. + * @returns The scale of the shape. + */ abstract get scale(): {x: number; y: number}; + /** + * Returns whether or not this shape is equal to another shape. + * + * @param shape The other shape to compare with. + * @returns Whether or not the two shapes are equal. + */ + abstract equals(shape: Shape): boolean; + /** * Returns the intersection area between this shape and another shape. *