mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(core): allow bs snapshot dragging targets (#9093)
fix AF-1924, AF-1848, AF-1928, AF-1931
dnd between affine & editor
<div class='graphite__hidden'>
<div>🎥 Video uploaded on Graphite:</div>
<a href="https://app.graphite.dev/media/video/T2klNLEk0wxLh4NRDzhk/dff3ceb1-dc82-4222-9b55-13be80b28b2f.mp4">
<img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/T2klNLEk0wxLh4NRDzhk/dff3ceb1-dc82-4222-9b55-13be80b28b2f.mp4">
</a>
</div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/dff3ceb1-dc82-4222-9b55-13be80b28b2f.mp4">20241210-1217-49.8960381.mp4</video>
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { DNDData, ExternalDataAdapter } from './types';
|
||||
import type { DNDData, fromExternalData, toExternalData } from './types';
|
||||
|
||||
export const DNDContext = createContext<{
|
||||
/**
|
||||
* external data adapter.
|
||||
* Convert the external data to the draggable data that are known to affine.
|
||||
*
|
||||
* if this is provided, the drop target will handle external elements as well.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
externalDataAdapter?: ExternalDataAdapter<DNDData>;
|
||||
fromExternalData?: fromExternalData<DNDData>;
|
||||
|
||||
/**
|
||||
* Convert the draggable data to the external data.
|
||||
* Mainly used to be consumed by blocksuite.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
toExternalData?: toExternalData<DNDData>;
|
||||
}>({});
|
||||
|
||||
@@ -92,7 +92,7 @@ export const DropTarget: StoryFn<{ canDrop: boolean }> = ({ canDrop }) => {
|
||||
onDrop(data) {
|
||||
setDropData(prev => prev + data.source.data.text);
|
||||
},
|
||||
externalDataAdapter(args) {
|
||||
fromExternalData(args) {
|
||||
return {
|
||||
text: args.source.getStringData(args.source.types[0]) || 'no value',
|
||||
};
|
||||
|
||||
@@ -5,42 +5,21 @@ import { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/eleme
|
||||
import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
|
||||
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
||||
import type { DropTargetRecord } from '@atlaskit/pragmatic-drag-and-drop/types';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import ReactDOM, { flushSync } from 'react-dom';
|
||||
|
||||
import type { DNDData } from './types';
|
||||
|
||||
type DraggableGetFeedback = Parameters<
|
||||
NonNullable<Parameters<typeof draggable>[0]['getInitialData']>
|
||||
>[0];
|
||||
|
||||
type DraggableGet<T> = T | ((data: DraggableGetFeedback) => T);
|
||||
|
||||
function draggableGet<T>(
|
||||
get: T
|
||||
): T extends undefined
|
||||
? undefined
|
||||
: T extends DraggableGet<infer I>
|
||||
? (args: DraggableGetFeedback) => I
|
||||
: never {
|
||||
if (get === undefined) {
|
||||
return undefined as any;
|
||||
}
|
||||
return ((args: DraggableGetFeedback) =>
|
||||
typeof get === 'function' ? (get as any)(args) : get) as any;
|
||||
}
|
||||
import { DNDContext } from './context';
|
||||
import {
|
||||
type DNDData,
|
||||
type DraggableGet,
|
||||
draggableGet,
|
||||
type DraggableGetFeedback,
|
||||
type toExternalData,
|
||||
} from './types';
|
||||
|
||||
export interface DraggableOptions<D extends DNDData = DNDData> {
|
||||
data?: DraggableGet<D['draggable']>;
|
||||
dataForExternal?: DraggableGet<{
|
||||
[Key in
|
||||
| 'text/uri-list'
|
||||
| 'text/plain'
|
||||
| 'text/html'
|
||||
| 'Files'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
| (string & {})]?: string;
|
||||
}>;
|
||||
toExternalData?: toExternalData<D>;
|
||||
canDrag?: DraggableGet<boolean>;
|
||||
disableDragPreview?: boolean;
|
||||
dragPreviewPosition?: DraggableDragPreviewPosition;
|
||||
@@ -82,8 +61,23 @@ export const useDraggable = <D extends DNDData = DNDData>(
|
||||
const enableDropTarget = useRef(false);
|
||||
const enableDragging = useRef(false);
|
||||
|
||||
const context = useContext(DNDContext);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const options = useMemo(getOptions, deps);
|
||||
const options = useMemo(() => {
|
||||
const opts = getOptions();
|
||||
|
||||
const toExternalData = opts.toExternalData ?? context.toExternalData;
|
||||
return {
|
||||
...opts,
|
||||
toExternalData: toExternalData
|
||||
? (args: DraggableGetFeedback) => {
|
||||
return (opts.toExternalData ?? toExternalData)(args, opts.data);
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...deps, context.toExternalData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dragRef.current) {
|
||||
@@ -110,7 +104,7 @@ export const useDraggable = <D extends DNDData = DNDData>(
|
||||
dragHandle: dragHandleRef.current ?? undefined,
|
||||
canDrag: draggableGet(options.canDrag),
|
||||
getInitialData: draggableGet(options.data),
|
||||
getInitialDataForExternal: draggableGet(options.dataForExternal),
|
||||
getInitialDataForExternal: draggableGet(options.toExternalData),
|
||||
onDragStart: args => {
|
||||
if (enableDragging.current) {
|
||||
setDragging(true);
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { DNDContext } from './context';
|
||||
import type { DNDData, ExternalDataAdapter } from './types';
|
||||
import type { DNDData, fromExternalData } from './types';
|
||||
|
||||
export type DropTargetDropEvent<D extends DNDData> = {
|
||||
treeInstruction: Instruction | null;
|
||||
@@ -74,8 +74,8 @@ const getAdaptedEventArgs = <
|
||||
isDropEvent = false
|
||||
): Args => {
|
||||
const data =
|
||||
isExternalDrag(args) && options.externalDataAdapter
|
||||
? options.externalDataAdapter(
|
||||
isExternalDrag(args) && options.fromExternalData
|
||||
? options.fromExternalData(
|
||||
// @ts-expect-error hack for external data adapter (source has no data field)
|
||||
args as ExternalGetDataFeedbackArgs,
|
||||
isDropEvent
|
||||
@@ -172,10 +172,10 @@ export interface DropTargetOptions<D extends DNDData = DNDData> {
|
||||
* external data adapter.
|
||||
* Will use the external data adapter from the context if not provided.
|
||||
*/
|
||||
externalDataAdapter?: ExternalDataAdapter<D>;
|
||||
fromExternalData?: fromExternalData<D>;
|
||||
/**
|
||||
* Make the drop target allow external data.
|
||||
* If this is undefined, it will be set to true if externalDataAdapter is provided.
|
||||
* If this is undefined, it will be set to true if fromExternalData is provided.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
@@ -217,17 +217,17 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
|
||||
const options = useMemo(() => {
|
||||
const opts = getOptions();
|
||||
const allowExternal = opts.allowExternal ?? !!opts.externalDataAdapter;
|
||||
const allowExternal = opts.allowExternal ?? !!opts.fromExternalData;
|
||||
return {
|
||||
...opts,
|
||||
allowExternal,
|
||||
externalDataAdapter: allowExternal
|
||||
? (opts.externalDataAdapter ??
|
||||
(dropTargetContext.externalDataAdapter as ExternalDataAdapter<D>))
|
||||
fromExternalData: allowExternal
|
||||
? (opts.fromExternalData ??
|
||||
(dropTargetContext.fromExternalData as fromExternalData<D>))
|
||||
: undefined,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...deps, dropTargetContext.externalDataAdapter]);
|
||||
}, [...deps, dropTargetContext.fromExternalData]);
|
||||
|
||||
const dropTargetOptions = useMemo(() => {
|
||||
const wrappedCanDrop = dropTargetGet(options.canDrop, options);
|
||||
@@ -240,7 +240,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
// check if args has data. if not, it's an external drag
|
||||
// we always allow external drag since the data is only
|
||||
// available in drop event
|
||||
if (isExternalDrag(args) && options.externalDataAdapter) {
|
||||
if (isExternalDrag(args) && options.fromExternalData) {
|
||||
return true;
|
||||
}
|
||||
return wrappedCanDrop(args);
|
||||
@@ -249,20 +249,6 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
getDropEffect: dropTargetGet(options.dropEffect, options),
|
||||
getIsSticky: dropTargetGet(options.isSticky, options),
|
||||
onDrop: (_args: DropTargetDropEvent<D>) => {
|
||||
// external data is only available in drop event thus
|
||||
// this is the only case for getAdaptedEventArgs
|
||||
const args = getAdaptedEventArgs(options, _args, true);
|
||||
if (
|
||||
isExternalDrag(_args) &&
|
||||
options.externalDataAdapter &&
|
||||
typeof options.canDrop === 'function' &&
|
||||
// there is a small flaw that canDrop called in onDrop misses
|
||||
// `input and `element` arguments
|
||||
!options.canDrop(args as any)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableDraggedOver.current) {
|
||||
setDraggedOver(false);
|
||||
}
|
||||
@@ -292,6 +278,21 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
if (dropTargetRef.current) {
|
||||
delete dropTargetRef.current.dataset['draggedOver'];
|
||||
}
|
||||
|
||||
// external data is only available in drop event thus
|
||||
// this is the only case for getAdaptedEventArgs
|
||||
const args = getAdaptedEventArgs(options, _args, true);
|
||||
if (
|
||||
isExternalDrag(_args) &&
|
||||
options.fromExternalData &&
|
||||
typeof options.canDrop === 'function' &&
|
||||
// there is a small flaw that canDrop called in onDrop misses
|
||||
// `input and `element` arguments
|
||||
!options.canDrop(args as any)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
args.location.current.dropTargets[0]?.element ===
|
||||
dropTargetRef.current
|
||||
@@ -451,11 +452,11 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
||||
}, [dropTargetOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dropTargetRef.current || !options.externalDataAdapter) {
|
||||
if (!dropTargetRef.current || !options.fromExternalData) {
|
||||
return;
|
||||
}
|
||||
return dropTargetForExternal(dropTargetOptions as any);
|
||||
}, [dropTargetOptions, options.externalDataAdapter]);
|
||||
}, [dropTargetOptions, options.fromExternalData]);
|
||||
|
||||
return {
|
||||
dropTargetRef,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import type { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||
|
||||
export interface DNDData<
|
||||
@@ -12,7 +13,44 @@ export type ExternalGetDataFeedbackArgs = Parameters<
|
||||
NonNullable<Parameters<typeof dropTargetForExternal>[0]['getData']>
|
||||
>[0];
|
||||
|
||||
export type ExternalDataAdapter<D extends DNDData> = (
|
||||
export type fromExternalData<D extends DNDData> = (
|
||||
args: ExternalGetDataFeedbackArgs,
|
||||
isDropEvent?: boolean
|
||||
) => D['draggable'];
|
||||
|
||||
export type DraggableGetFeedback = Parameters<
|
||||
NonNullable<Parameters<typeof draggable>[0]['getInitialData']>
|
||||
>[0];
|
||||
|
||||
type DraggableGetFeedbackArgs = Parameters<
|
||||
NonNullable<Parameters<typeof draggable>[0]['getInitialData']>
|
||||
>[0];
|
||||
|
||||
export function draggableGet<T>(
|
||||
get: T
|
||||
): T extends undefined
|
||||
? undefined
|
||||
: T extends DraggableGet<infer I>
|
||||
? (args: DraggableGetFeedback) => I
|
||||
: never {
|
||||
if (get === undefined) {
|
||||
return undefined as any;
|
||||
}
|
||||
return ((args: DraggableGetFeedback) =>
|
||||
typeof get === 'function' ? (get as any)(args) : get) as any;
|
||||
}
|
||||
|
||||
export type DraggableGet<T> = T | ((data: DraggableGetFeedback) => T);
|
||||
|
||||
export type toExternalData<D extends DNDData> = (
|
||||
args: DraggableGetFeedbackArgs,
|
||||
data?: DraggableGet<D['draggable']>
|
||||
) => {
|
||||
[Key in
|
||||
| 'text/uri-list'
|
||||
| 'text/plain'
|
||||
| 'text/html'
|
||||
| 'Files'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
| (string & {})]?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user