mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 03:49:56 +08:00
refactor(editor): move file drop manager to components (#9264)
This commit is contained in:
@@ -1,173 +0,0 @@
|
||||
import type { DragIndicator } from '@blocksuite/affine-components/drag-indicator';
|
||||
import {
|
||||
calcDropTarget,
|
||||
type DropResult,
|
||||
getClosestBlockComponentByPoint,
|
||||
isInsidePageEditor,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockService, EditorHost } from '@blocksuite/block-std';
|
||||
import type { IVec } from '@blocksuite/global/utils';
|
||||
import { assertExists, Point } from '@blocksuite/global/utils';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
export type onDropProps = {
|
||||
files: File[];
|
||||
targetModel: BlockModel | null;
|
||||
place: 'before' | 'after';
|
||||
point: IVec;
|
||||
};
|
||||
|
||||
export type FileDropOptions = {
|
||||
flavour: string;
|
||||
onDrop?: ({
|
||||
files,
|
||||
targetModel,
|
||||
place,
|
||||
point,
|
||||
}: onDropProps) => Promise<boolean> | void;
|
||||
};
|
||||
|
||||
export class FileDropManager {
|
||||
private static _dropResult: DropResult | null = null;
|
||||
|
||||
private readonly _blockService: BlockService;
|
||||
|
||||
private readonly _fileDropOptions: FileDropOptions;
|
||||
|
||||
private readonly _indicator!: DragIndicator;
|
||||
|
||||
private readonly _onDrop = (event: DragEvent) => {
|
||||
this._indicator.rect = null;
|
||||
|
||||
const { onDrop } = this._fileDropOptions;
|
||||
if (!onDrop) return;
|
||||
|
||||
const dataTransfer = event.dataTransfer;
|
||||
if (!dataTransfer) return;
|
||||
|
||||
const effectAllowed = dataTransfer.effectAllowed;
|
||||
if (effectAllowed === 'none') return;
|
||||
|
||||
const droppedFiles = dataTransfer.files;
|
||||
if (!droppedFiles || !droppedFiles.length) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const { targetModel, type: place } = this;
|
||||
const { x, y } = event;
|
||||
|
||||
onDrop({
|
||||
files: [...droppedFiles],
|
||||
targetModel,
|
||||
place,
|
||||
point: [x, y],
|
||||
})?.catch(console.error);
|
||||
};
|
||||
|
||||
onDragLeave = () => {
|
||||
FileDropManager._dropResult = null;
|
||||
this._indicator.rect = null;
|
||||
};
|
||||
|
||||
onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const dataTransfer = event.dataTransfer;
|
||||
if (!dataTransfer) return;
|
||||
|
||||
const effectAllowed = dataTransfer.effectAllowed;
|
||||
if (effectAllowed === 'none') return;
|
||||
|
||||
const { clientX, clientY } = event;
|
||||
const point = new Point(clientX, clientY);
|
||||
const element = getClosestBlockComponentByPoint(point.clone());
|
||||
|
||||
let result: DropResult | null = null;
|
||||
if (element) {
|
||||
const model = element.model;
|
||||
const parent = this.doc.getParent(model);
|
||||
if (!matchFlavours(parent, ['affine:surface'])) {
|
||||
result = calcDropTarget(point, model, element);
|
||||
}
|
||||
}
|
||||
if (result) {
|
||||
FileDropManager._dropResult = result;
|
||||
this._indicator.rect = result.rect;
|
||||
} else {
|
||||
FileDropManager._dropResult = null;
|
||||
this._indicator.rect = null;
|
||||
}
|
||||
};
|
||||
|
||||
get doc() {
|
||||
return this._blockService.doc;
|
||||
}
|
||||
|
||||
get editorHost(): EditorHost {
|
||||
return this._blockService.std.host;
|
||||
}
|
||||
|
||||
get targetModel(): BlockModel | null {
|
||||
let targetModel = FileDropManager._dropResult?.modelState.model || null;
|
||||
|
||||
if (!targetModel && isInsidePageEditor(this.editorHost)) {
|
||||
const rootModel = this.doc.root;
|
||||
assertExists(rootModel);
|
||||
|
||||
let lastNote = rootModel.children[rootModel.children.length - 1];
|
||||
if (!lastNote || !matchFlavours(lastNote, ['affine:note'])) {
|
||||
const newNoteId = this.doc.addBlock('affine:note', {}, rootModel.id);
|
||||
const newNote = this.doc.getBlockById(newNoteId);
|
||||
assertExists(newNote);
|
||||
lastNote = newNote;
|
||||
}
|
||||
|
||||
const lastItem = lastNote.children[lastNote.children.length - 1];
|
||||
if (lastItem) {
|
||||
targetModel = lastItem;
|
||||
} else {
|
||||
const newParagraphId = this.doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{},
|
||||
lastNote,
|
||||
0
|
||||
);
|
||||
const newParagraph = this.doc.getBlockById(newParagraphId);
|
||||
assertExists(newParagraph);
|
||||
targetModel = newParagraph;
|
||||
}
|
||||
}
|
||||
return targetModel;
|
||||
}
|
||||
|
||||
get type(): 'before' | 'after' {
|
||||
return !FileDropManager._dropResult ||
|
||||
FileDropManager._dropResult.type !== 'before'
|
||||
? 'after'
|
||||
: 'before';
|
||||
}
|
||||
|
||||
constructor(blockService: BlockService, fileDropOptions: FileDropOptions) {
|
||||
this._blockService = blockService;
|
||||
this._fileDropOptions = fileDropOptions;
|
||||
|
||||
this._indicator = document.querySelector(
|
||||
'affine-drag-indicator'
|
||||
) as DragIndicator;
|
||||
if (!this._indicator) {
|
||||
this._indicator = document.createElement(
|
||||
'affine-drag-indicator'
|
||||
) as DragIndicator;
|
||||
document.body.append(this._indicator);
|
||||
}
|
||||
|
||||
if (fileDropOptions.onDrop) {
|
||||
this._blockService.disposables.addFromEvent(
|
||||
this._blockService.std.host,
|
||||
'drop',
|
||||
this._onDrop
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './ai-item/index.js';
|
||||
export * from './block-selection.js';
|
||||
export * from './block-zero-width.js';
|
||||
export * from './file-drop-manager.js';
|
||||
export * from './menu-divider.js';
|
||||
export { scrollbarStyle } from './utils.js';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FileDropConfigExtension } from '@blocksuite/affine-components/drag-indicator';
|
||||
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DragHandleConfigExtension,
|
||||
@@ -13,65 +14,63 @@ import {
|
||||
import { BlockService } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
|
||||
import {
|
||||
FileDropManager,
|
||||
type FileDropOptions,
|
||||
} from '../_common/components/file-drop-manager.js';
|
||||
import { EMBED_CARD_HEIGHT, EMBED_CARD_WIDTH } from '../_common/consts.js';
|
||||
import { addAttachments } from '../root-block/edgeless/utils/common.js';
|
||||
import type { AttachmentBlockComponent } from './attachment-block.js';
|
||||
import { AttachmentEdgelessBlockComponent } from './attachment-edgeless-block.js';
|
||||
import { addSiblingAttachmentBlocks } from './utils.js';
|
||||
|
||||
// bytes.parse('2GB')
|
||||
const maxFileSize = 2147483648;
|
||||
|
||||
export class AttachmentBlockService extends BlockService {
|
||||
static override readonly flavour = AttachmentBlockSchema.model.flavour;
|
||||
|
||||
private readonly _fileDropOptions: FileDropOptions = {
|
||||
flavour: this.flavour,
|
||||
onDrop: async ({ files, targetModel, place, point }) => {
|
||||
if (!files.length) return false;
|
||||
maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
// generic attachment block for all files except images
|
||||
const attachmentFiles = files.filter(
|
||||
file => !file.type.startsWith('image/')
|
||||
);
|
||||
export const AttachmentDropOption = FileDropConfigExtension({
|
||||
flavour: AttachmentBlockSchema.model.flavour,
|
||||
onDrop: ({ files, targetModel, place, point, std }) => {
|
||||
// generic attachment block for all files except images
|
||||
const attachmentFiles = files.filter(
|
||||
file => !file.type.startsWith('image/')
|
||||
);
|
||||
|
||||
if (targetModel && !matchFlavours(targetModel, ['affine:surface'])) {
|
||||
await addSiblingAttachmentBlocks(
|
||||
this.host,
|
||||
attachmentFiles,
|
||||
this.maxFileSize,
|
||||
targetModel,
|
||||
place
|
||||
);
|
||||
} else if (isInsideEdgelessEditor(this.host)) {
|
||||
const gfx = this.std.get(GfxControllerIdentifier);
|
||||
point = gfx.viewport.toViewCoordFromClientCoord(point);
|
||||
await addAttachments(this.std, attachmentFiles, point);
|
||||
if (!attachmentFiles.length) return false;
|
||||
|
||||
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
control: 'canvas:drop',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: 'attachment',
|
||||
});
|
||||
}
|
||||
if (targetModel && !matchFlavours(targetModel, ['affine:surface'])) {
|
||||
addSiblingAttachmentBlocks(
|
||||
std.host,
|
||||
attachmentFiles,
|
||||
// TODO: use max file size from service
|
||||
maxFileSize,
|
||||
targetModel,
|
||||
place
|
||||
).catch(console.error);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fileDropManager!: FileDropManager;
|
||||
if (isInsideEdgelessEditor(std.host)) {
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
point = gfx.viewport.toViewCoordFromClientCoord(point);
|
||||
addAttachments(std, attachmentFiles, point).catch(console.error);
|
||||
|
||||
maxFileSize = 10 * 1000 * 1000; // 10MB (default)
|
||||
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
control: 'canvas:drop',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: 'attachment',
|
||||
});
|
||||
|
||||
override mounted(): void {
|
||||
super.mounted();
|
||||
return true;
|
||||
}
|
||||
|
||||
this.fileDropManager = new FileDropManager(this, this._fileDropOptions);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
export const AttachmentDragHandleOption = DragHandleConfigExtension({
|
||||
flavour: AttachmentBlockSchema.model.flavour,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-htm
|
||||
import {
|
||||
AttachmentBlockService,
|
||||
AttachmentDragHandleOption,
|
||||
AttachmentDropOption,
|
||||
} from './attachment-service.js';
|
||||
import {
|
||||
AttachmentEmbedConfigExtension,
|
||||
@@ -23,6 +24,7 @@ export const AttachmentBlockSpec: ExtensionType[] = [
|
||||
? literal`affine-edgeless-attachment`
|
||||
: literal`affine-attachment`;
|
||||
}),
|
||||
AttachmentDropOption,
|
||||
AttachmentDragHandleOption,
|
||||
AttachmentEmbedConfigExtension(),
|
||||
AttachmentEmbedService,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FileDropConfigExtension } from '@blocksuite/affine-components/drag-indicator';
|
||||
import { ImageBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DragHandleConfigExtension,
|
||||
@@ -13,64 +14,60 @@ import {
|
||||
import { BlockService } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
|
||||
import {
|
||||
FileDropManager,
|
||||
type FileDropOptions,
|
||||
} from '../_common/components/file-drop-manager.js';
|
||||
import { setImageProxyMiddlewareURL } from '../_common/transformers/middlewares.js';
|
||||
import { addImages } from '../root-block/edgeless/utils/common.js';
|
||||
import type { ImageBlockComponent } from './image-block.js';
|
||||
import { ImageEdgelessBlockComponent } from './image-edgeless-block.js';
|
||||
import { addSiblingImageBlock } from './utils.js';
|
||||
|
||||
// bytes.parse('2GB')
|
||||
const maxFileSize = 2147483648;
|
||||
|
||||
export class ImageBlockService extends BlockService {
|
||||
static override readonly flavour = ImageBlockSchema.model.flavour;
|
||||
|
||||
static setImageProxyURL = setImageProxyMiddlewareURL;
|
||||
|
||||
private readonly _fileDropOptions: FileDropOptions = {
|
||||
flavour: this.flavour,
|
||||
onDrop: async ({ files, targetModel, place, point }) => {
|
||||
const imageFiles = files.filter(file => file.type.startsWith('image/'));
|
||||
if (!imageFiles.length) return false;
|
||||
|
||||
if (targetModel && !matchFlavours(targetModel, ['affine:surface'])) {
|
||||
addSiblingImageBlock(
|
||||
this.host,
|
||||
imageFiles,
|
||||
this.maxFileSize,
|
||||
targetModel,
|
||||
place
|
||||
);
|
||||
} else if (isInsideEdgelessEditor(this.host)) {
|
||||
const gfx = this.std.get(GfxControllerIdentifier);
|
||||
point = gfx.viewport.toViewCoordFromClientCoord(point);
|
||||
await addImages(this.std, files, point);
|
||||
|
||||
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
control: 'canvas:drop',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: 'image',
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
fileDropManager!: FileDropManager;
|
||||
|
||||
maxFileSize = 10 * 1000 * 1000; // 10MB (default)
|
||||
|
||||
override mounted(): void {
|
||||
super.mounted();
|
||||
|
||||
this.fileDropManager = new FileDropManager(this, this._fileDropOptions);
|
||||
}
|
||||
maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
export const ImageDropOption = FileDropConfigExtension({
|
||||
flavour: ImageBlockSchema.model.flavour,
|
||||
onDrop: ({ files, targetModel, place, point, std }) => {
|
||||
const imageFiles = files.filter(file => file.type.startsWith('image/'));
|
||||
if (!imageFiles.length) return false;
|
||||
|
||||
if (targetModel && !matchFlavours(targetModel, ['affine:surface'])) {
|
||||
addSiblingImageBlock(
|
||||
std.host,
|
||||
imageFiles,
|
||||
// TODO: use max file size from service
|
||||
maxFileSize,
|
||||
targetModel,
|
||||
place
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isInsideEdgelessEditor(std.host)) {
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
point = gfx.viewport.toViewCoordFromClientCoord(point);
|
||||
addImages(std, files, point).catch(console.error);
|
||||
|
||||
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
control: 'canvas:drop',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: 'image',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
export const ImageDragHandleOption = DragHandleConfigExtension({
|
||||
flavour: ImageBlockSchema.model.flavour,
|
||||
edgeless: true,
|
||||
|
||||
@@ -10,7 +10,11 @@ import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { ImageBlockAdapterExtensions } from './adapters/extension.js';
|
||||
import { commands } from './commands/index.js';
|
||||
import { ImageBlockService, ImageDragHandleOption } from './image-service.js';
|
||||
import {
|
||||
ImageBlockService,
|
||||
ImageDragHandleOption,
|
||||
ImageDropOption,
|
||||
} from './image-service.js';
|
||||
|
||||
export const ImageBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension('affine:image'),
|
||||
@@ -29,6 +33,7 @@ export const ImageBlockSpec: ExtensionType[] = [
|
||||
imageToolbar: literal`affine-image-toolbar-widget`,
|
||||
}),
|
||||
ImageDragHandleOption,
|
||||
ImageDropOption,
|
||||
ImageSelectionExtension,
|
||||
ImageBlockAdapterExtensions,
|
||||
].flat();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FileDropExtension } from '@blocksuite/affine-components/drag-indicator';
|
||||
import {
|
||||
DNDAPIExtension,
|
||||
DocDisplayMetaService,
|
||||
@@ -98,6 +99,7 @@ const EdgelessCommonExtension: ExtensionType[] = [
|
||||
DNDAPIExtension,
|
||||
DocDisplayMetaService,
|
||||
RootBlockAdapterExtensions,
|
||||
FileDropExtension,
|
||||
].flat();
|
||||
|
||||
export const EdgelessRootBlockSpec: ExtensionType[] = [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FileDropExtension } from '@blocksuite/affine-components/drag-indicator';
|
||||
import {
|
||||
DNDAPIExtension,
|
||||
DocDisplayMetaService,
|
||||
@@ -75,4 +76,5 @@ export const PageRootBlockSpec: ExtensionType[] = [
|
||||
DNDAPIExtension,
|
||||
DocDisplayMetaService,
|
||||
RootBlockAdapterExtensions,
|
||||
FileDropExtension,
|
||||
].flat();
|
||||
|
||||
@@ -2,10 +2,6 @@ import { RootBlockSchema } from '@blocksuite/affine-model';
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
import { BlockService } from '@blocksuite/block-std';
|
||||
|
||||
import {
|
||||
FileDropManager,
|
||||
type FileDropOptions,
|
||||
} from '../_common/components/file-drop-manager.js';
|
||||
import {
|
||||
HtmlTransformer,
|
||||
MarkdownTransformer,
|
||||
@@ -16,12 +12,6 @@ import type { RootBlockComponent } from './types.js';
|
||||
export abstract class RootService extends BlockService {
|
||||
static override readonly flavour = RootBlockSchema.model.flavour;
|
||||
|
||||
private readonly _fileDropOptions: FileDropOptions = {
|
||||
flavour: this.flavour,
|
||||
};
|
||||
|
||||
readonly fileDropManager = new FileDropManager(this, this._fileDropOptions);
|
||||
|
||||
transformers = {
|
||||
markdown: MarkdownTransformer,
|
||||
html: HtmlTransformer,
|
||||
@@ -64,18 +54,6 @@ export abstract class RootService extends BlockService {
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
|
||||
this.disposables.addFromEvent(
|
||||
this.host,
|
||||
'dragover',
|
||||
this.fileDropManager.onDragOver
|
||||
);
|
||||
|
||||
this.disposables.addFromEvent(
|
||||
this.host,
|
||||
'dragleave',
|
||||
this.fileDropManager.onDragLeave
|
||||
);
|
||||
|
||||
this.disposables.add(
|
||||
this.std.event.add('pointerDown', ctx => {
|
||||
const state = ctx.get('pointerState');
|
||||
|
||||
@@ -109,6 +109,12 @@ export class DragEventWatcher {
|
||||
};
|
||||
|
||||
private readonly _dropHandler = (context: UIEventStateContext) => {
|
||||
const raw = context.get('dndState').raw;
|
||||
const fileLength = raw.dataTransfer?.files.length ?? 0;
|
||||
// If drop files, should let file drop extension handle it
|
||||
if (fileLength > 0) {
|
||||
return;
|
||||
}
|
||||
this._onDrop(context);
|
||||
this._std.selection.setGroup('gfx', []);
|
||||
this.widget.clearRaf();
|
||||
|
||||
Reference in New Issue
Block a user