mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
feat(core): dnd support external types (#9033)
fix AF-1847 two issues: 1. original `dropTargetForExternal` only works if dragging target is from another window context. patched the library to bypass this issue 2. `dataTransfer`'s content is only available on `drop` event. This means we cannot have `canDrop` checks for external elements (like blocksuite links).
This commit is contained in:
@@ -86,12 +86,17 @@ export const DropTarget: StoryFn<{ canDrop: boolean }> = ({ canDrop }) => {
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const { dropTargetRef } = useDropTarget(
|
||||
const { dropTargetRef } = useDropTarget<DNDData<{ text: string }>>(
|
||||
() => ({
|
||||
canDrop,
|
||||
onDrop(data) {
|
||||
setDropData(prev => prev + data.source.data.text);
|
||||
},
|
||||
externalDataAdapter(args) {
|
||||
return {
|
||||
text: args.source.getStringData(args.source.types[0]) || 'no value',
|
||||
};
|
||||
},
|
||||
}),
|
||||
[canDrop]
|
||||
);
|
||||
@@ -115,6 +120,8 @@ export const DropTarget: StoryFn<{ canDrop: boolean }> = ({ canDrop }) => {
|
||||
}`}
|
||||
</style>
|
||||
<div ref={dragRef}>👉 hello</div>
|
||||
<a href="https://www.google.com">https://www.google.com</a>
|
||||
<p>Some random texts</p>
|
||||
<div className="drop-target" ref={dropTargetRef}>
|
||||
{dropData || 'Drop here'}
|
||||
</div>
|
||||
|
||||
@@ -103,6 +103,8 @@ export const useDraggable = <D extends DNDData = DNDData>(
|
||||
},
|
||||
};
|
||||
|
||||
dragRef.current.dataset.affineDraggable = 'true';
|
||||
|
||||
const cleanupDraggable = draggable({
|
||||
element: dragRef.current,
|
||||
dragHandle: dragHandleRef.current ?? undefined,
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||
import type {
|
||||
DragLocationHistory,
|
||||
DropTargetRecord,
|
||||
ElementDragType,
|
||||
ExternalDragType,
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/types';
|
||||
import {
|
||||
attachClosestEdge,
|
||||
type Edge,
|
||||
@@ -14,6 +21,28 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { DNDData } from './types';
|
||||
|
||||
export type DropTargetDropEvent<D extends DNDData> = {
|
||||
treeInstruction: Instruction | null;
|
||||
closestEdge: Edge | null;
|
||||
/**
|
||||
* Location history for the drag operation
|
||||
*/
|
||||
location: DragLocationHistory;
|
||||
/**
|
||||
* Data associated with the entity that is being dragged
|
||||
*/
|
||||
source: Exclude<ElementDragType['payload'], 'data'> & {
|
||||
data: D['draggable'];
|
||||
};
|
||||
self: DropTargetRecord;
|
||||
};
|
||||
|
||||
export type DropTargetDragEvent<D extends DNDData> = DropTargetDropEvent<D>;
|
||||
|
||||
export type DropTargetTreeInstruction = Instruction;
|
||||
|
||||
export type ExternalDragPayload = ExternalDragType['payload'];
|
||||
|
||||
type DropTargetGetFeedback<D extends DNDData> = Parameters<
|
||||
NonNullable<Parameters<typeof dropTargetForElements>[0]['canDrop']>
|
||||
>[0] & {
|
||||
@@ -29,6 +58,36 @@ type DropTargetGet<T, D extends DNDData> =
|
||||
| T
|
||||
| ((data: DropTargetGetFeedback<D>) => T);
|
||||
|
||||
export type ExternalGetDataFeedbackArgs = Parameters<
|
||||
NonNullable<Parameters<typeof dropTargetForExternal>[0]['getData']>
|
||||
>[0];
|
||||
|
||||
export type ExternalDataAdapter<D extends DNDData> = (
|
||||
args: ExternalGetDataFeedbackArgs
|
||||
) => D['draggable'];
|
||||
|
||||
const getAdaptedEventArgs = <
|
||||
D extends DNDData,
|
||||
Args extends Pick<DropTargetGetFeedback<D>, 'source'>,
|
||||
>(
|
||||
options: DropTargetOptions<D>,
|
||||
args: Args
|
||||
): Args => {
|
||||
const data =
|
||||
!args.source['data'] && options.externalDataAdapter
|
||||
? // @ts-expect-error hack for external data adapter (source has no data field)
|
||||
options.externalDataAdapter(args as ExternalGetDataFeedbackArgs)
|
||||
: args.source['data'];
|
||||
|
||||
return {
|
||||
...args,
|
||||
source: {
|
||||
...args.source,
|
||||
data,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function dropTargetGet<T, D extends DNDData>(
|
||||
get: T,
|
||||
options: DropTargetOptions<D>
|
||||
@@ -42,12 +101,13 @@ function dropTargetGet<T, D extends DNDData>(
|
||||
if (get === undefined) {
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
return ((
|
||||
args: Omit<DropTargetGetFeedback<D>, 'treeInstruction' | 'closestEdge'>
|
||||
) => {
|
||||
if (typeof get === 'function') {
|
||||
return (get as any)({
|
||||
...args,
|
||||
...getAdaptedEventArgs(options, args),
|
||||
get treeInstruction() {
|
||||
return options.treeInstruction
|
||||
? extractInstruction(
|
||||
@@ -81,25 +141,14 @@ function dropTargetGet<T, D extends DNDData>(
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return get;
|
||||
return {
|
||||
...get,
|
||||
...getAdaptedEventArgs(options, args),
|
||||
};
|
||||
}
|
||||
}) as any;
|
||||
}
|
||||
|
||||
export type DropTargetDropEvent<D extends DNDData> = Parameters<
|
||||
NonNullable<Parameters<typeof dropTargetForElements>[0]['onDrop']>
|
||||
>[0] & { treeInstruction: Instruction | null; closestEdge: Edge | null } & {
|
||||
source: { data: D['draggable'] };
|
||||
};
|
||||
|
||||
export type DropTargetDragEvent<D extends DNDData> = Parameters<
|
||||
NonNullable<Parameters<typeof dropTargetForElements>[0]['onDrag']>
|
||||
>[0] & { treeInstruction: Instruction | null; closestEdge: Edge | null } & {
|
||||
source: { data: D['draggable'] };
|
||||
};
|
||||
|
||||
export type DropTargetTreeInstruction = Instruction;
|
||||
|
||||
export interface DropTargetOptions<D extends DNDData = DNDData> {
|
||||
data?: DropTargetGet<D['dropTarget'], D>;
|
||||
canDrop?: DropTargetGet<boolean, D>;
|
||||
@@ -116,6 +165,13 @@ export interface DropTargetOptions<D extends DNDData = DNDData> {
|
||||
};
|
||||
onDrop?: (data: DropTargetDropEvent<D>) => void;
|
||||
onDrag?: (data: DropTargetDragEvent<D>) => void;
|
||||
/**
|
||||
* external data adapter.
|
||||
* if this is provided, the drop target will handle external elements as well.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
externalDataAdapter?: ExternalDataAdapter<D>;
|
||||
}
|
||||
|
||||
export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
@@ -131,9 +187,9 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
const [dropEffect, setDropEffect] = useState<'copy' | 'link' | 'move' | null>(
|
||||
null
|
||||
);
|
||||
const [draggedOverDraggable, setDraggedOverDraggable] = useState<{
|
||||
data: D['draggable'];
|
||||
} | null>(null);
|
||||
const [draggedOverDraggable, setDraggedOverDraggable] = useState<
|
||||
DropTargetDropEvent<D>['source'] | null
|
||||
>(null);
|
||||
const [draggedOverPosition, setDraggedOverPosition] = useState<{
|
||||
/**
|
||||
* relative position to the drop target element top-left corner
|
||||
@@ -152,16 +208,16 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const options = useMemo(getOptions, deps);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dropTargetRef.current) {
|
||||
return;
|
||||
}
|
||||
return dropTargetForElements({
|
||||
element: dropTargetRef.current,
|
||||
const dropTargetOptions = useMemo(() => {
|
||||
return {
|
||||
get element() {
|
||||
return dropTargetRef.current;
|
||||
},
|
||||
canDrop: dropTargetGet(options.canDrop, options),
|
||||
getDropEffect: dropTargetGet(options.dropEffect, options),
|
||||
getIsSticky: dropTargetGet(options.isSticky, options),
|
||||
onDrop: args => {
|
||||
onDrop: (args: DropTargetDropEvent<D>) => {
|
||||
args = getAdaptedEventArgs(options, args);
|
||||
if (enableDraggedOver.current) {
|
||||
setDraggedOver(false);
|
||||
}
|
||||
@@ -202,7 +258,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
} as DropTargetDropEvent<D>);
|
||||
}
|
||||
},
|
||||
getData: args => {
|
||||
getData: (args: DropTargetGetFeedback<D>) => {
|
||||
args = getAdaptedEventArgs(options, args);
|
||||
const originData = dropTargetGet(options.data ?? {}, options)(args);
|
||||
const { input, element } = args;
|
||||
const withInstruction = options.treeInstruction
|
||||
@@ -224,13 +281,14 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
: withInstruction;
|
||||
return withClosestEdge;
|
||||
},
|
||||
onDrag: args => {
|
||||
onDrag: (args: DropTargetDragEvent<D>) => {
|
||||
args = getAdaptedEventArgs(options, args);
|
||||
if (
|
||||
args.location.current.dropTargets[0]?.element ===
|
||||
dropTargetRef.current
|
||||
) {
|
||||
if (enableDraggedOverDraggable.current) {
|
||||
setDraggedOverDraggable({ data: args.source.data });
|
||||
setDraggedOverDraggable(args.source);
|
||||
}
|
||||
let instruction = null;
|
||||
let closestEdge = null;
|
||||
@@ -266,7 +324,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
} as DropTargetDropEvent<D>);
|
||||
}
|
||||
},
|
||||
onDropTargetChange: args => {
|
||||
onDropTargetChange: (args: DropTargetDropEvent<D>) => {
|
||||
args = getAdaptedEventArgs(options, args);
|
||||
if (
|
||||
args.location.current.dropTargets[0]?.element ===
|
||||
dropTargetRef.current
|
||||
@@ -290,7 +349,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
setDropEffect(args.self.dropEffect);
|
||||
}
|
||||
if (enableDraggedOverDraggable.current) {
|
||||
setDraggedOverDraggable({ data: args.source.data });
|
||||
setDraggedOverDraggable(args.source);
|
||||
}
|
||||
if (enableDraggedOverPosition.current) {
|
||||
const rect = args.self.element.getBoundingClientRect();
|
||||
@@ -336,9 +395,23 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}, [options]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dropTargetRef.current) {
|
||||
return;
|
||||
}
|
||||
return dropTargetForElements(dropTargetOptions as any);
|
||||
}, [dropTargetOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dropTargetRef.current || !options.externalDataAdapter) {
|
||||
return;
|
||||
}
|
||||
return dropTargetForExternal(dropTargetOptions as any);
|
||||
}, [dropTargetOptions, options.externalDataAdapter]);
|
||||
|
||||
return {
|
||||
dropTargetRef,
|
||||
get draggedOver() {
|
||||
|
||||
Reference in New Issue
Block a user