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:
@@ -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)
|
||||||
@@ -157,6 +157,7 @@
|
|||||||
"macos-alias": "npm:@napi-rs/macos-alias@0.0.4",
|
"macos-alias": "npm:@napi-rs/macos-alias@0.0.4",
|
||||||
"fs-xattr": "npm:@napi-rs/xattr@latest",
|
"fs-xattr": "npm:@napi-rs/xattr@latest",
|
||||||
"vite": "6.0.2",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"@affine/electron-api": "workspace:*",
|
"@affine/electron-api": "workspace:*",
|
||||||
"@affine/graphql": "workspace:*",
|
"@affine/graphql": "workspace:*",
|
||||||
"@affine/i18n": "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",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||||
"@blocksuite/icons": "2.1.75",
|
"@blocksuite/icons": "2.1.75",
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
|
|||||||
@@ -86,12 +86,17 @@ export const DropTarget: StoryFn<{ canDrop: boolean }> = ({ canDrop }) => {
|
|||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const { dropTargetRef } = useDropTarget(
|
const { dropTargetRef } = useDropTarget<DNDData<{ text: string }>>(
|
||||||
() => ({
|
() => ({
|
||||||
canDrop,
|
canDrop,
|
||||||
onDrop(data) {
|
onDrop(data) {
|
||||||
setDropData(prev => prev + data.source.data.text);
|
setDropData(prev => prev + data.source.data.text);
|
||||||
},
|
},
|
||||||
|
externalDataAdapter(args) {
|
||||||
|
return {
|
||||||
|
text: args.source.getStringData(args.source.types[0]) || 'no value',
|
||||||
|
};
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[canDrop]
|
[canDrop]
|
||||||
);
|
);
|
||||||
@@ -115,6 +120,8 @@ export const DropTarget: StoryFn<{ canDrop: boolean }> = ({ canDrop }) => {
|
|||||||
}`}
|
}`}
|
||||||
</style>
|
</style>
|
||||||
<div ref={dragRef}>👉 hello</div>
|
<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}>
|
<div className="drop-target" ref={dropTargetRef}>
|
||||||
{dropData || 'Drop here'}
|
{dropData || 'Drop here'}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ export const useDraggable = <D extends DNDData = DNDData>(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dragRef.current.dataset.affineDraggable = 'true';
|
||||||
|
|
||||||
const cleanupDraggable = draggable({
|
const cleanupDraggable = draggable({
|
||||||
element: dragRef.current,
|
element: dragRef.current,
|
||||||
dragHandle: dragHandleRef.current ?? undefined,
|
dragHandle: dragHandleRef.current ?? undefined,
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
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 {
|
import {
|
||||||
attachClosestEdge,
|
attachClosestEdge,
|
||||||
type Edge,
|
type Edge,
|
||||||
@@ -14,6 +21,28 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import type { DNDData } from './types';
|
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<
|
type DropTargetGetFeedback<D extends DNDData> = Parameters<
|
||||||
NonNullable<Parameters<typeof dropTargetForElements>[0]['canDrop']>
|
NonNullable<Parameters<typeof dropTargetForElements>[0]['canDrop']>
|
||||||
>[0] & {
|
>[0] & {
|
||||||
@@ -29,6 +58,36 @@ type DropTargetGet<T, D extends DNDData> =
|
|||||||
| T
|
| T
|
||||||
| ((data: DropTargetGetFeedback<D>) => 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>(
|
function dropTargetGet<T, D extends DNDData>(
|
||||||
get: T,
|
get: T,
|
||||||
options: DropTargetOptions<D>
|
options: DropTargetOptions<D>
|
||||||
@@ -42,12 +101,13 @@ function dropTargetGet<T, D extends DNDData>(
|
|||||||
if (get === undefined) {
|
if (get === undefined) {
|
||||||
return undefined as any;
|
return undefined as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((
|
return ((
|
||||||
args: Omit<DropTargetGetFeedback<D>, 'treeInstruction' | 'closestEdge'>
|
args: Omit<DropTargetGetFeedback<D>, 'treeInstruction' | 'closestEdge'>
|
||||||
) => {
|
) => {
|
||||||
if (typeof get === 'function') {
|
if (typeof get === 'function') {
|
||||||
return (get as any)({
|
return (get as any)({
|
||||||
...args,
|
...getAdaptedEventArgs(options, args),
|
||||||
get treeInstruction() {
|
get treeInstruction() {
|
||||||
return options.treeInstruction
|
return options.treeInstruction
|
||||||
? extractInstruction(
|
? extractInstruction(
|
||||||
@@ -81,25 +141,14 @@ function dropTargetGet<T, D extends DNDData>(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return get;
|
return {
|
||||||
|
...get,
|
||||||
|
...getAdaptedEventArgs(options, args),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}) as any;
|
}) 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> {
|
export interface DropTargetOptions<D extends DNDData = DNDData> {
|
||||||
data?: DropTargetGet<D['dropTarget'], D>;
|
data?: DropTargetGet<D['dropTarget'], D>;
|
||||||
canDrop?: DropTargetGet<boolean, D>;
|
canDrop?: DropTargetGet<boolean, D>;
|
||||||
@@ -116,6 +165,13 @@ export interface DropTargetOptions<D extends DNDData = DNDData> {
|
|||||||
};
|
};
|
||||||
onDrop?: (data: DropTargetDropEvent<D>) => void;
|
onDrop?: (data: DropTargetDropEvent<D>) => void;
|
||||||
onDrag?: (data: DropTargetDragEvent<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>(
|
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>(
|
const [dropEffect, setDropEffect] = useState<'copy' | 'link' | 'move' | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [draggedOverDraggable, setDraggedOverDraggable] = useState<{
|
const [draggedOverDraggable, setDraggedOverDraggable] = useState<
|
||||||
data: D['draggable'];
|
DropTargetDropEvent<D>['source'] | null
|
||||||
} | null>(null);
|
>(null);
|
||||||
const [draggedOverPosition, setDraggedOverPosition] = useState<{
|
const [draggedOverPosition, setDraggedOverPosition] = useState<{
|
||||||
/**
|
/**
|
||||||
* relative position to the drop target element top-left corner
|
* 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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const options = useMemo(getOptions, deps);
|
const options = useMemo(getOptions, deps);
|
||||||
|
|
||||||
useEffect(() => {
|
const dropTargetOptions = useMemo(() => {
|
||||||
if (!dropTargetRef.current) {
|
return {
|
||||||
return;
|
get element() {
|
||||||
}
|
return dropTargetRef.current;
|
||||||
return dropTargetForElements({
|
},
|
||||||
element: dropTargetRef.current,
|
|
||||||
canDrop: dropTargetGet(options.canDrop, options),
|
canDrop: dropTargetGet(options.canDrop, options),
|
||||||
getDropEffect: dropTargetGet(options.dropEffect, options),
|
getDropEffect: dropTargetGet(options.dropEffect, options),
|
||||||
getIsSticky: dropTargetGet(options.isSticky, options),
|
getIsSticky: dropTargetGet(options.isSticky, options),
|
||||||
onDrop: args => {
|
onDrop: (args: DropTargetDropEvent<D>) => {
|
||||||
|
args = getAdaptedEventArgs(options, args);
|
||||||
if (enableDraggedOver.current) {
|
if (enableDraggedOver.current) {
|
||||||
setDraggedOver(false);
|
setDraggedOver(false);
|
||||||
}
|
}
|
||||||
@@ -202,7 +258,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
|||||||
} as DropTargetDropEvent<D>);
|
} as DropTargetDropEvent<D>);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getData: args => {
|
getData: (args: DropTargetGetFeedback<D>) => {
|
||||||
|
args = getAdaptedEventArgs(options, args);
|
||||||
const originData = dropTargetGet(options.data ?? {}, options)(args);
|
const originData = dropTargetGet(options.data ?? {}, options)(args);
|
||||||
const { input, element } = args;
|
const { input, element } = args;
|
||||||
const withInstruction = options.treeInstruction
|
const withInstruction = options.treeInstruction
|
||||||
@@ -224,13 +281,14 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
|||||||
: withInstruction;
|
: withInstruction;
|
||||||
return withClosestEdge;
|
return withClosestEdge;
|
||||||
},
|
},
|
||||||
onDrag: args => {
|
onDrag: (args: DropTargetDragEvent<D>) => {
|
||||||
|
args = getAdaptedEventArgs(options, args);
|
||||||
if (
|
if (
|
||||||
args.location.current.dropTargets[0]?.element ===
|
args.location.current.dropTargets[0]?.element ===
|
||||||
dropTargetRef.current
|
dropTargetRef.current
|
||||||
) {
|
) {
|
||||||
if (enableDraggedOverDraggable.current) {
|
if (enableDraggedOverDraggable.current) {
|
||||||
setDraggedOverDraggable({ data: args.source.data });
|
setDraggedOverDraggable(args.source);
|
||||||
}
|
}
|
||||||
let instruction = null;
|
let instruction = null;
|
||||||
let closestEdge = null;
|
let closestEdge = null;
|
||||||
@@ -266,7 +324,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
|||||||
} as DropTargetDropEvent<D>);
|
} as DropTargetDropEvent<D>);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDropTargetChange: args => {
|
onDropTargetChange: (args: DropTargetDropEvent<D>) => {
|
||||||
|
args = getAdaptedEventArgs(options, args);
|
||||||
if (
|
if (
|
||||||
args.location.current.dropTargets[0]?.element ===
|
args.location.current.dropTargets[0]?.element ===
|
||||||
dropTargetRef.current
|
dropTargetRef.current
|
||||||
@@ -290,7 +349,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
|||||||
setDropEffect(args.self.dropEffect);
|
setDropEffect(args.self.dropEffect);
|
||||||
}
|
}
|
||||||
if (enableDraggedOverDraggable.current) {
|
if (enableDraggedOverDraggable.current) {
|
||||||
setDraggedOverDraggable({ data: args.source.data });
|
setDraggedOverDraggable(args.source);
|
||||||
}
|
}
|
||||||
if (enableDraggedOverPosition.current) {
|
if (enableDraggedOverPosition.current) {
|
||||||
const rect = args.self.element.getBoundingClientRect();
|
const rect = args.self.element.getBoundingClientRect();
|
||||||
@@ -336,9 +395,23 @@ export const useDropTarget = <D extends DNDData = DNDData>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
}, [options]);
|
}, [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 {
|
return {
|
||||||
dropTargetRef,
|
dropTargetRef,
|
||||||
get draggedOver() {
|
get draggedOver() {
|
||||||
|
|||||||
14
packages/frontend/core/src/modules/dnd/index.ts
Normal file
14
packages/frontend/core/src/modules/dnd/index.ts
Normal file
@@ -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]);
|
||||||
|
}
|
||||||
89
packages/frontend/core/src/modules/dnd/services/index.ts
Normal file
89
packages/frontend/core/src/modules/dnd/services/index.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import type { ExternalGetDataFeedbackArgs } from '@affine/component';
|
||||||
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
|
import type { DocsService, WorkspaceService } from '@toeverything/infra';
|
||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { resolveLinkToDoc } from '../../navigation';
|
||||||
|
|
||||||
|
type EntityResolver = (
|
||||||
|
data: string
|
||||||
|
) => AffineDNDData['draggable']['entity'] | null;
|
||||||
|
|
||||||
|
export class DndService extends Service {
|
||||||
|
constructor(
|
||||||
|
private readonly docsService: DocsService,
|
||||||
|
private readonly workspaceService: WorkspaceService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// order matters
|
||||||
|
this.resolvers.set('text/html', this.resolveHTML);
|
||||||
|
this.resolvers.set('text/uri-list', this.resolveUriList);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly resolvers = new Map<string, EntityResolver>();
|
||||||
|
|
||||||
|
externalDataAdapter = (args: ExternalGetDataFeedbackArgs) => {
|
||||||
|
const from: AffineDNDData['draggable']['from'] = {
|
||||||
|
at: 'external',
|
||||||
|
};
|
||||||
|
let entity: AffineDNDData['draggable']['entity'];
|
||||||
|
|
||||||
|
// in the order of the resolvers instead of the order of the types
|
||||||
|
for (const [type, resolver] of this.resolvers) {
|
||||||
|
if (args.source.types.includes(type)) {
|
||||||
|
const stringData = args.source.getStringData(type);
|
||||||
|
if (stringData) {
|
||||||
|
const candidate = resolver(stringData);
|
||||||
|
if (candidate) {
|
||||||
|
entity = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from,
|
||||||
|
entity,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly resolveUriList: EntityResolver = urls => {
|
||||||
|
// only deal with the first url
|
||||||
|
const url = urls
|
||||||
|
?.split('\n')
|
||||||
|
.filter(u => u.trim() && !u.trim().startsWith('#'))[0];
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
const maybeDocLink = resolveLinkToDoc(url);
|
||||||
|
|
||||||
|
// check if the doc is in the current workspace
|
||||||
|
if (
|
||||||
|
maybeDocLink?.workspaceId === this.workspaceService.workspace.id &&
|
||||||
|
this.docsService.list.doc$(maybeDocLink.docId).value &&
|
||||||
|
// skip for block references for now
|
||||||
|
!maybeDocLink.blockIds?.length
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
type: 'doc',
|
||||||
|
id: maybeDocLink.docId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// todo: implement this
|
||||||
|
private readonly resolveHTML: EntityResolver = _html => {
|
||||||
|
try {
|
||||||
|
// const parser = new DOMParser();
|
||||||
|
// const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
// return doc.body.innerText;
|
||||||
|
} catch {
|
||||||
|
// ignore the error
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -189,9 +189,10 @@ export const ExplorerCollectionNode = ({
|
|||||||
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
||||||
() => args => {
|
() => args => {
|
||||||
const entityType = args.source.data.entity?.type;
|
const entityType = args.source.data.entity?.type;
|
||||||
|
const isExternalDrop = args.source.data.from?.at === 'external';
|
||||||
return args.treeInstruction?.type !== 'make-child'
|
return args.treeInstruction?.type !== 'make-child'
|
||||||
? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true)
|
? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true)
|
||||||
: entityType === 'doc';
|
: entityType === 'doc' || isExternalDrop;
|
||||||
},
|
},
|
||||||
[canDrop]
|
[canDrop]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -180,9 +180,10 @@ export const ExplorerDocNode = ({
|
|||||||
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
||||||
() => args => {
|
() => args => {
|
||||||
const entityType = args.source.data.entity?.type;
|
const entityType = args.source.data.entity?.type;
|
||||||
|
const isExternalDrop = args.source.data.from?.at === 'external';
|
||||||
return args.treeInstruction?.type !== 'make-child'
|
return args.treeInstruction?.type !== 'make-child'
|
||||||
? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true)
|
? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true)
|
||||||
: entityType === 'doc';
|
: entityType === 'doc' || isExternalDrop;
|
||||||
},
|
},
|
||||||
[canDrop]
|
[canDrop]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ export const favoriteChildrenDropEffect: ExplorerTreeNodeDropEffect = data => {
|
|||||||
) {
|
) {
|
||||||
return 'move';
|
return 'move';
|
||||||
} else if (
|
} else if (
|
||||||
data.source.data.entity?.type &&
|
(data.source.data.entity?.type &&
|
||||||
isFavoriteSupportType(data.source.data.entity.type)
|
isFavoriteSupportType(data.source.data.entity.type)) ||
|
||||||
|
// always allow external drop
|
||||||
|
data.source.data.from?.at === 'external'
|
||||||
) {
|
) {
|
||||||
return 'link';
|
return 'link';
|
||||||
}
|
}
|
||||||
@@ -37,7 +39,7 @@ export const favoriteRootCanDrop: DropTargetOptions<AffineDNDData>['canDrop'] =
|
|||||||
data => {
|
data => {
|
||||||
return data.source.data.entity?.type
|
return data.source.data.entity?.type
|
||||||
? isFavoriteSupportType(data.source.data.entity.type)
|
? isFavoriteSupportType(data.source.data.entity.type)
|
||||||
: false;
|
: data.source.data.from?.at === 'external'; // always allow external drop
|
||||||
};
|
};
|
||||||
|
|
||||||
export const favoriteChildrenCanDrop: DropTargetOptions<AffineDNDData>['canDrop'] =
|
export const favoriteChildrenCanDrop: DropTargetOptions<AffineDNDData>['canDrop'] =
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import {
|
|||||||
Skeleton,
|
Skeleton,
|
||||||
useDropTarget,
|
useDropTarget,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
|
import { DndService } from '@affine/core/modules/dnd/services';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { FavoriteIcon } from '@blocksuite/icons/rc';
|
import { FavoriteIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
|
||||||
import { ExplorerEmptySection } from '../../layouts/empty-section';
|
import { ExplorerEmptySection } from '../../layouts/empty-section';
|
||||||
import { DropEffect } from '../../tree';
|
import { DropEffect } from '../../tree';
|
||||||
@@ -21,6 +23,7 @@ const RootEmptyLoading = () => {
|
|||||||
};
|
};
|
||||||
const RootEmptyReady = ({ onDrop }: Omit<RootEmptyProps, 'isLoading'>) => {
|
const RootEmptyReady = ({ onDrop }: Omit<RootEmptyProps, 'isLoading'>) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const dndService = useService(DndService);
|
||||||
|
|
||||||
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
||||||
useDropTarget<AffineDNDData>(
|
useDropTarget<AffineDNDData>(
|
||||||
@@ -30,8 +33,9 @@ const RootEmptyReady = ({ onDrop }: Omit<RootEmptyProps, 'isLoading'>) => {
|
|||||||
},
|
},
|
||||||
onDrop: onDrop,
|
onDrop: onDrop,
|
||||||
canDrop: favoriteRootCanDrop,
|
canDrop: favoriteRootCanDrop,
|
||||||
|
externalDataAdapter: dndService.externalDataAdapter,
|
||||||
}),
|
}),
|
||||||
[onDrop]
|
[dndService.externalDataAdapter, onDrop]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
useDropTarget,
|
useDropTarget,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
||||||
|
import { DndService } from '@affine/core/modules/dnd/services';
|
||||||
import {
|
import {
|
||||||
DropEffect,
|
DropEffect,
|
||||||
ExplorerTreeRoot,
|
ExplorerTreeRoot,
|
||||||
@@ -20,6 +21,7 @@ import { track } from '@affine/track';
|
|||||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
useLiveData,
|
useLiveData,
|
||||||
|
useService,
|
||||||
useServices,
|
useServices,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
@@ -149,6 +151,8 @@ export const ExplorerFavorites = () => {
|
|||||||
[favoriteService]
|
[favoriteService]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dndService = useService(DndService);
|
||||||
|
|
||||||
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
||||||
useDropTarget<AffineDNDData>(
|
useDropTarget<AffineDNDData>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -157,8 +161,9 @@ export const ExplorerFavorites = () => {
|
|||||||
},
|
},
|
||||||
onDrop: handleDrop,
|
onDrop: handleDrop,
|
||||||
canDrop: favoriteRootCanDrop,
|
canDrop: favoriteRootCanDrop,
|
||||||
|
externalDataAdapter: dndService.externalDataAdapter,
|
||||||
}),
|
}),
|
||||||
[handleDrop]
|
[dndService.externalDataAdapter, handleDrop]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { RenameModal } from '@affine/component/rename-modal';
|
import { RenameModal } from '@affine/component/rename-modal';
|
||||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||||
|
import { DndService } from '@affine/core/modules/dnd/services';
|
||||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import { extractEmojiIcon } from '@affine/core/utils';
|
import { extractEmojiIcon } from '@affine/core/utils';
|
||||||
@@ -185,6 +186,8 @@ export const ExplorerTreeNode = ({
|
|||||||
},
|
},
|
||||||
[canDrop, reorderable]
|
[canDrop, reorderable]
|
||||||
);
|
);
|
||||||
|
const dndService = useService(DndService);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dropTargetRef,
|
dropTargetRef,
|
||||||
treeInstruction,
|
treeInstruction,
|
||||||
@@ -221,6 +224,9 @@ export const ExplorerTreeNode = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
canDrop: handleCanDrop,
|
canDrop: handleCanDrop,
|
||||||
|
externalDataAdapter(args) {
|
||||||
|
return dndService.externalDataAdapter(args) as any;
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
dndData?.dropTarget,
|
dndData?.dropTarget,
|
||||||
@@ -232,6 +238,7 @@ export const ExplorerTreeNode = ({
|
|||||||
cid,
|
cid,
|
||||||
onDrop,
|
onDrop,
|
||||||
setCollapsed,
|
setCollapsed,
|
||||||
|
dndService,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
const isSelfDraggedOver = draggedOverDraggable?.data.__cid === cid;
|
const isSelfDraggedOver = draggedOverDraggable?.data.__cid === cid;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { configAtMenuConfigModule } from './at-menu-config';
|
|||||||
import { configureCloudModule } from './cloud';
|
import { configureCloudModule } from './cloud';
|
||||||
import { configureCollectionModule } from './collection';
|
import { configureCollectionModule } from './collection';
|
||||||
import { configureDialogModule } from './dialogs';
|
import { configureDialogModule } from './dialogs';
|
||||||
|
import { configureDndModule } from './dnd';
|
||||||
import { configureDocDisplayMetaModule } from './doc-display-meta';
|
import { configureDocDisplayMetaModule } from './doc-display-meta';
|
||||||
import { configureDocInfoModule } from './doc-info';
|
import { configureDocInfoModule } from './doc-info';
|
||||||
import { configureDocLinksModule } from './doc-link';
|
import { configureDocLinksModule } from './doc-link';
|
||||||
@@ -69,4 +70,5 @@ export function configureCommonModules(framework: Framework) {
|
|||||||
configureDocInfoModule(framework);
|
configureDocInfoModule(framework);
|
||||||
configureOpenInApp(framework);
|
configureOpenInApp(framework);
|
||||||
configAtMenuConfigModule(framework);
|
configAtMenuConfigModule(framework);
|
||||||
|
configureDndModule(framework);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ export interface AffineDNDData extends DNDData {
|
|||||||
| {
|
| {
|
||||||
at: 'doc-property:manager';
|
at: 'doc-property:manager';
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
at: 'external'; // for blocksuite or external apps
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
dropTarget:
|
dropTarget:
|
||||||
|
|||||||
15
yarn.lock
15
yarn.lock
@@ -316,7 +316,7 @@ __metadata:
|
|||||||
"@affine/electron-api": "workspace:*"
|
"@affine/electron-api": "workspace:*"
|
||||||
"@affine/graphql": "workspace:*"
|
"@affine/graphql": "workspace:*"
|
||||||
"@affine/i18n": "workspace:*"
|
"@affine/i18n": "workspace:*"
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "npm:^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": "npm:^1.0.3"
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "npm:^1.0.3"
|
||||||
"@blocksuite/affine": "npm:0.18.4"
|
"@blocksuite/affine": "npm:0.18.4"
|
||||||
"@blocksuite/icons": "npm:2.1.75"
|
"@blocksuite/icons": "npm:2.1.75"
|
||||||
@@ -1209,7 +1209,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@atlaskit/pragmatic-drag-and-drop@npm:^1.1.0, @atlaskit/pragmatic-drag-and-drop@npm:^1.2.1":
|
"@atlaskit/pragmatic-drag-and-drop@npm:1.4.0":
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
resolution: "@atlaskit/pragmatic-drag-and-drop@npm:1.4.0"
|
resolution: "@atlaskit/pragmatic-drag-and-drop@npm:1.4.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1220,6 +1220,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@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":
|
||||||
|
version: 1.4.0
|
||||||
|
resolution: "@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::version=1.4.0&hash=9e529b"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": "npm:^7.0.0"
|
||||||
|
bind-event-listener: "npm:^3.0.0"
|
||||||
|
raf-schd: "npm:^4.0.3"
|
||||||
|
checksum: 10/4376cd2dfe2771081b601ce33d79ec2d2f412739bfa096b6f7bc098cca73659e2ae84ef69f4632f03708b814ed920794fbf9e1826c92c042d397597098de1f6b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@aws-crypto/crc32@npm:5.2.0":
|
"@aws-crypto/crc32@npm:5.2.0":
|
||||||
version: 5.2.0
|
version: 5.2.0
|
||||||
resolution: "@aws-crypto/crc32@npm:5.2.0"
|
resolution: "@aws-crypto/crc32@npm:5.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user