mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
refactor(editor): move file drop manager to components (#9264)
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Rect } from '@blocksuite/global/utils';
|
||||||
|
import { css, html, LitElement } from 'lit';
|
||||||
|
import { property } from 'lit/decorators.js';
|
||||||
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
|
export class DragIndicator extends LitElement {
|
||||||
|
static override styles = css`
|
||||||
|
.affine-drag-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: var(--affine-primary-color);
|
||||||
|
transition-property: width, height, transform;
|
||||||
|
transition-duration: 100ms;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-delay: 0s;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
if (!this.rect) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { left, top, width, height } = this.rect;
|
||||||
|
const style = styleMap({
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
transform: `translate(${left}px, ${top}px)`,
|
||||||
|
});
|
||||||
|
return html`<div class="affine-drag-indicator" style=${style}></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor rect: Rect | null = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'affine-drag-indicator': DragIndicator;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { DragIndicator } from '@blocksuite/affine-components/drag-indicator';
|
|
||||||
import {
|
import {
|
||||||
calcDropTarget,
|
calcDropTarget,
|
||||||
type DropResult,
|
type DropResult,
|
||||||
@@ -6,12 +5,21 @@ import {
|
|||||||
isInsidePageEditor,
|
isInsidePageEditor,
|
||||||
matchFlavours,
|
matchFlavours,
|
||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
import type { BlockService, EditorHost } from '@blocksuite/block-std';
|
import {
|
||||||
|
type BlockStdScope,
|
||||||
|
type EditorHost,
|
||||||
|
type ExtensionType,
|
||||||
|
LifeCycleWatcher,
|
||||||
|
} from '@blocksuite/block-std';
|
||||||
|
import { createIdentifier } from '@blocksuite/global/di';
|
||||||
import type { IVec } from '@blocksuite/global/utils';
|
import type { IVec } from '@blocksuite/global/utils';
|
||||||
import { assertExists, Point } from '@blocksuite/global/utils';
|
import { Point } from '@blocksuite/global/utils';
|
||||||
import type { BlockModel } from '@blocksuite/store';
|
import type { BlockModel } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { DragIndicator } from './index.js';
|
||||||
|
|
||||||
export type onDropProps = {
|
export type onDropProps = {
|
||||||
|
std: BlockStdScope;
|
||||||
files: File[];
|
files: File[];
|
||||||
targetModel: BlockModel | null;
|
targetModel: BlockModel | null;
|
||||||
place: 'before' | 'after';
|
place: 'before' | 'after';
|
||||||
@@ -20,54 +28,32 @@ export type onDropProps = {
|
|||||||
|
|
||||||
export type FileDropOptions = {
|
export type FileDropOptions = {
|
||||||
flavour: string;
|
flavour: string;
|
||||||
onDrop?: ({
|
onDrop?: (onDropProps: onDropProps) => boolean;
|
||||||
files,
|
|
||||||
targetModel,
|
|
||||||
place,
|
|
||||||
point,
|
|
||||||
}: onDropProps) => Promise<boolean> | void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FileDropManager {
|
export class FileDropExtension extends LifeCycleWatcher {
|
||||||
private static _dropResult: DropResult | null = null;
|
static override readonly key = 'FileDropExtension';
|
||||||
|
|
||||||
private readonly _blockService: BlockService;
|
static dropResult: DropResult | null = null;
|
||||||
|
|
||||||
private readonly _fileDropOptions: FileDropOptions;
|
static get indicator() {
|
||||||
|
let indicator = document.querySelector<DragIndicator>(
|
||||||
|
'affine-drag-indicator'
|
||||||
|
);
|
||||||
|
|
||||||
private readonly _indicator!: DragIndicator;
|
if (!indicator) {
|
||||||
|
indicator = document.createElement(
|
||||||
|
'affine-drag-indicator'
|
||||||
|
) as DragIndicator;
|
||||||
|
document.body.append(indicator);
|
||||||
|
}
|
||||||
|
|
||||||
private readonly _onDrop = (event: DragEvent) => {
|
return indicator;
|
||||||
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 = () => {
|
onDragLeave = () => {
|
||||||
FileDropManager._dropResult = null;
|
FileDropExtension.dropResult = null;
|
||||||
this._indicator.rect = null;
|
FileDropExtension.indicator.rect = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
onDragOver = (event: DragEvent) => {
|
onDragOver = (event: DragEvent) => {
|
||||||
@@ -86,40 +72,32 @@ export class FileDropManager {
|
|||||||
let result: DropResult | null = null;
|
let result: DropResult | null = null;
|
||||||
if (element) {
|
if (element) {
|
||||||
const model = element.model;
|
const model = element.model;
|
||||||
const parent = this.doc.getParent(model);
|
const parent = this.std.doc.getParent(model);
|
||||||
if (!matchFlavours(parent, ['affine:surface'])) {
|
if (!matchFlavours(parent, ['affine:surface' as BlockSuite.Flavour])) {
|
||||||
result = calcDropTarget(point, model, element);
|
result = calcDropTarget(point, model, element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result) {
|
if (result) {
|
||||||
FileDropManager._dropResult = result;
|
FileDropExtension.dropResult = result;
|
||||||
this._indicator.rect = result.rect;
|
FileDropExtension.indicator.rect = result.rect;
|
||||||
} else {
|
} else {
|
||||||
FileDropManager._dropResult = null;
|
FileDropExtension.dropResult = null;
|
||||||
this._indicator.rect = null;
|
FileDropExtension.indicator.rect = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
get doc() {
|
|
||||||
return this._blockService.doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
get editorHost(): EditorHost {
|
|
||||||
return this._blockService.std.host;
|
|
||||||
}
|
|
||||||
|
|
||||||
get targetModel(): BlockModel | null {
|
get targetModel(): BlockModel | null {
|
||||||
let targetModel = FileDropManager._dropResult?.modelState.model || null;
|
let targetModel = FileDropExtension.dropResult?.modelState.model || null;
|
||||||
|
|
||||||
if (!targetModel && isInsidePageEditor(this.editorHost)) {
|
if (!targetModel && isInsidePageEditor(this.editorHost)) {
|
||||||
const rootModel = this.doc.root;
|
const rootModel = this.doc.root;
|
||||||
assertExists(rootModel);
|
if (!rootModel) return null;
|
||||||
|
|
||||||
let lastNote = rootModel.children[rootModel.children.length - 1];
|
let lastNote = rootModel.children[rootModel.children.length - 1];
|
||||||
if (!lastNote || !matchFlavours(lastNote, ['affine:note'])) {
|
if (!lastNote || !matchFlavours(lastNote, ['affine:note'])) {
|
||||||
const newNoteId = this.doc.addBlock('affine:note', {}, rootModel.id);
|
const newNoteId = this.doc.addBlock('affine:note', {}, rootModel.id);
|
||||||
const newNote = this.doc.getBlockById(newNoteId);
|
const newNote = this.doc.getBlockById(newNoteId);
|
||||||
assertExists(newNote);
|
if (!newNote) return null;
|
||||||
lastNote = newNote;
|
lastNote = newNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,40 +112,93 @@ export class FileDropManager {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
const newParagraph = this.doc.getBlockById(newParagraphId);
|
const newParagraph = this.doc.getBlockById(newParagraphId);
|
||||||
assertExists(newParagraph);
|
if (!newParagraph) return null;
|
||||||
targetModel = newParagraph;
|
targetModel = newParagraph;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return targetModel;
|
return targetModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get doc() {
|
||||||
|
return this.std.doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
get editorHost(): EditorHost {
|
||||||
|
return this.std.host;
|
||||||
|
}
|
||||||
|
|
||||||
get type(): 'before' | 'after' {
|
get type(): 'before' | 'after' {
|
||||||
return !FileDropManager._dropResult ||
|
return !FileDropExtension.dropResult ||
|
||||||
FileDropManager._dropResult.type !== 'before'
|
FileDropExtension.dropResult.type !== 'before'
|
||||||
? 'after'
|
? 'after'
|
||||||
: 'before';
|
: 'before';
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(blockService: BlockService, fileDropOptions: FileDropOptions) {
|
private readonly _onDrop = (event: DragEvent, options: FileDropOptions) => {
|
||||||
this._blockService = blockService;
|
FileDropExtension.indicator.rect = null;
|
||||||
this._fileDropOptions = fileDropOptions;
|
|
||||||
|
|
||||||
this._indicator = document.querySelector(
|
const { onDrop } = options;
|
||||||
'affine-drag-indicator'
|
if (!onDrop) return;
|
||||||
) as DragIndicator;
|
|
||||||
if (!this._indicator) {
|
|
||||||
this._indicator = document.createElement(
|
|
||||||
'affine-drag-indicator'
|
|
||||||
) as DragIndicator;
|
|
||||||
document.body.append(this._indicator);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileDropOptions.onDrop) {
|
const dataTransfer = event.dataTransfer;
|
||||||
this._blockService.disposables.addFromEvent(
|
if (!dataTransfer) return;
|
||||||
this._blockService.std.host,
|
|
||||||
'drop',
|
const effectAllowed = dataTransfer.effectAllowed;
|
||||||
this._onDrop
|
if (effectAllowed === 'none') return;
|
||||||
);
|
|
||||||
|
const droppedFiles = dataTransfer.files;
|
||||||
|
if (!droppedFiles || !droppedFiles.length) return;
|
||||||
|
|
||||||
|
const { targetModel, type: place } = this;
|
||||||
|
const { x, y } = event;
|
||||||
|
|
||||||
|
const drop = onDrop({
|
||||||
|
std: this.std,
|
||||||
|
files: [...droppedFiles],
|
||||||
|
targetModel,
|
||||||
|
place,
|
||||||
|
point: [x, y],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (drop) {
|
||||||
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
return drop;
|
||||||
|
};
|
||||||
|
|
||||||
|
override mounted() {
|
||||||
|
super.mounted();
|
||||||
|
const std = this.std;
|
||||||
|
|
||||||
|
std.event.add('nativeDragOver', context => {
|
||||||
|
const event = context.get('dndState');
|
||||||
|
this.onDragOver(event.raw);
|
||||||
|
});
|
||||||
|
std.event.add('nativeDragLeave', () => {
|
||||||
|
this.onDragLeave();
|
||||||
|
});
|
||||||
|
std.provider.getAll(FileDropConfigExtensionIdentifier).forEach(options => {
|
||||||
|
if (options.onDrop) {
|
||||||
|
std.event.add('nativeDrop', context => {
|
||||||
|
const event = context.get('dndState');
|
||||||
|
return this._onDrop(event.raw, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FileDropConfigExtensionIdentifier = createIdentifier<FileDropOptions>(
|
||||||
|
'FileDropConfigExtension'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FileDropConfigExtension = (
|
||||||
|
options: FileDropOptions
|
||||||
|
): ExtensionType => {
|
||||||
|
const identifier = FileDropConfigExtensionIdentifier(options.flavour);
|
||||||
|
return {
|
||||||
|
setup: di => {
|
||||||
|
di.addImpl(identifier, () => options);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,47 +1,12 @@
|
|||||||
import type { Rect } from '@blocksuite/global/utils';
|
import { DragIndicator } from './drag-indicator.js';
|
||||||
import { css, html, LitElement } from 'lit';
|
export {
|
||||||
import { property } from 'lit/decorators.js';
|
FileDropConfigExtension,
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
FileDropExtension,
|
||||||
|
type FileDropOptions,
|
||||||
|
type onDropProps,
|
||||||
|
} from './file-drop-manager.js';
|
||||||
|
|
||||||
export class DragIndicator extends LitElement {
|
export { DragIndicator };
|
||||||
static override styles = css`
|
|
||||||
.affine-drag-indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background: var(--affine-primary-color);
|
|
||||||
transition-property: width, height, transform;
|
|
||||||
transition-duration: 100ms;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-delay: 0s;
|
|
||||||
transform-origin: 0 0;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
override render() {
|
|
||||||
if (!this.rect) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { left, top, width, height } = this.rect;
|
|
||||||
const style = styleMap({
|
|
||||||
width: `${width}px`,
|
|
||||||
height: `${height}px`,
|
|
||||||
transform: `translate(${left}px, ${top}px)`,
|
|
||||||
});
|
|
||||||
return html`<div class="affine-drag-indicator" style=${style}></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor rect: Rect | null = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'affine-drag-indicator': DragIndicator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function effects() {
|
export function effects() {
|
||||||
customElements.define('affine-drag-indicator', DragIndicator);
|
customElements.define('affine-drag-indicator', DragIndicator);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export * from './ai-item/index.js';
|
export * from './ai-item/index.js';
|
||||||
export * from './block-selection.js';
|
export * from './block-selection.js';
|
||||||
export * from './block-zero-width.js';
|
export * from './block-zero-width.js';
|
||||||
export * from './file-drop-manager.js';
|
|
||||||
export * from './menu-divider.js';
|
export * from './menu-divider.js';
|
||||||
export { scrollbarStyle } from './utils.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 { AttachmentBlockSchema } from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
DragHandleConfigExtension,
|
DragHandleConfigExtension,
|
||||||
@@ -13,65 +14,63 @@ import {
|
|||||||
import { BlockService } from '@blocksuite/block-std';
|
import { BlockService } from '@blocksuite/block-std';
|
||||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
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 { EMBED_CARD_HEIGHT, EMBED_CARD_WIDTH } from '../_common/consts.js';
|
||||||
import { addAttachments } from '../root-block/edgeless/utils/common.js';
|
import { addAttachments } from '../root-block/edgeless/utils/common.js';
|
||||||
import type { AttachmentBlockComponent } from './attachment-block.js';
|
import type { AttachmentBlockComponent } from './attachment-block.js';
|
||||||
import { AttachmentEdgelessBlockComponent } from './attachment-edgeless-block.js';
|
import { AttachmentEdgelessBlockComponent } from './attachment-edgeless-block.js';
|
||||||
import { addSiblingAttachmentBlocks } from './utils.js';
|
import { addSiblingAttachmentBlocks } from './utils.js';
|
||||||
|
|
||||||
|
// bytes.parse('2GB')
|
||||||
|
const maxFileSize = 2147483648;
|
||||||
|
|
||||||
export class AttachmentBlockService extends BlockService {
|
export class AttachmentBlockService extends BlockService {
|
||||||
static override readonly flavour = AttachmentBlockSchema.model.flavour;
|
static override readonly flavour = AttachmentBlockSchema.model.flavour;
|
||||||
|
|
||||||
private readonly _fileDropOptions: FileDropOptions = {
|
maxFileSize = maxFileSize;
|
||||||
flavour: this.flavour,
|
}
|
||||||
onDrop: async ({ files, targetModel, place, point }) => {
|
|
||||||
if (!files.length) return false;
|
|
||||||
|
|
||||||
// generic attachment block for all files except images
|
export const AttachmentDropOption = FileDropConfigExtension({
|
||||||
const attachmentFiles = files.filter(
|
flavour: AttachmentBlockSchema.model.flavour,
|
||||||
file => !file.type.startsWith('image/')
|
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'])) {
|
if (!attachmentFiles.length) return false;
|
||||||
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);
|
|
||||||
|
|
||||||
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
if (targetModel && !matchFlavours(targetModel, ['affine:surface'])) {
|
||||||
control: 'canvas:drop',
|
addSiblingAttachmentBlocks(
|
||||||
page: 'whiteboard editor',
|
std.host,
|
||||||
module: 'toolbar',
|
attachmentFiles,
|
||||||
segment: 'toolbar',
|
// TODO: use max file size from service
|
||||||
type: 'attachment',
|
maxFileSize,
|
||||||
});
|
targetModel,
|
||||||
}
|
place
|
||||||
|
).catch(console.error);
|
||||||
|
|
||||||
return true;
|
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 {
|
return true;
|
||||||
super.mounted();
|
}
|
||||||
|
|
||||||
this.fileDropManager = new FileDropManager(this, this._fileDropOptions);
|
return false;
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
export const AttachmentDragHandleOption = DragHandleConfigExtension({
|
export const AttachmentDragHandleOption = DragHandleConfigExtension({
|
||||||
flavour: AttachmentBlockSchema.model.flavour,
|
flavour: AttachmentBlockSchema.model.flavour,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-htm
|
|||||||
import {
|
import {
|
||||||
AttachmentBlockService,
|
AttachmentBlockService,
|
||||||
AttachmentDragHandleOption,
|
AttachmentDragHandleOption,
|
||||||
|
AttachmentDropOption,
|
||||||
} from './attachment-service.js';
|
} from './attachment-service.js';
|
||||||
import {
|
import {
|
||||||
AttachmentEmbedConfigExtension,
|
AttachmentEmbedConfigExtension,
|
||||||
@@ -23,6 +24,7 @@ export const AttachmentBlockSpec: ExtensionType[] = [
|
|||||||
? literal`affine-edgeless-attachment`
|
? literal`affine-edgeless-attachment`
|
||||||
: literal`affine-attachment`;
|
: literal`affine-attachment`;
|
||||||
}),
|
}),
|
||||||
|
AttachmentDropOption,
|
||||||
AttachmentDragHandleOption,
|
AttachmentDragHandleOption,
|
||||||
AttachmentEmbedConfigExtension(),
|
AttachmentEmbedConfigExtension(),
|
||||||
AttachmentEmbedService,
|
AttachmentEmbedService,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FileDropConfigExtension } from '@blocksuite/affine-components/drag-indicator';
|
||||||
import { ImageBlockSchema } from '@blocksuite/affine-model';
|
import { ImageBlockSchema } from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
DragHandleConfigExtension,
|
DragHandleConfigExtension,
|
||||||
@@ -13,64 +14,60 @@ import {
|
|||||||
import { BlockService } from '@blocksuite/block-std';
|
import { BlockService } from '@blocksuite/block-std';
|
||||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
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 { setImageProxyMiddlewareURL } from '../_common/transformers/middlewares.js';
|
||||||
import { addImages } from '../root-block/edgeless/utils/common.js';
|
import { addImages } from '../root-block/edgeless/utils/common.js';
|
||||||
import type { ImageBlockComponent } from './image-block.js';
|
import type { ImageBlockComponent } from './image-block.js';
|
||||||
import { ImageEdgelessBlockComponent } from './image-edgeless-block.js';
|
import { ImageEdgelessBlockComponent } from './image-edgeless-block.js';
|
||||||
import { addSiblingImageBlock } from './utils.js';
|
import { addSiblingImageBlock } from './utils.js';
|
||||||
|
|
||||||
|
// bytes.parse('2GB')
|
||||||
|
const maxFileSize = 2147483648;
|
||||||
|
|
||||||
export class ImageBlockService extends BlockService {
|
export class ImageBlockService extends BlockService {
|
||||||
static override readonly flavour = ImageBlockSchema.model.flavour;
|
static override readonly flavour = ImageBlockSchema.model.flavour;
|
||||||
|
|
||||||
static setImageProxyURL = setImageProxyMiddlewareURL;
|
static setImageProxyURL = setImageProxyMiddlewareURL;
|
||||||
|
|
||||||
private readonly _fileDropOptions: FileDropOptions = {
|
maxFileSize = maxFileSize;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
export const ImageDragHandleOption = DragHandleConfigExtension({
|
||||||
flavour: ImageBlockSchema.model.flavour,
|
flavour: ImageBlockSchema.model.flavour,
|
||||||
edgeless: true,
|
edgeless: true,
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import { literal } from 'lit/static-html.js';
|
|||||||
|
|
||||||
import { ImageBlockAdapterExtensions } from './adapters/extension.js';
|
import { ImageBlockAdapterExtensions } from './adapters/extension.js';
|
||||||
import { commands } from './commands/index.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[] = [
|
export const ImageBlockSpec: ExtensionType[] = [
|
||||||
FlavourExtension('affine:image'),
|
FlavourExtension('affine:image'),
|
||||||
@@ -29,6 +33,7 @@ export const ImageBlockSpec: ExtensionType[] = [
|
|||||||
imageToolbar: literal`affine-image-toolbar-widget`,
|
imageToolbar: literal`affine-image-toolbar-widget`,
|
||||||
}),
|
}),
|
||||||
ImageDragHandleOption,
|
ImageDragHandleOption,
|
||||||
|
ImageDropOption,
|
||||||
ImageSelectionExtension,
|
ImageSelectionExtension,
|
||||||
ImageBlockAdapterExtensions,
|
ImageBlockAdapterExtensions,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FileDropExtension } from '@blocksuite/affine-components/drag-indicator';
|
||||||
import {
|
import {
|
||||||
DNDAPIExtension,
|
DNDAPIExtension,
|
||||||
DocDisplayMetaService,
|
DocDisplayMetaService,
|
||||||
@@ -98,6 +99,7 @@ const EdgelessCommonExtension: ExtensionType[] = [
|
|||||||
DNDAPIExtension,
|
DNDAPIExtension,
|
||||||
DocDisplayMetaService,
|
DocDisplayMetaService,
|
||||||
RootBlockAdapterExtensions,
|
RootBlockAdapterExtensions,
|
||||||
|
FileDropExtension,
|
||||||
].flat();
|
].flat();
|
||||||
|
|
||||||
export const EdgelessRootBlockSpec: ExtensionType[] = [
|
export const EdgelessRootBlockSpec: ExtensionType[] = [
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FileDropExtension } from '@blocksuite/affine-components/drag-indicator';
|
||||||
import {
|
import {
|
||||||
DNDAPIExtension,
|
DNDAPIExtension,
|
||||||
DocDisplayMetaService,
|
DocDisplayMetaService,
|
||||||
@@ -75,4 +76,5 @@ export const PageRootBlockSpec: ExtensionType[] = [
|
|||||||
DNDAPIExtension,
|
DNDAPIExtension,
|
||||||
DocDisplayMetaService,
|
DocDisplayMetaService,
|
||||||
RootBlockAdapterExtensions,
|
RootBlockAdapterExtensions,
|
||||||
|
FileDropExtension,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import { RootBlockSchema } from '@blocksuite/affine-model';
|
|||||||
import type { BlockComponent } from '@blocksuite/block-std';
|
import type { BlockComponent } from '@blocksuite/block-std';
|
||||||
import { BlockService } from '@blocksuite/block-std';
|
import { BlockService } from '@blocksuite/block-std';
|
||||||
|
|
||||||
import {
|
|
||||||
FileDropManager,
|
|
||||||
type FileDropOptions,
|
|
||||||
} from '../_common/components/file-drop-manager.js';
|
|
||||||
import {
|
import {
|
||||||
HtmlTransformer,
|
HtmlTransformer,
|
||||||
MarkdownTransformer,
|
MarkdownTransformer,
|
||||||
@@ -16,12 +12,6 @@ import type { RootBlockComponent } from './types.js';
|
|||||||
export abstract class RootService extends BlockService {
|
export abstract class RootService extends BlockService {
|
||||||
static override readonly flavour = RootBlockSchema.model.flavour;
|
static override readonly flavour = RootBlockSchema.model.flavour;
|
||||||
|
|
||||||
private readonly _fileDropOptions: FileDropOptions = {
|
|
||||||
flavour: this.flavour,
|
|
||||||
};
|
|
||||||
|
|
||||||
readonly fileDropManager = new FileDropManager(this, this._fileDropOptions);
|
|
||||||
|
|
||||||
transformers = {
|
transformers = {
|
||||||
markdown: MarkdownTransformer,
|
markdown: MarkdownTransformer,
|
||||||
html: HtmlTransformer,
|
html: HtmlTransformer,
|
||||||
@@ -64,18 +54,6 @@ export abstract class RootService extends BlockService {
|
|||||||
override mounted() {
|
override mounted() {
|
||||||
super.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.disposables.add(
|
||||||
this.std.event.add('pointerDown', ctx => {
|
this.std.event.add('pointerDown', ctx => {
|
||||||
const state = ctx.get('pointerState');
|
const state = ctx.get('pointerState');
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ export class DragEventWatcher {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly _dropHandler = (context: UIEventStateContext) => {
|
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._onDrop(context);
|
||||||
this._std.selection.setGroup('gfx', []);
|
this._std.selection.setGroup('gfx', []);
|
||||||
this.widget.clearRaf();
|
this.widget.clearRaf();
|
||||||
|
|||||||
@@ -272,6 +272,22 @@ class DragController extends PointerControllerBase {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly _nativeDragOver = (event: DragEvent) => {
|
||||||
|
const dndEventState = new DndEventState({ event });
|
||||||
|
this._dispatcher.run(
|
||||||
|
'nativeDragOver',
|
||||||
|
this._createContext(event, dndEventState)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _nativeDragLeave = (event: DragEvent) => {
|
||||||
|
const dndEventState = new DndEventState({ event });
|
||||||
|
this._dispatcher.run(
|
||||||
|
'nativeDragLeave',
|
||||||
|
this._createContext(event, dndEventState)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private readonly _nativeDrop = (event: DragEvent) => {
|
private readonly _nativeDrop = (event: DragEvent) => {
|
||||||
this._reset();
|
this._reset();
|
||||||
this._nativeDragging = false;
|
this._nativeDragging = false;
|
||||||
@@ -354,6 +370,8 @@ class DragController extends PointerControllerBase {
|
|||||||
disposables.addFromEvent(host, 'dragend', this._nativeDragEnd);
|
disposables.addFromEvent(host, 'dragend', this._nativeDragEnd);
|
||||||
disposables.addFromEvent(host, 'drag', this._nativeDragMove);
|
disposables.addFromEvent(host, 'drag', this._nativeDragMove);
|
||||||
disposables.addFromEvent(host, 'drop', this._nativeDrop);
|
disposables.addFromEvent(host, 'drop', this._nativeDrop);
|
||||||
|
disposables.addFromEvent(host, 'dragover', this._nativeDragOver);
|
||||||
|
disposables.addFromEvent(host, 'dragleave', this._nativeDragLeave);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ const eventNames = [
|
|||||||
'nativeDragMove',
|
'nativeDragMove',
|
||||||
'nativeDragEnd',
|
'nativeDragEnd',
|
||||||
'nativeDrop',
|
'nativeDrop',
|
||||||
|
'nativeDragOver',
|
||||||
|
'nativeDragLeave',
|
||||||
|
|
||||||
...bypassEventNames,
|
...bypassEventNames,
|
||||||
] as const;
|
] as const;
|
||||||
@@ -175,12 +177,22 @@ export class UIEventDispatcher extends LifeCycleWatcher {
|
|||||||
this._setActive(false);
|
this._setActive(false);
|
||||||
});
|
});
|
||||||
this.disposables.addFromEvent(this.host, 'dragover', () => {
|
this.disposables.addFromEvent(this.host, 'dragover', () => {
|
||||||
|
_dragging = true;
|
||||||
|
this._setActive(true);
|
||||||
|
});
|
||||||
|
this.disposables.addFromEvent(this.host, 'dragenter', () => {
|
||||||
|
_dragging = true;
|
||||||
|
this._setActive(true);
|
||||||
|
});
|
||||||
|
this.disposables.addFromEvent(this.host, 'dragstart', () => {
|
||||||
|
_dragging = true;
|
||||||
this._setActive(true);
|
this._setActive(true);
|
||||||
});
|
});
|
||||||
this.disposables.addFromEvent(this.host, 'dragend', () => {
|
this.disposables.addFromEvent(this.host, 'dragend', () => {
|
||||||
this._setActive(false);
|
_dragging = false;
|
||||||
});
|
});
|
||||||
this.disposables.addFromEvent(this.host, 'drop', () => {
|
this.disposables.addFromEvent(this.host, 'drop', () => {
|
||||||
|
_dragging = false;
|
||||||
this._setActive(true);
|
this._setActive(true);
|
||||||
});
|
});
|
||||||
this.disposables.addFromEvent(this.host, 'pointerenter', () => {
|
this.disposables.addFromEvent(this.host, 'pointerenter', () => {
|
||||||
|
|||||||
@@ -200,30 +200,30 @@ test('move to the last block of each level in multi-level nesting', async ({
|
|||||||
`${testInfo.title}_drag_3_9.json`
|
`${testInfo.title}_drag_3_9.json`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await dragHandleFromBlockToBlockBottomById(
|
||||||
|
page,
|
||||||
|
'4',
|
||||||
|
'3',
|
||||||
|
true,
|
||||||
|
-(1 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT)
|
||||||
|
);
|
||||||
|
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
|
||||||
|
|
||||||
|
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
||||||
|
`${testInfo.title}_drag_4_3.json`
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertRichTexts(page, ['C', 'D', 'E', 'F', 'G', 'A', 'B']);
|
||||||
|
await dragHandleFromBlockToBlockBottomById(
|
||||||
|
page,
|
||||||
|
'3',
|
||||||
|
'4',
|
||||||
|
true,
|
||||||
|
-(2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT)
|
||||||
|
);
|
||||||
|
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
|
||||||
|
|
||||||
// FIXME(DND)
|
// FIXME(DND)
|
||||||
// await dragHandleFromBlockToBlockBottomById(
|
|
||||||
// page,
|
|
||||||
// '4',
|
|
||||||
// '3',
|
|
||||||
// true,
|
|
||||||
// -(1 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT)
|
|
||||||
// );
|
|
||||||
// await expect(page.locator('.affine-drag-indicator')).toBeHidden();
|
|
||||||
//
|
|
||||||
// expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
|
||||||
// `${testInfo.title}_drag_4_3.json`
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// await assertRichTexts(page, ['C', 'D', 'E', 'F', 'G', 'A', 'B']);
|
|
||||||
// await dragHandleFromBlockToBlockBottomById(
|
|
||||||
// page,
|
|
||||||
// '3',
|
|
||||||
// '4',
|
|
||||||
// true,
|
|
||||||
// -(2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT)
|
|
||||||
// );
|
|
||||||
// await expect(page.locator('.affine-drag-indicator')).toBeHidden();
|
|
||||||
//
|
|
||||||
// expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
// expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
||||||
// `${testInfo.title}_drag_3_4.json`
|
// `${testInfo.title}_drag_3_4.json`
|
||||||
// );
|
// );
|
||||||
|
|||||||
@@ -259,10 +259,10 @@ test.describe('Embed synced doc', () => {
|
|||||||
'.affine-database-column-header.database-row'
|
'.affine-database-column-header.database-row'
|
||||||
);
|
);
|
||||||
await databaseFirstCell.click({ force: true });
|
await databaseFirstCell.click({ force: true });
|
||||||
const indicatorCount = await page
|
const selectedCount = await page
|
||||||
.locator('affine-drag-indicator')
|
.locator('.affine-embed-synced-doc-container.selected')
|
||||||
.count();
|
.count();
|
||||||
expect(indicatorCount).toBe(1);
|
expect(selectedCount).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user