diff --git a/apps/docs/stories/Droppable/DroppableExample.tsx b/apps/docs/stories/Droppable/DroppableExample.tsx index 69077889..3019450e 100644 --- a/apps/docs/stories/Droppable/DroppableExample.tsx +++ b/apps/docs/stories/Droppable/DroppableExample.tsx @@ -7,45 +7,123 @@ import {closestCenter, CollisionDetector} from '@dnd-kit/collision'; import {Button, Dropzone} from '../components'; import {DraggableIcon} from '../icons'; -const items = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, -]; - export function DroppableExample() { - const [parent, setParent] = useState(null); - const draggableMarkup = ; + const [items, setItems] = useState({ + A: [ + {id: 'A1', children: [{id: 'A1', type: 'A'}]}, + {id: 'A2', children: []}, + {id: 'A3', children: []}, + {id: 'A4', children: []}, + {id: 'A5', children: []}, + {id: 'A6', children: []}, + {id: 'A7', children: []}, + ], + B: [ + {id: 'B1', children: []}, + {id: 'B2', children: [{id: 'A2', type: 'A'}]}, + {id: 'B3', children: []}, + {id: 'B4', children: []}, + ], + C: [ + {id: 'C1', children: []}, + {id: 'C2', children: []}, + {id: 'C3', children: []}, + {id: 'C4', children: []}, + ], + }); return ( { - console.log(event); - setParent(event.operation.target?.id ?? null); + onDragOver={(event) => { + const {source, target} = event.operation; + + if (source && target) { + const [targetParentId] = String(target.id); + const [currentParentId] = String(source.data!.parent); + + if (source.data!.parent !== target.id) { + setItems((items) => { + if (targetParentId !== currentParentId) { + return { + ...items, + [currentParentId]: items[currentParentId].map((item) => { + if (item.id === source.data!.parent) { + return { + ...item, + children: item.children.filter( + (child) => child.id !== source.id + ), + }; + } + + return item; + }), + [targetParentId]: items[targetParentId].map((item) => { + if (item.id === target.id) { + return { + ...item, + children: [ + ...item.children, + {id: source.id, type: source.type}, + ], + }; + } + + return item; + }), + }; + } else { + return { + ...items, + [targetParentId]: items[targetParentId].map((item) => { + if (item.id === target.id) { + return { + ...item, + children: [ + ...item.children, + {id: source.id, type: source.type}, + ], + }; + } + + if (item.id === source.data!.parent) { + return { + ...item, + children: item.children.filter( + (child) => child.id !== source.id + ), + }; + } + + return item; + }), + }; + } + }); + } + } }} > -
-
- {items.map((id) => ( - - {parent === `A${id}` ? draggableMarkup : null} - - ))} -
- -
- {parent == null ? draggableMarkup : null} -
- {/* */} - {items.map((id) => ( - - {parent === `B${id}` ? draggableMarkup : null} - - ))} -
+
+ {Object.entries(items).map(([id, items]) => ( +
+ {items.map((item) => ( + + {item.children.map((child) => ( + + ))} + + ))} +
+ ))}
); @@ -53,16 +131,17 @@ export function DroppableExample() { interface DraggableProps { id: UniqueIdentifier; + parent: UniqueIdentifier; type?: UniqueIdentifier; } -function Draggable({id, type}: DraggableProps) { +function Draggable({id, parent, type}: DraggableProps) { const [element, setElement] = useState(null); - const [backgroundColor, setBackgroundColor] = useState(''); const activatorRef = useRef(null); const {isDragging} = useDraggable({ id, + data: {parent}, element, activator: activatorRef, type, diff --git a/apps/docs/stories/components/Dropzone/Dropzone.module.css b/apps/docs/stories/components/Dropzone/Dropzone.module.css index a03e5133..fcb9fee2 100644 --- a/apps/docs/stories/components/Dropzone/Dropzone.module.css +++ b/apps/docs/stories/components/Dropzone/Dropzone.module.css @@ -1,7 +1,9 @@ .Dropzone { display: flex; - align-items: flex-start; - justify-content: center; + flex-direction: column; + gap: 20px; + align-items: center; + justify-content: flex-start; position: relative; padding-top: 80px; text-align: center; diff --git a/packages/abstract/src/index.ts b/packages/abstract/src/index.ts index e22c4b5b..894366e7 100644 --- a/packages/abstract/src/index.ts +++ b/packages/abstract/src/index.ts @@ -9,12 +9,15 @@ export type { export {CollisionPriority} from './collision'; export type {Collision, CollisionDetector} from './collision'; -export {Plugin} from './plugins'; -export type {PluginConstructor} from './plugins'; +export {Modifier} from './modifiers'; +export type {ModifierConstructor} from './modifiers'; export {Draggable, Droppable} from './nodes'; export type {Data, Node, DraggableInput, DroppableInput} from './nodes'; +export {Plugin} from './plugins'; +export type {PluginConstructor} from './plugins'; + export {Sensor} from './sensors'; export type { SensorConstructor, diff --git a/packages/abstract/src/manager/dragOperation.ts b/packages/abstract/src/manager/dragOperation.ts index 96153d4f..49520af1 100644 --- a/packages/abstract/src/manager/dragOperation.ts +++ b/packages/abstract/src/manager/dragOperation.ts @@ -5,8 +5,7 @@ import {batch, computed, signal} from '@dnd-kit/state'; import type {Draggable, Droppable} from '../nodes'; -import type {DragDropRegistry} from './registry'; -import type {DragDropMonitor} from './manager'; +import type {DragDropManager} from './manager'; export enum Status { Idle = 'idle', @@ -15,19 +14,6 @@ export enum Status { Dropping = 'dropped', } -export interface Input< - T extends Draggable = Draggable, - U extends Droppable = Droppable, -> { - registry: DragDropRegistry; - monitor: DragDropMonitor; -} - -export type DragOperationManager< - T extends Draggable = Draggable, - U extends Droppable = Droppable, -> = ReturnType>; - export type Serializable = { [key: string]: string | number | null | Serializable | Serializable[]; }; @@ -38,6 +24,7 @@ export interface DragOperation< > { status: Status; position: Position; + transform: Coordinates; initialized: boolean; shape: Shape | null; source: T | null; @@ -45,10 +32,22 @@ export interface DragOperation< data?: Serializable; } +export type DragActions< + T extends Draggable, + U extends Droppable, + V extends DragDropManager, +> = ReturnType>['actions']; + export function DragOperationManager< - T extends Draggable = Draggable, - U extends Droppable = Droppable, ->({registry: {draggable, droppable}, monitor}: Input) { + T extends Draggable, + U extends Droppable, + V extends DragDropManager, +>(manager: V) { + const { + registry: {draggable, droppable}, + monitor, + modifiers, + } = manager; const status = signal(Status.Idle); const shape = signal(null); const position = new Position({x: 0, y: 0}); @@ -64,6 +63,25 @@ export function DragOperationManager< }); const dragging = computed(() => status.value === Status.Dragging); + const transform = computed(() => { + const {x, y} = position.delta; + let transform = {x, y}; + const operation = { + source: source.peek() ?? null, + target: target.peek() ?? null, + initialized: status.peek() !== Status.Idle, + status: status.peek(), + shape: shape.peek(), + position, + }; + + for (const modifier of modifiers) { + transform = modifier.apply({...operation, transform}); + } + + return transform; + }); + const operation: DragOperation = { get source() { return source.value ?? null; @@ -81,8 +99,15 @@ export function DragOperationManager< return shape.value; }, set shape(value: Shape | null) { + if (value && shape.peek()?.equals(value)) { + return; + } + shape.value = value; }, + get transform() { + return transform.value; + }, position, }; @@ -98,6 +123,10 @@ export function DragOperationManager< } targetIdentifier.value = identifier; + + monitor.dispatch('dragover', { + operation: snapshot(operation), + }); }, start(coordinates: Coordinates) { status.value = Status.Initializing; @@ -138,6 +167,7 @@ export function DragOperationManager< status.value = Status.Idle; sourceIdentifier.value = null; targetIdentifier.value = null; + shape.value = null; position.reset({x: 0, y: 0}); }); }); diff --git a/packages/abstract/src/manager/index.ts b/packages/abstract/src/manager/index.ts index 0216c7ef..3f0f8c43 100644 --- a/packages/abstract/src/manager/index.ts +++ b/packages/abstract/src/manager/index.ts @@ -1,9 +1,6 @@ export {DragDropManager} from './manager'; -export type { - DragDropManagerInput, - DragDropConfiguration, - DragDropEvents, -} from './manager'; +export type {DragDropManagerInput, DragDropConfiguration} from './manager'; +export type {DragDropEvents} from './monitor'; export {Status as DragOperationStatus} from './dragOperation'; export type {DragOperation, DragOperationManager} from './dragOperation'; export type {DragDropRegistry} from './registry'; diff --git a/packages/abstract/src/manager/manager.ts b/packages/abstract/src/manager/manager.ts index 943948f9..f906cef2 100644 --- a/packages/abstract/src/manager/manager.ts +++ b/packages/abstract/src/manager/manager.ts @@ -2,62 +2,59 @@ import type {Draggable, Droppable} from '../nodes'; import {CollisionObserver} from '../collision'; import {DragDropRegistry} from './registry'; -import {DragOperationManager} from './dragOperation'; -import type {DragOperation} from './dragOperation'; -import {Monitor} from './monitor'; +import { + DragOperationManager, + type DragOperation, + type DragActions, +} from './dragOperation'; +import {DragDropMonitor} from './monitor'; import {PluginRegistry, type PluginConstructor} from '../plugins'; -import type {SensorConstructor} from '../sensors'; +import type {Sensor, SensorConstructor} from '../sensors'; +import type {Modifier, ModifierConstructor} from '../modifiers'; export interface DragDropConfiguration> { plugins: PluginConstructor[]; sensors: SensorConstructor[]; + modifiers: ModifierConstructor[]; } export type DragDropManagerInput> = Partial< DragDropConfiguration >; -export type DragDropEvents = { - dragstart: {}; - dragmove: {}; - dragend: {operation: DragOperation; canceled: boolean}; -}; - -export class DragDropMonitor< - T extends DragDropManager = DragDropManager, -> extends Monitor { - constructor(private manager: T) { - super(); - } - - public dispatch( - type: T, - event: DragDropEvents[T] - ) { - super.dispatch(type, event); - } -} - export class DragDropManager< T extends Draggable = Draggable, U extends Droppable = Droppable, > { - public actions: DragOperationManager['actions']; + public actions: DragActions>; public collisionObserver: CollisionObserver; public dragOperation: DragOperation; public registry: DragDropRegistry; public monitor: DragDropMonitor>; public plugins: PluginRegistry>; - public sensors: PluginRegistry>; + public sensors: PluginRegistry< + DragDropManager, + Sensor> + >; + public modifiers: PluginRegistry< + DragDropManager, + Modifier> + >; constructor(config?: DragDropManagerInput>) { - const {plugins = [], sensors = []} = config ?? {}; - const monitor = new DragDropMonitor(this); + type V = DragDropManager; + + const {plugins = [], sensors = [], modifiers = []} = config ?? {}; + const monitor = new DragDropMonitor(this); const registry = new DragDropRegistry(); - const {actions, operation} = DragOperationManager({ - registry, - monitor, - }); + + this.registry = registry; + this.monitor = monitor; + this.plugins = new PluginRegistry(this); + this.sensors = new PluginRegistry>(this); + this.modifiers = new PluginRegistry>(this); + + const {actions, operation} = DragOperationManager(this); const collisionObserver = new CollisionObserver({ dragOperation: operation, registry, @@ -65,11 +62,11 @@ export class DragDropManager< this.actions = actions; this.collisionObserver = collisionObserver; - this.registry = registry; this.dragOperation = operation; - this.monitor = monitor; - this.plugins = new PluginRegistry(this); - this.sensors = new PluginRegistry(this); + + for (const modifier of modifiers) { + this.modifiers.register(modifier); + } for (const plugin of plugins) { this.plugins.register(plugin); diff --git a/packages/abstract/src/manager/monitor.ts b/packages/abstract/src/manager/monitor.ts index f42eb67a..391f0bc5 100644 --- a/packages/abstract/src/manager/monitor.ts +++ b/packages/abstract/src/manager/monitor.ts @@ -1,8 +1,11 @@ import type {AnyFunction} from '@dnd-kit/types'; +import type {DragDropManager} from './manager'; +import type {DragOperation} from './dragOperation'; + export type Events = Record; -export class Monitor { +class Monitor { private registry = new Map>(); public addEventListener(name: keyof T, handler: AnyFunction) { @@ -34,3 +37,25 @@ export class Monitor { } } } + +export type DragDropEvents = { + dragstart: {}; + dragmove: {}; + dragover: {operation: DragOperation}; + dragend: {operation: DragOperation; canceled: boolean}; +}; + +export class DragDropMonitor< + T extends DragDropManager = DragDropManager, +> extends Monitor { + constructor(private manager: T) { + super(); + } + + public dispatch( + type: T, + event: DragDropEvents[T] + ) { + super.dispatch(type, event); + } +} diff --git a/packages/abstract/src/modifiers/index.ts b/packages/abstract/src/modifiers/index.ts new file mode 100644 index 00000000..b7952f03 --- /dev/null +++ b/packages/abstract/src/modifiers/index.ts @@ -0,0 +1,3 @@ +export {Modifier} from './modifier'; + +export type {ModifierConstructor} from './modifier'; diff --git a/packages/abstract/src/modifiers/modifier.ts b/packages/abstract/src/modifiers/modifier.ts new file mode 100644 index 00000000..d3fa9f74 --- /dev/null +++ b/packages/abstract/src/modifiers/modifier.ts @@ -0,0 +1,18 @@ +import type {Coordinates} from '@dnd-kit/geometry'; + +import {Plugin, type PluginConstructor} from '../plugins'; + +import type {DragDropManager, DragOperation} from '../manager'; + +export class Modifier> extends Plugin { + constructor(manager: T) { + super(manager); + } + + public apply(operation: DragOperation): Coordinates { + return operation.transform; + } +} + +export type ModifierConstructor> = + PluginConstructor>; diff --git a/packages/abstract/src/plugins/registry.ts b/packages/abstract/src/plugins/registry.ts index d89df1f9..7f5985bd 100644 --- a/packages/abstract/src/plugins/registry.ts +++ b/packages/abstract/src/plugins/registry.ts @@ -4,26 +4,30 @@ import type {Plugin} from './plugin'; import type {PluginConstructor} from './types'; export class PluginRegistry< - T extends DragDropManager = DragDropManager, + T extends DragDropManager, + U extends Plugin = Plugin, + V extends PluginConstructor = PluginConstructor, > { - private instances: Map, Plugin> = new Map(); + private instances: Map = new Map(); constructor(private manager: T) {} - public get>( - plugin: S - ): InstanceType | undefined { + public [Symbol.iterator]() { + return this.instances.values(); + } + + public get(plugin: V): U | undefined { const instance = this.instances.get(plugin); - return instance as InstanceType | undefined; + return instance; } - public register>(plugin: S): InstanceType { + public register(plugin: V): U { const instance = new plugin(this.manager); this.instances.set(plugin, instance); - return instance as InstanceType; + return instance; } public destroy() { diff --git a/packages/dom/src/manager/manager.ts b/packages/dom/src/manager/manager.ts index 43b8a6f4..c31b18f9 100644 --- a/packages/dom/src/manager/manager.ts +++ b/packages/dom/src/manager/manager.ts @@ -9,17 +9,20 @@ import {batch, effect} from '@dnd-kit/state'; import type {Draggable, Droppable} from '../nodes'; import { AutoScroller, - DraggablePlaceholder, + CloneFeedback, + PlaceholderFeedback, ScrollManager, Scroller, } from '../plugins'; import {PointerSensor} from '../sensors'; +import {DragSourceDeltaModifier} from '../modifiers'; export interface Input extends DragDropManagerInput {} const defaultPlugins: PluginConstructor[] = [ AutoScroller, - DraggablePlaceholder, + CloneFeedback, + PlaceholderFeedback, ]; const defaultSensors: SensorConstructor[] = [PointerSensor]; @@ -33,9 +36,15 @@ export class DragDropManager< constructor({ plugins = defaultPlugins, sensors = defaultSensors, + modifiers = [], ...input }: Input = {}) { - super({...input, plugins, sensors}); + super({ + ...input, + plugins, + sensors, + modifiers: [DragSourceDeltaModifier, ...modifiers], + }); const scrollManager = new ScrollManager(this); this.scroller = new Scroller(this); diff --git a/packages/dom/src/modifiers/DragSourceDeltaModifier.ts b/packages/dom/src/modifiers/DragSourceDeltaModifier.ts new file mode 100644 index 00000000..a22c846c --- /dev/null +++ b/packages/dom/src/modifiers/DragSourceDeltaModifier.ts @@ -0,0 +1,64 @@ +import {Modifier} from '@dnd-kit/abstract'; +import type {BoundingRectangle} from '@dnd-kit/geometry'; +import {derived, effect, reactive} from '@dnd-kit/state'; + +import {DragDropManager} from '../manager'; +import {DOMRectangle} from '../shapes'; + +export class DragSourceDeltaModifier extends Modifier { + constructor(manager: DragDropManager) { + super(manager); + + this.destroy = effect(() => { + const {source, initialized} = manager.dragOperation; + + if (initialized) { + if (source?.element) { + const {boundingRectangle} = new DOMRectangle(source.element); + + if (!this.initialBoundingRectangle) { + this.initialBoundingRectangle = boundingRectangle; + } + + this.currentBoundingRectangle = boundingRectangle; + } + } else { + this.initialBoundingRectangle = null; + this.currentBoundingRectangle = null; + } + }); + } + + @reactive + private initialBoundingRectangle: BoundingRectangle | null = null; + + @reactive + private currentBoundingRectangle: BoundingRectangle | null = null; + + @derived + private get boundingRectangleDelta() { + if (!this.initialBoundingRectangle || !this.currentBoundingRectangle) { + return null; + } + + return { + top: + this.currentBoundingRectangle.top - this.initialBoundingRectangle.top, + left: + this.currentBoundingRectangle.left - this.initialBoundingRectangle.left, + }; + } + + public apply({transform}: DragDropManager['dragOperation']) { + const {boundingRectangleDelta} = this; + + if (!boundingRectangleDelta) { + return transform; + } + + return { + x: transform.x - boundingRectangleDelta.left, + y: transform.y - boundingRectangleDelta.top, + }; + } +} diff --git a/packages/dom/src/modifiers/index.ts b/packages/dom/src/modifiers/index.ts new file mode 100644 index 00000000..8fdced5d --- /dev/null +++ b/packages/dom/src/modifiers/index.ts @@ -0,0 +1 @@ +export {DragSourceDeltaModifier} from './DragSourceDeltaModifier'; diff --git a/packages/dom/src/nodes/draggable/draggable.ts b/packages/dom/src/nodes/draggable/draggable.ts index 1c06d912..f2392fb5 100644 --- a/packages/dom/src/nodes/draggable/draggable.ts +++ b/packages/dom/src/nodes/draggable/draggable.ts @@ -7,7 +7,7 @@ import {reactive} from '@dnd-kit/state'; export interface Input extends DraggableInput {} -export type DraggableFeedback = 'none' | 'clone' | 'move'; +export type DraggableFeedback = 'none' | 'clone' | 'move' | 'placeholder'; export class Draggable extends AbstractDraggable { @reactive @@ -17,7 +17,7 @@ export class Draggable extends AbstractDraggable { public element: Element | undefined; @reactive - public feedback: boolean = true; + public feedback: string = 'placeholder'; constructor(input: Input) { super(input); diff --git a/packages/dom/src/plugins/feedback/DraggableClone.ts b/packages/dom/src/plugins/feedback/CloneFeedback.ts similarity index 79% rename from packages/dom/src/plugins/feedback/DraggableClone.ts rename to packages/dom/src/plugins/feedback/CloneFeedback.ts index 9ac793d4..f23c9607 100644 --- a/packages/dom/src/plugins/feedback/DraggableClone.ts +++ b/packages/dom/src/plugins/feedback/CloneFeedback.ts @@ -4,11 +4,11 @@ import {effect} from '@dnd-kit/state'; import {cloneElement} from '@dnd-kit/dom-utilities'; import type {DragDropManager} from '../../manager'; -import {createOverlay} from './DraggableOverlay'; +import {createOverlay} from './Overlay'; interface Options {} -export class DraggableClone extends Plugin { +export class CloneFeedback extends Plugin { public destroy: CleanupFunction; constructor(manager: DragDropManager, _options?: Options) { @@ -19,7 +19,12 @@ export class DraggableClone extends Plugin { const {status, source} = dragOperation; const isDragging = status === 'dragging'; - if (!isDragging || !source || !source.feedback || !source.element) { + if ( + !isDragging || + !source || + !source.element || + source.feedback !== 'clone' + ) { return; } diff --git a/packages/dom/src/plugins/feedback/DraggableOverlay.ts b/packages/dom/src/plugins/feedback/DraggableOverlay.ts deleted file mode 100644 index ef869028..00000000 --- a/packages/dom/src/plugins/feedback/DraggableOverlay.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {effect} from '@dnd-kit/state'; -import {InlineStyles, supportsViewTransition} from '@dnd-kit/dom-utilities'; -import type {CleanupFunction} from '@dnd-kit/types'; - -import {DragDropManager} from '../../manager'; -import {DOMRectangle} from '../../shapes'; - -const VIEW_TRANSITION_NAME = 'dnd-kit--drop-animation'; - -class DraggableOverlay extends HTMLElement { - private destroy: CleanupFunction; - - private dropAnimation: () => void; - - constructor(manager: DragDropManager, element: Element) { - super(); - - const {top, left, width, height} = element.getBoundingClientRect(); - - this.style.pointerEvents = 'none'; - this.style.setProperty('position', 'fixed'); - this.style.setProperty('top', `${top}px`); - this.style.setProperty('left', `${left}px`); - this.style.setProperty('width', `${width}px`); - this.style.setProperty('height', `${height}px`); - - this.destroy = effect(() => { - const {dragOperation} = manager; - const {position, status} = dragOperation; - const {x, y} = position.delta; - - if (status === 'dragging') { - this.style.setProperty('transform', `translate3d(${x}px, ${y}px, 0)`); - dragOperation.shape = new DOMRectangle(this); - } - }); - - const id = manager.dragOperation.source?.id; - - this.dropAnimation = () => { - requestAnimationFrame(() => { - if (supportsViewTransition(document)) { - const draggable = - id != null ? manager.registry.draggable.get(id) : null; - const element = draggable?.element; - const elementStyles = - element instanceof HTMLElement ? new InlineStyles(element) : null; - - this.style.setProperty('view-transition-name', VIEW_TRANSITION_NAME); - elementStyles?.set({ - visibility: 'hidden', - }); - - const transition = document.startViewTransition(() => { - super.remove(); - elementStyles?.set({ - visibility: 'visible', - viewTransitionName: VIEW_TRANSITION_NAME, - }); - }); - - transition.finished.then(() => { - elementStyles?.reset(); - }); - } - }); - }; - } - - remove() { - this.destroy(); - - if (supportsViewTransition(document)) { - this.dropAnimation(); - return; - } - - super.remove(); - } -} - -export function createOverlay( - manager: DragDropManager, - element: Element -): DraggableOverlay { - if (customElements.get('draggable-overlay') == null) { - customElements.define('draggable-overlay', DraggableOverlay); - } - - return new DraggableOverlay(manager, element); -} diff --git a/packages/dom/src/plugins/feedback/Overlay.ts b/packages/dom/src/plugins/feedback/Overlay.ts new file mode 100644 index 00000000..ee0b89a8 --- /dev/null +++ b/packages/dom/src/plugins/feedback/Overlay.ts @@ -0,0 +1,150 @@ +import {effect} from '@dnd-kit/state'; +import {InlineStyles, supportsViewTransition} from '@dnd-kit/dom-utilities'; +import type {CleanupFunction} from '@dnd-kit/types'; + +import {DragDropManager} from '../../manager'; +import {DOMRectangle} from '../../shapes'; + +const VIEW_TRANSITION_NAME = 'dnd-kit--drop-animation'; + +class Overlay extends HTMLElement { + private destroy: CleanupFunction; + + private dropAnimation: () => void; + private transform = { + x: 0, + y: 0, + }; + + constructor( + private manager: DragDropManager, + element: Element + ) { + super(); + + const {top, left, width, height} = element.getBoundingClientRect(); + + this.style.pointerEvents = 'none'; + this.style.setProperty('position', 'fixed'); + this.style.setProperty('top', `${top}px`); + this.style.setProperty('left', `${left}px`); + this.style.setProperty('width', `${width}px`); + this.style.setProperty('height', `${height}px`); + + const effectCleanup = effect(() => { + const {dragOperation} = manager; + const {initialized, transform} = dragOperation; + const {x, y} = transform; + + if (initialized) { + this.style.setProperty('transform', `translate3d(${x}px, ${y}px, 0)`); + this.transform = transform; + + if (this.isConnected) { + dragOperation.shape = new DOMRectangle(this); + } + } + }); + + this.destroy = () => { + effectCleanup(); + }; + + const id = manager.dragOperation.source?.id; + + this.dropAnimation = () => { + if (manager.dragOperation.status === 'dragging') { + super.remove(); + return; + } + + requestAnimationFrame(() => { + const draggable = + id != null ? manager.registry.draggable.get(id) : null; + const element = draggable?.element; + const elementStyles = + element instanceof HTMLElement ? new InlineStyles(element) : null; + const onFinish = () => { + elementStyles?.reset(); + super.remove(); + }; + + elementStyles?.set({ + visibility: 'hidden', + }); + + if (supportsViewTransition(document)) { + this.style.setProperty('view-transition-name', VIEW_TRANSITION_NAME); + + const transition = document.startViewTransition(() => { + super.remove(); + elementStyles?.set({ + visibility: 'visible', + viewTransitionName: VIEW_TRANSITION_NAME, + }); + }); + + transition.finished.then(() => { + elementStyles?.reset(); + }); + return; + } + + if (element) { + const {top, left} = this.getBoundingClientRect(); + const {top: elementTop, left: elementLeft} = + element.getBoundingClientRect(); + const delta = { + x: left - elementLeft, + y: top - elementTop, + }; + const finalTransform = { + x: this.transform.x - delta.x, + y: this.transform.y - delta.y, + }; + + if ( + this.transform.x !== finalTransform.x || + this.transform.y !== finalTransform.y + ) { + this.animate( + { + transform: [ + this.style.transform, + `translate3d(${finalTransform.x}px, ${finalTransform.y}px, 0)`, + ], + }, + { + duration: 250, + easing: 'ease', + } + ).finished.then(onFinish); + return; + } + } + + onFinish(); + }); + }; + } + + connectedCallback() { + this.manager.dragOperation.shape = new DOMRectangle(this); + } + + remove() { + this.destroy(); + this.dropAnimation(); + } +} + +export function createOverlay( + manager: DragDropManager, + element: Element +): Overlay { + if (customElements.get('draggable-overlay') == null) { + customElements.define('draggable-overlay', Overlay); + } + + return new Overlay(manager, element); +} diff --git a/packages/dom/src/plugins/feedback/DraggablePlaceholder.ts b/packages/dom/src/plugins/feedback/PlaceholderFeedback.ts similarity index 72% rename from packages/dom/src/plugins/feedback/DraggablePlaceholder.ts rename to packages/dom/src/plugins/feedback/PlaceholderFeedback.ts index c9dcec16..86fd4d47 100644 --- a/packages/dom/src/plugins/feedback/DraggablePlaceholder.ts +++ b/packages/dom/src/plugins/feedback/PlaceholderFeedback.ts @@ -1,13 +1,14 @@ import {Plugin} from '@dnd-kit/abstract'; import type {CleanupFunction} from '@dnd-kit/types'; import {effect} from '@dnd-kit/state'; +import {cloneElement} from '@dnd-kit/dom-utilities'; import type {DragDropManager} from '../../manager'; -import {createOverlay} from './DraggableOverlay'; +import {createOverlay} from './Overlay'; interface Options {} -export class DraggablePlaceholder extends Plugin { +export class PlaceholderFeedback extends Plugin { public destroy: CleanupFunction; constructor(manager: DragDropManager, _options?: Options) { @@ -18,7 +19,12 @@ export class DraggablePlaceholder extends Plugin { const {status, source} = dragOperation; const isDragging = status === 'dragging'; - if (!isDragging || !source || !source.feedback || !source.element) { + if ( + !isDragging || + !source || + !source.element || + source.feedback !== 'placeholder' + ) { return; } @@ -31,11 +37,13 @@ export class DraggablePlaceholder extends Plugin { placeholder.style.height = `${height}px`; element.replaceWith(placeholder); - overlay.appendChild(element); document.body.appendChild(overlay); + console.log('mount overlay'); return () => { + const clone = cloneElement(element); + element.replaceWith(clone); placeholder.replaceWith(element); overlay.remove(); }; diff --git a/packages/dom/src/plugins/feedback/index.ts b/packages/dom/src/plugins/feedback/index.ts index 71bdcc1b..06251786 100644 --- a/packages/dom/src/plugins/feedback/index.ts +++ b/packages/dom/src/plugins/feedback/index.ts @@ -1,2 +1,2 @@ -export {DraggableClone} from './DraggableClone'; -export {DraggablePlaceholder} from './DraggablePlaceholder'; +export {CloneFeedback} from './CloneFeedback'; +export {PlaceholderFeedback} from './PlaceholderFeedback'; diff --git a/packages/dom/src/plugins/index.ts b/packages/dom/src/plugins/index.ts index c33991e9..ef38a955 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 {CloneFeedback, PlaceholderFeedback} from './feedback'; export {AutoScroller, Scroller, ScrollManager} from './scrolling'; diff --git a/packages/dom/src/sensors/keyboard/KeyboardSensor.ts b/packages/dom/src/sensors/keyboard/KeyboardSensor.ts index f1c5b205..161c981b 100644 --- a/packages/dom/src/sensors/keyboard/KeyboardSensor.ts +++ b/packages/dom/src/sensors/keyboard/KeyboardSensor.ts @@ -119,11 +119,13 @@ export class KeyboardSensor extends Sensor< return; } - if (!source.element) { + const {shape} = this.manager.dragOperation; + + if (!shape) { return; } - const {center} = new DOMRectangle(source.element); + const {center} = shape; const factor = event.shiftKey ? 5 : 1; const offset = { x: 0, diff --git a/packages/react/src/context/DndContext.tsx b/packages/react/src/context/DndContext.tsx index ea8cce25..c298e3e7 100644 --- a/packages/react/src/context/DndContext.tsx +++ b/packages/react/src/context/DndContext.tsx @@ -8,20 +8,24 @@ import {DragDropContext} from './context'; export interface Props { onDragStart?(event: DragDropEvents['dragstart']): void; + onDragOver?(event: DragDropEvents['dragover']): void; onDragEnd?(event: DragDropEvents['dragend']): void; } export function DndContext({ children, onDragStart, + onDragOver, onDragEnd, }: PropsWithChildren) { const manager = useConstant(() => new DragDropManager()); const handleDragStart = useEvent(onDragStart); + const handleDragOver = useEvent(onDragOver); const handleDragEnd = useEvent(onDragEnd); useEffect(() => { manager.monitor.addEventListener('dragstart', handleDragStart); + manager.monitor.addEventListener('dragover', handleDragOver); manager.monitor.addEventListener('dragend', handleDragEnd); return () => { diff --git a/packages/react/src/draggable/useDraggable.ts b/packages/react/src/draggable/useDraggable.ts index a76612a9..7f526220 100644 --- a/packages/react/src/draggable/useDraggable.ts +++ b/packages/react/src/draggable/useDraggable.ts @@ -4,6 +4,7 @@ import type {DragDropManager, DraggableInput} from '@dnd-kit/dom'; import {useDndContext} from '../context'; import {useComputed, useConstant, useIsomorphicLayoutEffect} from '../hooks'; +import {useConnectSensors} from '../sensors'; import {getCurrentValue, type RefOrValue} from '../utilities'; export interface UseDraggableInput @@ -47,24 +48,12 @@ export function useDraggable( }; }, [manager]); - useIsomorphicLayoutEffect(() => { - const unbindFunctions = sensors.map(({sensor}) => { - const sensorInstance = - manager.sensors.get(sensor) ?? manager.sensors.register(sensor); - - const unbind = sensorInstance.bind(draggable, manager); - return unbind; - }); - - return function cleanup() { - unbindFunctions.forEach((unbind) => unbind()); - }; - }, [sensors, manager, draggable]); - useIsomorphicLayoutEffect(() => { draggable.disabled = Boolean(disabled); }, [disabled]); + useConnectSensors(sensors, manager, draggable); + return { get isDragging() { return isDragging.value; diff --git a/packages/react/src/sensors/index.ts b/packages/react/src/sensors/index.ts new file mode 100644 index 00000000..e9e87a3b --- /dev/null +++ b/packages/react/src/sensors/index.ts @@ -0,0 +1 @@ +export {useConnectSensors} from './useConnectSensors'; diff --git a/packages/react/src/sensors/useConnectSensors.ts b/packages/react/src/sensors/useConnectSensors.ts new file mode 100644 index 00000000..1057ddec --- /dev/null +++ b/packages/react/src/sensors/useConnectSensors.ts @@ -0,0 +1,23 @@ +import type {DragDropManager, Draggable} from '@dnd-kit/dom'; +import {useIsomorphicLayoutEffect} from '../hooks'; +import {SensorDescriptor} from '@dnd-kit/abstract'; + +export function useConnectSensors( + sensors: SensorDescriptor[], + manager: DragDropManager, + draggable: Draggable +) { + useIsomorphicLayoutEffect(() => { + const unbindFunctions = sensors.map(({sensor}) => { + const sensorInstance = + manager.sensors.get(sensor) ?? manager.sensors.register(sensor); + + const unbind = sensorInstance.bind(draggable, manager); + return unbind; + }); + + return function cleanup() { + unbindFunctions.forEach((unbind) => unbind()); + }; + }, [sensors, manager, draggable]); +} diff --git a/packages/state/src/decorators.ts b/packages/state/src/decorators.ts index fb4e4731..3a079d78 100644 --- a/packages/state/src/decorators.ts +++ b/packages/state/src/decorators.ts @@ -29,57 +29,6 @@ export function reactive(target: Object, propertyKey: string) { }); } -// export function reactive( -// _value: ClassAccessorDecoratorTarget, -// context: ClassAccessorDecoratorContext -// ): ClassAccessorDecoratorResult | void { -// const {kind, name} = context; -// const propertyKey = `#__${String(name)}`; - -// if (kind === 'accessor') { -// let state: Signal; - -// return { -// get(this: This): Value { -// console.log(`Getting ${String(name)}`); -// return state.value; -// }, - -// set(this: This, val: Value) { -// console.log(`Setting ${String(name)} to ${val}`); -// state.value = val; -// return val; -// }, - -// init(this: This, initialValue: Value) { -// console.log(`Initializing ${String(name)} with value ${initialValue}`); -// state = signal(initialValue); -// return initialValue; -// }, -// }; -// } -// } - -// export function derived(instance: Value, key: keyof Value) { -// // const {name, kind} = context; -// // const propertyKey = `#__${String(name)}`; -// // console.log(originalMethod, context, args, name, kind); -// // context.addInitializer(function () { -// // const computedSignal = computed(originalMethod.bind(this)); -// // Object.defineProperty(this, propertyKey, { -// // get() { -// // return computedSignal; -// // }, -// // }); -// // }); -// // function replacementMethod(this: any) { -// // return this[propertyKey].value; -// // } -// // return replacementMethod; - -// console.log(instance, key); -// } - export function derived( target: Object, propertyKey: string,