Skip to content

Commit

Permalink
feat(core): dnd support external types (#9033)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
pengx17 committed Dec 6, 2024
1 parent 6b14e1c commit fafacdb
Show file tree
Hide file tree
Showing 17 changed files with 285 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
diff --git a/dist/esm/adapter/external-adapter.js b/dist/esm/adapter/external-adapter.js
index ef7a963d91f08c9e70c8ed9c6b41972bec349319..e682841ec10a4a8a9ce7a79642e58de5c9e664d5 100644
--- a/dist/esm/adapter/external-adapter.js
+++ b/dist/esm/adapter/external-adapter.js
@@ -54,9 +54,11 @@ var adapter = makeAdapter({
type: 'dragenter',
listener: function listener(event) {
// drag operation was started within the document, it won't be an "external" drag
- if (didDragStartLocally) {
- return;
- }
+
+ // we will handle all events actually
+ // if (didDragStartLocally) {
+ // return;
+ // }

// Note: not checking if event was cancelled (`event.defaultPrevented`) as
// cancelling a "dragenter" accepts the drag operation (not prevent it)
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"macos-alias": "npm:@napi-rs/[email protected]",
"fs-xattr": "npm:@napi-rs/xattr@latest",
"vite": "6.0.2",
"decode-named-character-reference@npm:^1.0.0": "patch:decode-named-character-reference@npm%3A1.0.2#~/.yarn/patches/decode-named-character-reference-npm-1.0.2-db17a755fd.patch"
"decode-named-character-reference@npm:^1.0.0": "patch:decode-named-character-reference@npm%3A1.0.2#~/.yarn/patches/decode-named-character-reference-npm-1.0.2-db17a755fd.patch",
"@atlaskit/pragmatic-drag-and-drop@npm:^1.1.0": "patch:@atlaskit/pragmatic-drag-and-drop@npm%3A1.4.0#~/.yarn/patches/@atlaskit-pragmatic-drag-and-drop-npm-1.4.0-75c45f52d3.patch"
}
}
2 changes: 1 addition & 1 deletion packages/frontend/component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@affine/electron-api": "workspace:*",
"@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*",
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
"@atlaskit/pragmatic-drag-and-drop": "patch:@atlaskit/pragmatic-drag-and-drop@npm%3A1.4.0#~/.yarn/patches/@atlaskit-pragmatic-drag-and-drop-npm-1.4.0-75c45f52d3.patch",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blocksuite/icons": "2.1.75",
"@emotion/react": "^11.11.4",
Expand Down
9 changes: 8 additions & 1 deletion packages/frontend/component/src/ui/dnd/dnd.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
);
Expand All @@ -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>
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/component/src/ui/dnd/draggable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
137 changes: 105 additions & 32 deletions packages/frontend/component/src/ui/dnd/drop-target.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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] & {
Expand All @@ -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>
Expand All @@ -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(
Expand Down Expand Up @@ -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>;
Expand All @@ -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>(
Expand All @@ -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
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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() {
Expand Down
14 changes: 14 additions & 0 deletions packages/frontend/core/src/modules/dnd/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
DocsService,
type Framework,
WorkspaceScope,
WorkspaceService,
} from '@toeverything/infra';

import { DndService } from './services';

export function configureDndModule(framework: Framework) {
framework
.scope(WorkspaceScope)
.service(DndService, [DocsService, WorkspaceService]);
}
Loading

0 comments on commit fafacdb

Please sign in to comment.