diff --git a/.changeset/small-plants-sparkle.md b/.changeset/small-plants-sparkle.md new file mode 100644 index 000000000..80a720e1e --- /dev/null +++ b/.changeset/small-plants-sparkle.md @@ -0,0 +1,6 @@ +--- +'@dnd-kit/abstract': minor +'@dnd-kit/dom': minor +--- + +feat: Add a plugin to allow dragging file from native file system diff --git a/packages/abstract/src/core/collision/observer.ts b/packages/abstract/src/core/collision/observer.ts index 92cafab0c..d564a31dc 100644 --- a/packages/abstract/src/core/collision/observer.ts +++ b/packages/abstract/src/core/collision/observer.ts @@ -71,9 +71,13 @@ export class CollisionObserver< collisionDetector?: CollisionDetector ) { const {registry, dragOperation} = this.manager; - const {source, shape, status} = dragOperation; + const {source, status, position, shape} = dragOperation; - if (!status.initialized || !shape) { + // Make sure effects will re-run when those properties change + void position.current; + void shape; + + if (!status.initialized) { return DEFAULT_VALUE; } @@ -99,7 +103,7 @@ export class CollisionObserver< // Force collisions to be recomputed when the shape changes void entry.shape; - + void dragOperation.position.current; const collision = untracked(() => detectCollision({ droppable: entry, diff --git a/packages/abstract/src/core/manager/dragOperation.ts b/packages/abstract/src/core/manager/dragOperation.ts index fadfb9a3f..4e7afb66e 100644 --- a/packages/abstract/src/core/manager/dragOperation.ts +++ b/packages/abstract/src/core/manager/dragOperation.ts @@ -106,7 +106,7 @@ export function DragOperationManager< }); const target = computed(() => { const identifier = targetIdentifier.value; - return identifier != null ? (droppables.get(identifier) ?? null) : null; + return identifier != null ? droppables.get(identifier) ?? null : null; }); const modifiers = signal([]); diff --git a/packages/dom/package.json b/packages/dom/package.json index adc4d8454..3c3b1e499 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -63,13 +63,14 @@ } }, "scripts": { - "build": "bun build:utilities && bun build:core && bun build:sortable && bun build:modifiers && bun build:plugins", + "build": "bun build:utilities && bun build:core && bun build:sortable && bun build:draggableFile && bun build:modifiers && bun build:plugins", "build:core": "tsup src/core/index.ts", "build:modifiers": "tsup --entry.modifiers src/modifiers/index.ts", "build:plugins": "tsup --entry.debug src/plugins/debug/index.ts --outDir ./plugins", + "build:draggableFile": "tsup --entry.draggableFile src/plugins/draggableFile/index.ts --outDir ./plugins", "build:sortable": "tsup --entry.sortable src/sortable/index.ts", "build:utilities": "tsup --entry.utilities src/utilities/index.ts", - "dev": "bun build:utilities --watch & bun build:core --watch & bun build:sortable --watch & bun build:modifiers --watch & bun build:plugins --watch", + "dev": "bun build:utilities --watch & bun build:core --watch & bun build:sortable --watch & bun build:modifiers --watch & bun build:plugins --watch & bun build:draggableFile --watch", "lint": "TIMING=1 eslint src/**/*.ts* --fix", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, diff --git a/packages/dom/src/plugins/draggableFile/draggableFile.ts b/packages/dom/src/plugins/draggableFile/draggableFile.ts new file mode 100644 index 000000000..87ac87e6e --- /dev/null +++ b/packages/dom/src/plugins/draggableFile/draggableFile.ts @@ -0,0 +1,104 @@ +import {batch, CleanupFunction} from '@dnd-kit/state'; +import {Plugin} from '@dnd-kit/abstract'; +import {Draggable, type DragDropManager} from '@dnd-kit/dom'; +import {getDocument, Listeners} from '@dnd-kit/dom/utilities'; + +export const DraggableFileId = '$DRAGGABLE_FILE_GLOBALLY_UNIQUE_ID'; + +export class DraggableFile extends Plugin { + private listeners = new Listeners(); + + private cleanup: Set = new Set(); + + constructor(manager: DragDropManager) { + super(manager); + console.log('DraggableFile constructor'); + + const draggable = new Draggable({id: DraggableFileId}, manager); + + const unbind = this.listeners.bind(document.body, [ + {type: 'dragenter', listener: this.handleDragEnter.bind(this)}, + ]); + + this.destroy = () => { + this.cleanup.forEach((cleanup) => cleanup()); + this.cleanup.clear(); + unbind(); + draggable.destroy(); + }; + } + + private handleDragEnter(event: DragEvent) { + const isDraggingFile = (event.dataTransfer?.types ?? []).includes('Files'); + if (this.disabled || event.relatedTarget || !isDraggingFile) { + return; + } + event.preventDefault(); + event.stopImmediatePropagation(); + // console.log('handleDragEnter', event); + + batch(() => { + this.manager.actions.setDragSource(DraggableFileId); + this.manager.actions.start({ + coordinates: {x: event.clientX, y: event.clientY}, + event, + }); + }); + + const ownerDocument = getDocument(event.target); + + const unbindListeners = this.listeners.bind(ownerDocument, [ + {type: 'dragover', listener: this.handleDragOver.bind(this)}, + {type: 'dragleave', listener: this.handleDragLeave.bind(this)}, + {type: 'drop', listener: this.handleDrop.bind(this)}, + ]); + + const cleanup = () => { + setTimeout(unbindListeners); + }; + + this.cleanup.add(cleanup); + } + + private handleDragLeave(event: DragEvent) { + if (event.relatedTarget) { + return; + } + // Prevent the default behaviour of the event + event.preventDefault(); + event.stopPropagation(); + + // console.log('handleDragLeave', event); + + // End the drag and drop operation + + this.manager.actions.stop({canceled: true}); + + // Remove the pointer move and up event listeners + this.cleanup.forEach((cleanup) => cleanup()); + this.cleanup.clear(); + } + + private handleDragOver(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + + // console.log('handleDragOver', event); + + this.manager.actions.move({to: {x: event.clientX, y: event.clientY}}); + } + + private handleDrop(event: DragEvent) { + // Prevent the default behaviour of the event + event.preventDefault(); + event.stopPropagation(); + + // End the drag and drop operation + + this.manager.actions.stop(); + + // Remove the pointer move and up event listeners + this.cleanup.forEach((cleanup) => cleanup()); + this.cleanup.clear(); + } +} diff --git a/packages/dom/src/plugins/draggableFile/index.ts b/packages/dom/src/plugins/draggableFile/index.ts new file mode 100644 index 000000000..473dedafa --- /dev/null +++ b/packages/dom/src/plugins/draggableFile/index.ts @@ -0,0 +1 @@ +export {DraggableFile, DraggableFileId} from './draggableFile.ts';