mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +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:
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']>(
|
||||
() => args => {
|
||||
const entityType = args.source.data.entity?.type;
|
||||
const isExternalDrop = args.source.data.from?.at === 'external';
|
||||
return args.treeInstruction?.type !== 'make-child'
|
||||
? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true)
|
||||
: entityType === 'doc';
|
||||
: entityType === 'doc' || isExternalDrop;
|
||||
},
|
||||
[canDrop]
|
||||
);
|
||||
|
||||
@@ -180,9 +180,10 @@ export const ExplorerDocNode = ({
|
||||
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
||||
() => args => {
|
||||
const entityType = args.source.data.entity?.type;
|
||||
const isExternalDrop = args.source.data.from?.at === 'external';
|
||||
return args.treeInstruction?.type !== 'make-child'
|
||||
? ((typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true)
|
||||
: entityType === 'doc';
|
||||
: entityType === 'doc' || isExternalDrop;
|
||||
},
|
||||
[canDrop]
|
||||
);
|
||||
|
||||
@@ -16,8 +16,10 @@ export const favoriteChildrenDropEffect: ExplorerTreeNodeDropEffect = data => {
|
||||
) {
|
||||
return 'move';
|
||||
} else if (
|
||||
data.source.data.entity?.type &&
|
||||
isFavoriteSupportType(data.source.data.entity.type)
|
||||
(data.source.data.entity?.type &&
|
||||
isFavoriteSupportType(data.source.data.entity.type)) ||
|
||||
// always allow external drop
|
||||
data.source.data.from?.at === 'external'
|
||||
) {
|
||||
return 'link';
|
||||
}
|
||||
@@ -37,7 +39,7 @@ export const favoriteRootCanDrop: DropTargetOptions<AffineDNDData>['canDrop'] =
|
||||
data => {
|
||||
return 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'] =
|
||||
|
||||
@@ -3,9 +3,11 @@ import {
|
||||
Skeleton,
|
||||
useDropTarget,
|
||||
} from '@affine/component';
|
||||
import { DndService } from '@affine/core/modules/dnd/services';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { FavoriteIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
|
||||
import { ExplorerEmptySection } from '../../layouts/empty-section';
|
||||
import { DropEffect } from '../../tree';
|
||||
@@ -21,6 +23,7 @@ const RootEmptyLoading = () => {
|
||||
};
|
||||
const RootEmptyReady = ({ onDrop }: Omit<RootEmptyProps, 'isLoading'>) => {
|
||||
const t = useI18n();
|
||||
const dndService = useService(DndService);
|
||||
|
||||
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
||||
useDropTarget<AffineDNDData>(
|
||||
@@ -30,8 +33,9 @@ const RootEmptyReady = ({ onDrop }: Omit<RootEmptyProps, 'isLoading'>) => {
|
||||
},
|
||||
onDrop: onDrop,
|
||||
canDrop: favoriteRootCanDrop,
|
||||
externalDataAdapter: dndService.externalDataAdapter,
|
||||
}),
|
||||
[onDrop]
|
||||
[dndService.externalDataAdapter, onDrop]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
useDropTarget,
|
||||
} from '@affine/component';
|
||||
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
||||
import { DndService } from '@affine/core/modules/dnd/services';
|
||||
import {
|
||||
DropEffect,
|
||||
ExplorerTreeRoot,
|
||||
@@ -20,6 +21,7 @@ import { track } from '@affine/track';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
@@ -149,6 +151,8 @@ export const ExplorerFavorites = () => {
|
||||
[favoriteService]
|
||||
);
|
||||
|
||||
const dndService = useService(DndService);
|
||||
|
||||
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
||||
useDropTarget<AffineDNDData>(
|
||||
() => ({
|
||||
@@ -157,8 +161,9 @@ export const ExplorerFavorites = () => {
|
||||
},
|
||||
onDrop: handleDrop,
|
||||
canDrop: favoriteRootCanDrop,
|
||||
externalDataAdapter: dndService.externalDataAdapter,
|
||||
}),
|
||||
[handleDrop]
|
||||
[dndService.externalDataAdapter, handleDrop]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@affine/component';
|
||||
import { RenameModal } from '@affine/component/rename-modal';
|
||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||
import { DndService } from '@affine/core/modules/dnd/services';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { extractEmojiIcon } from '@affine/core/utils';
|
||||
@@ -185,6 +186,8 @@ export const ExplorerTreeNode = ({
|
||||
},
|
||||
[canDrop, reorderable]
|
||||
);
|
||||
const dndService = useService(DndService);
|
||||
|
||||
const {
|
||||
dropTargetRef,
|
||||
treeInstruction,
|
||||
@@ -221,6 +224,9 @@ export const ExplorerTreeNode = ({
|
||||
}
|
||||
},
|
||||
canDrop: handleCanDrop,
|
||||
externalDataAdapter(args) {
|
||||
return dndService.externalDataAdapter(args) as any;
|
||||
},
|
||||
}),
|
||||
[
|
||||
dndData?.dropTarget,
|
||||
@@ -232,6 +238,7 @@ export const ExplorerTreeNode = ({
|
||||
cid,
|
||||
onDrop,
|
||||
setCollapsed,
|
||||
dndService,
|
||||
]
|
||||
);
|
||||
const isSelfDraggedOver = draggedOverDraggable?.data.__cid === cid;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { configAtMenuConfigModule } from './at-menu-config';
|
||||
import { configureCloudModule } from './cloud';
|
||||
import { configureCollectionModule } from './collection';
|
||||
import { configureDialogModule } from './dialogs';
|
||||
import { configureDndModule } from './dnd';
|
||||
import { configureDocDisplayMetaModule } from './doc-display-meta';
|
||||
import { configureDocInfoModule } from './doc-info';
|
||||
import { configureDocLinksModule } from './doc-link';
|
||||
@@ -69,4 +70,5 @@ export function configureCommonModules(framework: Framework) {
|
||||
configureDocInfoModule(framework);
|
||||
configureOpenInApp(framework);
|
||||
configAtMenuConfigModule(framework);
|
||||
configureDndModule(framework);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@ export interface AffineDNDData extends DNDData {
|
||||
| {
|
||||
at: 'doc-property:manager';
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
at: 'external'; // for blocksuite or external apps
|
||||
};
|
||||
};
|
||||
dropTarget:
|
||||
|
||||
Reference in New Issue
Block a user