feat(editor): edgeless clipboard config extension (#11168)
This commit is contained in:
@@ -1,9 +1,13 @@
|
|||||||
import { addAttachments } from '@blocksuite/affine-block-attachment';
|
import { addAttachments } from '@blocksuite/affine-block-attachment';
|
||||||
|
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
|
||||||
import { addImages } from '@blocksuite/affine-block-image';
|
import { addImages } from '@blocksuite/affine-block-image';
|
||||||
import {
|
import {
|
||||||
CanvasElementType,
|
CanvasElementType,
|
||||||
|
type ClipboardConfigCreationContext,
|
||||||
|
EdgelessClipboardConfigIdentifier,
|
||||||
EdgelessCRUDIdentifier,
|
EdgelessCRUDIdentifier,
|
||||||
ExportManager,
|
ExportManager,
|
||||||
|
getSurfaceComponent,
|
||||||
SurfaceGroupLikeModel,
|
SurfaceGroupLikeModel,
|
||||||
TextUtils,
|
TextUtils,
|
||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
@@ -14,7 +18,6 @@ import {
|
|||||||
DEFAULT_NOTE_WIDTH,
|
DEFAULT_NOTE_WIDTH,
|
||||||
FrameBlockModel,
|
FrameBlockModel,
|
||||||
MAX_IMAGE_WIDTH,
|
MAX_IMAGE_WIDTH,
|
||||||
ReferenceInfoSchema,
|
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
CANVAS_EXPORT_IGNORE_TAGS,
|
CANVAS_EXPORT_IGNORE_TAGS,
|
||||||
@@ -34,6 +37,7 @@ import {
|
|||||||
referenceToNode,
|
referenceToNode,
|
||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
import type {
|
import type {
|
||||||
|
BlockComponent,
|
||||||
BlockStdScope,
|
BlockStdScope,
|
||||||
EditorHost,
|
EditorHost,
|
||||||
SurfaceSelection,
|
SurfaceSelection,
|
||||||
@@ -63,7 +67,6 @@ import { assertType } from '@blocksuite/global/utils';
|
|||||||
import {
|
import {
|
||||||
type BlockSnapshot,
|
type BlockSnapshot,
|
||||||
BlockSnapshotSchema,
|
BlockSnapshotSchema,
|
||||||
fromJSON,
|
|
||||||
type SliceSnapshot,
|
type SliceSnapshot,
|
||||||
} from '@blocksuite/store';
|
} from '@blocksuite/store';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
@@ -75,15 +78,12 @@ import {
|
|||||||
decodeClipboardBlobs,
|
decodeClipboardBlobs,
|
||||||
encodeClipboardBlobs,
|
encodeClipboardBlobs,
|
||||||
} from '../../clipboard/utils.js';
|
} from '../../clipboard/utils.js';
|
||||||
import type { RootBlockComponent } from '../../types.js';
|
|
||||||
import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js';
|
|
||||||
import { edgelessElementsBoundFromRawData } from '../utils/bound-utils.js';
|
import { edgelessElementsBoundFromRawData } from '../utils/bound-utils.js';
|
||||||
import { createNewPresentationIndexes } from '../utils/clipboard-utils.js';
|
import { createNewPresentationIndexes } from '../utils/clipboard-utils.js';
|
||||||
import {
|
import {
|
||||||
getSortedCloneElements,
|
getSortedCloneElements,
|
||||||
serializeElement,
|
serializeElement,
|
||||||
} from '../utils/clone-utils.js';
|
} from '../utils/clone-utils.js';
|
||||||
import { deleteElements } from '../utils/crud.js';
|
|
||||||
import {
|
import {
|
||||||
isAttachmentBlock,
|
isAttachmentBlock,
|
||||||
isCanvasElementWithText,
|
isCanvasElementWithText,
|
||||||
@@ -95,41 +95,13 @@ const BLOCKSUITE_SURFACE = 'blocksuite/surface';
|
|||||||
const { GROUP, MINDMAP, CONNECTOR } = CanvasElementType;
|
const { GROUP, MINDMAP, CONNECTOR } = CanvasElementType;
|
||||||
const IMAGE_PADDING = 5; // for rotated shapes some padding is needed
|
const IMAGE_PADDING = 5; // for rotated shapes some padding is needed
|
||||||
|
|
||||||
type CreationContext = {
|
|
||||||
/**
|
|
||||||
* element old id to new id
|
|
||||||
*/
|
|
||||||
oldToNewIdMap: Map<string, string>;
|
|
||||||
/**
|
|
||||||
* element old id to new layer index
|
|
||||||
*/
|
|
||||||
originalIndexes: Map<string, string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* frame old id to new presentation index
|
|
||||||
*/
|
|
||||||
newPresentationIndexes: Map<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type BlockCreationFunction = (
|
|
||||||
snapshot: BlockSnapshot,
|
|
||||||
context: CreationContext
|
|
||||||
) => Promise<string | null> | string | null; // new Id
|
|
||||||
|
|
||||||
interface CanvasExportOptions {
|
interface CanvasExportOptions {
|
||||||
dpr?: number;
|
dpr?: number;
|
||||||
padding?: number;
|
padding?: number;
|
||||||
background?: string;
|
background?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BlockConfig {
|
|
||||||
flavour: string;
|
|
||||||
createFunction: BlockCreationFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EdgelessClipboardController extends PageClipboard {
|
export class EdgelessClipboardController extends PageClipboard {
|
||||||
private readonly _blockConfigs: BlockConfig[] = [];
|
|
||||||
|
|
||||||
private readonly _initEdgelessClipboard = () => {
|
private readonly _initEdgelessClipboard = () => {
|
||||||
this.host.handleEvent(
|
this.host.handleEvent(
|
||||||
'copy',
|
'copy',
|
||||||
@@ -210,7 +182,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
this.selectionManager.selectedElements
|
this.selectionManager.selectedElements
|
||||||
);
|
);
|
||||||
this.doc.transact(() => {
|
this.doc.transact(() => {
|
||||||
deleteElements(this.edgeless, elements);
|
this.crud.deleteElements(elements);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.selectionManager.set({
|
this.selectionManager.set({
|
||||||
@@ -241,6 +213,8 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
const data = event.clipboardData;
|
const data = event.clipboardData;
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
|
if (!this.surface) return;
|
||||||
|
|
||||||
const lastMousePos = this.toolManager.lastMousePos$.peek();
|
const lastMousePos = this.toolManager.lastMousePos$.peek();
|
||||||
const point: IVec = [lastMousePos.x, lastMousePos.y];
|
const point: IVec = [lastMousePos.x, lastMousePos.y];
|
||||||
|
|
||||||
@@ -283,7 +257,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
if (isUrlInClipboard(data)) {
|
if (isUrlInClipboard(data)) {
|
||||||
const url = data.getData('text/plain');
|
const url = data.getData('text/plain');
|
||||||
const lastMousePos = this.toolManager.lastMousePos$.peek();
|
const lastMousePos = this.toolManager.lastMousePos$.peek();
|
||||||
const [x, y] = this.host.service.viewport.toModelCoord(
|
const [x, y] = this.gfx.viewport.toModelCoord(
|
||||||
lastMousePos.x,
|
lastMousePos.x,
|
||||||
lastMousePos.y
|
lastMousePos.y
|
||||||
);
|
);
|
||||||
@@ -407,7 +381,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get selectionManager() {
|
private get selectionManager() {
|
||||||
return this.std.get(GfxControllerIdentifier).selection;
|
return this.gfx.selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get std() {
|
private get std() {
|
||||||
@@ -415,7 +389,15 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get surface() {
|
private get surface() {
|
||||||
return this.host.surface;
|
return getSurfaceComponent(this.std);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get frame() {
|
||||||
|
return this.std.get(EdgelessFrameManagerIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get gfx() {
|
||||||
|
return this.std.get(GfxControllerIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get crud() {
|
private get crud() {
|
||||||
@@ -423,36 +405,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get toolManager() {
|
private get toolManager() {
|
||||||
return this.host.gfx.tool;
|
return this.gfx.tool;
|
||||||
}
|
|
||||||
|
|
||||||
constructor(public override host: EdgelessRootBlockComponent) {
|
|
||||||
super(host);
|
|
||||||
// Register existing block creation functions
|
|
||||||
this.registerBlock('affine:note', this._createNoteBlock);
|
|
||||||
this.registerBlock('affine:edgeless-text', this._createEdgelessTextBlock);
|
|
||||||
this.registerBlock('affine:image', this._createImageBlock);
|
|
||||||
this.registerBlock('affine:frame', this._createFrameBlock);
|
|
||||||
this.registerBlock('affine:attachment', this._createAttachmentBlock);
|
|
||||||
|
|
||||||
// external links
|
|
||||||
this.registerBlock('affine:bookmark', this._createBookmarkBlock);
|
|
||||||
this.registerBlock('affine:embed-figma', this._createFigmaEmbedBlock);
|
|
||||||
this.registerBlock('affine:embed-github', this._createGithubEmbedBlock);
|
|
||||||
this.registerBlock('affine:embed-html', this._createHtmlEmbedBlock);
|
|
||||||
this.registerBlock('affine:embed-loom', this._createLoomEmbedBlock);
|
|
||||||
this.registerBlock('affine:embed-youtube', this._createYoutubeEmbedBlock);
|
|
||||||
this.registerBlock('affine:embed-iframe', this._createIframeEmbedBlock);
|
|
||||||
|
|
||||||
// internal links
|
|
||||||
this.registerBlock(
|
|
||||||
'affine:embed-linked-doc',
|
|
||||||
this._createLinkedDocEmbedBlock
|
|
||||||
);
|
|
||||||
this.registerBlock(
|
|
||||||
'affine:embed-synced-doc',
|
|
||||||
this._createSyncedDocEmbedBlock
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _checkCanContinueToCanvas(
|
private _checkCanContinueToCanvas(
|
||||||
@@ -468,54 +421,9 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createAttachmentBlock(attachment: BlockSnapshot) {
|
|
||||||
const { xywh, rotate, sourceId, name, size, type, embed, style } =
|
|
||||||
attachment.props;
|
|
||||||
|
|
||||||
if (!(await this.host.std.workspace.blobSync.get(sourceId as string))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const attachmentId = this.crud.addBlock(
|
|
||||||
'affine:attachment',
|
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
rotate,
|
|
||||||
sourceId,
|
|
||||||
name,
|
|
||||||
size,
|
|
||||||
type,
|
|
||||||
embed,
|
|
||||||
style,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
return attachmentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createBookmarkBlock(bookmark: BlockSnapshot) {
|
|
||||||
const { xywh, style, url, caption, description, icon, image, title } =
|
|
||||||
bookmark.props;
|
|
||||||
|
|
||||||
const bookmarkId = this.crud.addBlock(
|
|
||||||
'affine:bookmark',
|
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
style,
|
|
||||||
url,
|
|
||||||
caption,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
image,
|
|
||||||
title,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
return bookmarkId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createCanvasElement(
|
private _createCanvasElement(
|
||||||
clipboardData: SerializedElement,
|
clipboardData: SerializedElement,
|
||||||
context: CreationContext,
|
context: ClipboardConfigCreationContext,
|
||||||
newXYWH: SerializedXYWH
|
newXYWH: SerializedXYWH
|
||||||
): GfxPrimitiveElementModel | null {
|
): GfxPrimitiveElementModel | null {
|
||||||
if (clipboardData.type === GROUP) {
|
if (clipboardData.type === GROUP) {
|
||||||
@@ -614,307 +522,8 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createEdgelessTextBlock(edgelessText: BlockSnapshot) {
|
|
||||||
const oldId = edgelessText.id;
|
|
||||||
delete edgelessText.props.index;
|
|
||||||
if (!edgelessText.props.xywh) {
|
|
||||||
console.error(
|
|
||||||
`EdgelessText block(id: ${oldId}) does not have xywh property`
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const newId = await this.onBlockSnapshotPaste(
|
|
||||||
edgelessText,
|
|
||||||
this.doc,
|
|
||||||
this.edgeless.surface.model.id
|
|
||||||
);
|
|
||||||
if (!newId) {
|
|
||||||
console.error(`Failed to paste EdgelessText block(id: ${oldId})`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createFigmaEmbedBlock(figmaEmbed: BlockSnapshot) {
|
|
||||||
const { xywh, style, url, caption, title, description } = figmaEmbed.props;
|
|
||||||
|
|
||||||
const embedFigmaId = this.crud.addBlock(
|
|
||||||
'affine:embed-figma',
|
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
style,
|
|
||||||
url,
|
|
||||||
caption,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
return embedFigmaId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createFrameBlock(frame: BlockSnapshot, context: CreationContext) {
|
|
||||||
const { oldToNewIdMap, newPresentationIndexes } = context;
|
|
||||||
const { xywh, title, background, childElementIds } = frame.props;
|
|
||||||
|
|
||||||
const newChildElementIds: Record<string, boolean> = {};
|
|
||||||
|
|
||||||
if (typeof childElementIds === 'object' && childElementIds !== null) {
|
|
||||||
Object.keys(childElementIds).forEach(oldId => {
|
|
||||||
const newId = oldToNewIdMap.get(oldId);
|
|
||||||
if (newId) {
|
|
||||||
newChildElementIds[newId] = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const frameId = this.crud.addBlock(
|
|
||||||
'affine:frame',
|
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
background,
|
|
||||||
title: fromJSON(title),
|
|
||||||
childElementIds: newChildElementIds,
|
|
||||||
presentationIndex: newPresentationIndexes.get(frame.id),
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
return frameId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createGithubEmbedBlock(githubEmbed: BlockSnapshot) {
|
|
||||||
const {
|
|
||||||
xywh,
|
|
||||||
style,
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
githubType,
|
|
||||||
githubId,
|
|
||||||
url,
|
|
||||||
caption,
|
|
||||||
image,
|
|
||||||
status,
|
|
||||||
statusReason,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
createdAt,
|
|
||||||
assignees,
|
|
||||||
} = githubEmbed.props;
|
|
||||||
|
|
||||||
const embedGithubId = this.crud.addBlock(
|
|
||||||
'affine:embed-github',
|
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
style,
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
githubType,
|
|
||||||
githubId,
|
|
||||||
url,
|
|
||||||
caption,
|
|
||||||
image,
|
|
||||||
status,
|
|
||||||
statusReason,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
createdAt,
|
|
||||||
assignees,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
return embedGithubId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createHtmlEmbedBlock(htmlEmbed: BlockSnapshot) {
|
|
||||||
const { xywh, style, caption, html, design } = htmlEmbed.props;
|
|
||||||
|
|
||||||
const embedHtmlId = this.crud.addBlock(
|
|
||||||
'affine:embed-html',
|
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
style,
|
|
||||||
caption,
|
|
||||||
html,
|
|
||||||
design,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
return embedHtmlId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _createImageBlock(image: BlockSnapshot) {
|
|
||||||
const { xywh, rotate, sourceId, size, width, height, caption } =
|
|
||||||
image.props;
|
|
||||||
|
|
||||||
if (!(await this.host.std.workspace.blobSync.get(sourceId as string))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.crud.addBlock(
|
|
||||||
'affine:image',
|
|
||||||
{
|
|
||||||
caption,
|
|
||||||
sourceId,
|
|
||||||
xywh,
|
|
||||||
rotate,
|
|
||||||
size,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createLinkedDocEmbedBlock(linkedDocEmbed: BlockSnapshot) {
|
|
||||||
const { xywh, style, caption, pageId, params, title, description } =
|
|
||||||
linkedDocEmbed.props;
|
|
||||||
const referenceInfo = ReferenceInfoSchema.parse({
|
|
||||||
pageId,
|
|
||||||
params,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.crud.addBlock(
|
|
||||||
'affine:embed-linked-doc',
|
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
style,
|
|
||||||
caption,
|
|
||||||
...referenceInfo,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createLoomEmbedBlock(loomEmbed: BlockSnapshot) {
|
|
||||||
const { xywh, style, url, caption, videoId, image, title, description } =
|
|
||||||
loomEmbed.props;
|
|
||||||
|
|
||||||
const embedLoomId = this.crud.addBlock(
|
|
||||||
'affine:embed-loom',
|
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
style,
|
|
||||||
url,
|
|
||||||
caption,
|
|
||||||
videoId,
|
|
||||||
image,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
return embedLoomId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _createNoteBlock(note: BlockSnapshot) {
|
|
||||||
const oldId = note.id;
|
|
||||||
|
|
||||||
delete note.props.index;
|
|
||||||
if (!note.props.xywh) {
|
|
||||||
console.error(`Note block(id: ${oldId}) does not have xywh property`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newId = await this.onBlockSnapshotPaste(
|
|
||||||
note,
|
|
||||||
this.doc,
|
|
||||||
this.doc.root!.id
|
|
||||||
);
|
|
||||||
if (!newId) {
|
|
||||||
console.error(`Failed to paste note block(id: ${oldId})`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createSyncedDocEmbedBlock(syncedDocEmbed: BlockSnapshot) {
|
|
||||||
const { xywh, style, caption, scale, pageId, params } =
|
|
||||||
syncedDocEmbed.props;
|
|
||||||
const referenceInfo = ReferenceInfoSchema.parse({ pageId, params });
|
|
||||||
|
|
||||||
return this.crud.addBlock(
|
|
||||||
'affine:embed-synced-doc',
|
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
style,
|
|
||||||
caption,
|
|
||||||
scale,
|
|
||||||
...referenceInfo,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createYoutubeEmbedBlock(youtubeEmbed: BlockSnapshot) {
|
|
||||||
const {
|
|
||||||
xywh,
|
|
||||||
style,
|
|
||||||
url,
|
|
||||||
caption,
|
|
||||||
videoId,
|
|
||||||
image,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
creator,
|
|
||||||
creatorUrl,
|
|
||||||
creatorImage,
|
|
||||||
} = youtubeEmbed.props;
|
|
||||||
|
|
||||||
const embedYoutubeId = this.crud.addBlock(
|
|
||||||
'affine:embed-youtube',
|
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
style,
|
|
||||||
url,
|
|
||||||
caption,
|
|
||||||
videoId,
|
|
||||||
image,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
creator,
|
|
||||||
creatorUrl,
|
|
||||||
creatorImage,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
return embedYoutubeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createIframeEmbedBlock(embedIframe: BlockSnapshot) {
|
|
||||||
const {
|
|
||||||
xywh,
|
|
||||||
caption,
|
|
||||||
url,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
iframeUrl,
|
|
||||||
scale,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
} = embedIframe.props;
|
|
||||||
|
|
||||||
return this.crud.addBlock(
|
|
||||||
'affine:embed-iframe',
|
|
||||||
{
|
|
||||||
url,
|
|
||||||
iframeUrl,
|
|
||||||
xywh,
|
|
||||||
caption,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
scale,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
},
|
|
||||||
this.surface.model.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _edgelessToCanvas(
|
private async _edgelessToCanvas(
|
||||||
edgeless: EdgelessRootBlockComponent,
|
edgeless: BlockComponent,
|
||||||
bound: IBound,
|
bound: IBound,
|
||||||
nodes?: GfxBlockElementModel[],
|
nodes?: GfxBlockElementModel[],
|
||||||
canvasElements: GfxPrimitiveElementModel[] = [],
|
canvasElements: GfxPrimitiveElementModel[] = [],
|
||||||
@@ -930,18 +539,11 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
|
|
||||||
const html2canvas = (await import('html2canvas')).default;
|
const html2canvas = (await import('html2canvas')).default;
|
||||||
if (!(html2canvas instanceof Function)) return;
|
if (!(html2canvas instanceof Function)) return;
|
||||||
|
if (!this.surface) return;
|
||||||
|
|
||||||
const pathname = location.pathname;
|
const pathname = location.pathname;
|
||||||
const editorMode = isInsidePageEditor(host);
|
const editorMode = isInsidePageEditor(host);
|
||||||
|
|
||||||
const rootComponent = getRootByEditorHost(host);
|
|
||||||
if (!rootComponent) return;
|
|
||||||
|
|
||||||
const container = rootComponent.querySelector(
|
|
||||||
'.affine-block-children-container'
|
|
||||||
);
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = (bound.w + padding * 2) * dpr;
|
canvas.width = (bound.w + padding * 2) * dpr;
|
||||||
canvas.height = (bound.h + padding * 2) * dpr;
|
canvas.height = (bound.h + padding * 2) * dpr;
|
||||||
@@ -1024,7 +626,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
|
|
||||||
const nodeElements =
|
const nodeElements =
|
||||||
nodes ??
|
nodes ??
|
||||||
(edgeless.service.gfx.getElementsByBound(bound, {
|
(this.gfx.getElementsByBound(bound, {
|
||||||
type: 'block',
|
type: 'block',
|
||||||
}) as GfxBlockElementModel[]);
|
}) as GfxBlockElementModel[]);
|
||||||
for (const nodeElement of nodeElements) {
|
for (const nodeElement of nodeElements) {
|
||||||
@@ -1032,15 +634,13 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
|
|
||||||
if (matchModels(nodeElement, [FrameBlockModel])) {
|
if (matchModels(nodeElement, [FrameBlockModel])) {
|
||||||
const blocksInsideFrame: GfxBlockElementModel[] = [];
|
const blocksInsideFrame: GfxBlockElementModel[] = [];
|
||||||
this.edgeless.service.frame
|
this.frame.getElementsInFrameBound(nodeElement, false).forEach(ele => {
|
||||||
.getElementsInFrameBound(nodeElement, false)
|
if (isTopLevelBlock(ele)) {
|
||||||
.forEach(ele => {
|
blocksInsideFrame.push(ele as GfxBlockElementModel);
|
||||||
if (isTopLevelBlock(ele)) {
|
} else {
|
||||||
blocksInsideFrame.push(ele as GfxBlockElementModel);
|
canvasElements.push(ele as GfxPrimitiveElementModel);
|
||||||
} else {
|
}
|
||||||
canvasElements.push(ele as GfxPrimitiveElementModel);
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||||
for (let i = 0; i < blocksInsideFrame.length; i++) {
|
for (let i = 0; i < blocksInsideFrame.length; i++) {
|
||||||
@@ -1052,7 +652,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
this._checkCanContinueToCanvas(host, pathname, editorMode);
|
this._checkCanContinueToCanvas(host, pathname, editorMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const surfaceCanvas = edgeless.surface.renderer.getCanvasByBound(
|
const surfaceCanvas = this.surface.renderer.getCanvasByBound(
|
||||||
bound,
|
bound,
|
||||||
canvasElements
|
canvasElements
|
||||||
);
|
);
|
||||||
@@ -1114,9 +714,8 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _pasteTextContentAsNote(content: BlockSnapshot[] | string) {
|
private async _pasteTextContentAsNote(content: BlockSnapshot[] | string) {
|
||||||
const edgeless = this.host;
|
|
||||||
const lastMousePos = this.toolManager.lastMousePos$.peek();
|
const lastMousePos = this.toolManager.lastMousePos$.peek();
|
||||||
const [x, y] = edgeless.service.viewport.toModelCoord(
|
const [x, y] = this.gfx.viewport.toModelCoord(
|
||||||
lastMousePos.x,
|
lastMousePos.x,
|
||||||
lastMousePos.y
|
lastMousePos.y
|
||||||
);
|
);
|
||||||
@@ -1167,11 +766,11 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeless.service.selection.set({
|
this.gfx.selection.set({
|
||||||
elements: [noteId],
|
elements: [noteId],
|
||||||
editing: false,
|
editing: false,
|
||||||
});
|
});
|
||||||
edgeless.gfx.tool.setTool('default');
|
this.gfx.tool.setTool('default');
|
||||||
}
|
}
|
||||||
|
|
||||||
private _replaceRichTextWithSvgElement(element: HTMLElement) {
|
private _replaceRichTextWithSvgElement(element: HTMLElement) {
|
||||||
@@ -1221,7 +820,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const idxGenerator = this.edgeless.service.layer.createIndexGenerator();
|
const idxGenerator = this.gfx.layer.createIndexGenerator();
|
||||||
const sortedElements = elements.sort(compare);
|
const sortedElements = elements.sort(compare);
|
||||||
sortedElements.forEach(ele => {
|
sortedElements.forEach(ele => {
|
||||||
const newIndex = idxGenerator();
|
const newIndex = idxGenerator();
|
||||||
@@ -1250,7 +849,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
const lastMousePos = this.toolManager.lastMousePos$.peek();
|
const lastMousePos = this.toolManager.lastMousePos$.peek();
|
||||||
pasteCenter =
|
pasteCenter =
|
||||||
pasteCenter ??
|
pasteCenter ??
|
||||||
this.host.service.viewport.toModelCoord(lastMousePos.x, lastMousePos.y);
|
this.gfx.viewport.toModelCoord(lastMousePos.x, lastMousePos.y);
|
||||||
const [modelX, modelY] = pasteCenter;
|
const [modelX, modelY] = pasteCenter;
|
||||||
oldCommonBound = edgelessElementsBoundFromRawData(elementsRawData);
|
oldCommonBound = edgelessElementsBoundFromRawData(elementsRawData);
|
||||||
|
|
||||||
@@ -1270,7 +869,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
|
|
||||||
// create blocks and canvas elements
|
// create blocks and canvas elements
|
||||||
|
|
||||||
const context: CreationContext = {
|
const context: ClipboardConfigCreationContext = {
|
||||||
oldToNewIdMap: new Map<string, string>(),
|
oldToNewIdMap: new Map<string, string>(),
|
||||||
originalIndexes: new Map<string, string>(),
|
originalIndexes: new Map<string, string>(),
|
||||||
newPresentationIndexes: createNewPresentationIndexes(
|
newPresentationIndexes: createNewPresentationIndexes(
|
||||||
@@ -1288,8 +887,8 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
if (blockSnapshot) {
|
if (blockSnapshot) {
|
||||||
const oldId = blockSnapshot.id;
|
const oldId = blockSnapshot.id;
|
||||||
|
|
||||||
const config = this._blockConfigs.find(
|
const config = this.std.getOptional(
|
||||||
({ flavour }) => flavour === blockSnapshot.flavour
|
EdgelessClipboardConfigIdentifier(blockSnapshot.flavour)
|
||||||
);
|
);
|
||||||
if (!config) continue;
|
if (!config) continue;
|
||||||
|
|
||||||
@@ -1311,7 +910,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
);
|
);
|
||||||
blockSnapshot.props.lockedBySelf = false;
|
blockSnapshot.props.lockedBySelf = false;
|
||||||
|
|
||||||
const newId = await config.createFunction(blockSnapshot, context);
|
const newId = await config.createBlock(blockSnapshot, context);
|
||||||
if (!newId) continue;
|
if (!newId) continue;
|
||||||
|
|
||||||
const block = this.doc.getBlock(newId);
|
const block = this.doc.getBlock(newId);
|
||||||
@@ -1369,13 +968,6 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
this._initEdgelessClipboard();
|
this._initEdgelessClipboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
registerBlock(flavour: string, createFunction: BlockCreationFunction) {
|
|
||||||
this._blockConfigs.push({
|
|
||||||
flavour,
|
|
||||||
createFunction: createFunction.bind(this),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async toCanvas(
|
async toCanvas(
|
||||||
blocks: GfxBlockElementModel[],
|
blocks: GfxBlockElementModel[],
|
||||||
shapes: ShapeElementModel[],
|
shapes: ShapeElementModel[],
|
||||||
@@ -1466,12 +1058,3 @@ function tryGetSvgFromClipboard(clipboardData: DataTransfer) {
|
|||||||
const file = new File([blob], 'pasted-image.svg', { type: 'image/svg+xml' });
|
const file = new File([blob], 'pasted-image.svg', { type: 'image/svg+xml' });
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRootByEditorHost(
|
|
||||||
editorHost: EditorHost
|
|
||||||
): RootBlockComponent | null {
|
|
||||||
const model = editorHost.doc.root;
|
|
||||||
if (!model) return null;
|
|
||||||
const root = editorHost.view.getBlock(model.id);
|
|
||||||
return root as RootBlockComponent | null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,435 @@
|
|||||||
|
import {
|
||||||
|
type ClipboardConfigCreationContext,
|
||||||
|
EdgelessClipboardConfig,
|
||||||
|
} from '@blocksuite/affine-block-surface';
|
||||||
|
import { ReferenceInfoSchema } from '@blocksuite/affine-model';
|
||||||
|
import { type BlockSnapshot, fromJSON } from '@blocksuite/store';
|
||||||
|
|
||||||
|
export class EdgelessClipboardNoteConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:note';
|
||||||
|
|
||||||
|
override async createBlock(note: BlockSnapshot): Promise<null | string> {
|
||||||
|
const oldId = note.id;
|
||||||
|
|
||||||
|
delete note.props.index;
|
||||||
|
if (!note.props.xywh) {
|
||||||
|
console.error(`Note block(id: ${oldId}) does not have xywh property`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newId = await this.onBlockSnapshotPaste(
|
||||||
|
note,
|
||||||
|
this.std.store,
|
||||||
|
this.std.store.root!.id
|
||||||
|
);
|
||||||
|
if (!newId) {
|
||||||
|
console.error(`Failed to paste note block(id: ${oldId})`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardEdgelessTextConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:edgeless-text';
|
||||||
|
|
||||||
|
override async createBlock(
|
||||||
|
edgelessText: BlockSnapshot
|
||||||
|
): Promise<string | null> {
|
||||||
|
const oldId = edgelessText.id;
|
||||||
|
delete edgelessText.props.index;
|
||||||
|
if (!edgelessText.props.xywh) {
|
||||||
|
console.error(
|
||||||
|
`EdgelessText block(id: ${oldId}) does not have xywh property`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!this.surface) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const newId = await this.onBlockSnapshotPaste(
|
||||||
|
edgelessText,
|
||||||
|
this.std.store,
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
if (!newId) {
|
||||||
|
console.error(`Failed to paste EdgelessText block(id: ${oldId})`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardImageConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:image';
|
||||||
|
|
||||||
|
override async createBlock(image: BlockSnapshot) {
|
||||||
|
const { xywh, rotate, sourceId, size, width, height, caption } =
|
||||||
|
image.props;
|
||||||
|
|
||||||
|
if (!this.surface) return null;
|
||||||
|
|
||||||
|
if (!(await this.std.workspace.blobSync.get(sourceId as string))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.crud.addBlock(
|
||||||
|
'affine:image',
|
||||||
|
{
|
||||||
|
caption,
|
||||||
|
sourceId,
|
||||||
|
xywh,
|
||||||
|
rotate,
|
||||||
|
size,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardFrameConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:frame';
|
||||||
|
|
||||||
|
override createBlock(
|
||||||
|
frame: BlockSnapshot,
|
||||||
|
context: ClipboardConfigCreationContext
|
||||||
|
): string | null {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
|
||||||
|
const { oldToNewIdMap, newPresentationIndexes } = context;
|
||||||
|
const { xywh, title, background, childElementIds } = frame.props;
|
||||||
|
|
||||||
|
const newChildElementIds: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
if (typeof childElementIds === 'object' && childElementIds !== null) {
|
||||||
|
Object.keys(childElementIds).forEach(oldId => {
|
||||||
|
const newId = oldToNewIdMap.get(oldId);
|
||||||
|
if (newId) {
|
||||||
|
newChildElementIds[newId] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameId = this.crud.addBlock(
|
||||||
|
'affine:frame',
|
||||||
|
{
|
||||||
|
xywh,
|
||||||
|
background,
|
||||||
|
title: fromJSON(title),
|
||||||
|
childElementIds: newChildElementIds,
|
||||||
|
presentationIndex: newPresentationIndexes.get(frame.id),
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
return frameId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardAttachmentConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:attachment';
|
||||||
|
|
||||||
|
override async createBlock(
|
||||||
|
attachment: BlockSnapshot
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
|
||||||
|
const { xywh, rotate, sourceId, name, size, type, embed, style } =
|
||||||
|
attachment.props;
|
||||||
|
|
||||||
|
if (!(await this.std.workspace.blobSync.get(sourceId as string))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const attachmentId = this.crud.addBlock(
|
||||||
|
'affine:attachment',
|
||||||
|
{
|
||||||
|
xywh,
|
||||||
|
rotate,
|
||||||
|
sourceId,
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
type,
|
||||||
|
embed,
|
||||||
|
style,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
return attachmentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardBookmarkConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:bookmark';
|
||||||
|
|
||||||
|
override createBlock(bookmark: BlockSnapshot): string | null {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
|
||||||
|
const { xywh, style, url, caption, description, icon, image, title } =
|
||||||
|
bookmark.props;
|
||||||
|
|
||||||
|
const bookmarkId = this.crud.addBlock(
|
||||||
|
'affine:bookmark',
|
||||||
|
{
|
||||||
|
xywh,
|
||||||
|
style,
|
||||||
|
url,
|
||||||
|
caption,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
image,
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
return bookmarkId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardEmbedFigmaConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:embed-figma';
|
||||||
|
|
||||||
|
override createBlock(figmaEmbed: BlockSnapshot): string | null {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
const { xywh, style, url, caption, title, description } = figmaEmbed.props;
|
||||||
|
|
||||||
|
const embedFigmaId = this.crud.addBlock(
|
||||||
|
'affine:embed-figma',
|
||||||
|
{
|
||||||
|
xywh,
|
||||||
|
style,
|
||||||
|
url,
|
||||||
|
caption,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
return embedFigmaId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardEmbedGithubConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:embed-github';
|
||||||
|
|
||||||
|
override createBlock(githubEmbed: BlockSnapshot): string | null {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
xywh,
|
||||||
|
style,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
githubType,
|
||||||
|
githubId,
|
||||||
|
url,
|
||||||
|
caption,
|
||||||
|
image,
|
||||||
|
status,
|
||||||
|
statusReason,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
createdAt,
|
||||||
|
assignees,
|
||||||
|
} = githubEmbed.props;
|
||||||
|
|
||||||
|
const embedGithubId = this.crud.addBlock(
|
||||||
|
'affine:embed-github',
|
||||||
|
{
|
||||||
|
xywh,
|
||||||
|
style,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
githubType,
|
||||||
|
githubId,
|
||||||
|
url,
|
||||||
|
caption,
|
||||||
|
image,
|
||||||
|
status,
|
||||||
|
statusReason,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
createdAt,
|
||||||
|
assignees,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
return embedGithubId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardEmbedHtmlConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:embed-html';
|
||||||
|
|
||||||
|
override createBlock(htmlEmbed: BlockSnapshot): string | null {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
const { xywh, style, caption, html, design } = htmlEmbed.props;
|
||||||
|
|
||||||
|
const embedHtmlId = this.crud.addBlock(
|
||||||
|
'affine:embed-html',
|
||||||
|
{
|
||||||
|
xywh,
|
||||||
|
style,
|
||||||
|
caption,
|
||||||
|
html,
|
||||||
|
design,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
return embedHtmlId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardEmbedLoomConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:embed-loom';
|
||||||
|
|
||||||
|
override createBlock(loomEmbed: BlockSnapshot): string | null {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
const { xywh, style, url, caption, videoId, image, title, description } =
|
||||||
|
loomEmbed.props;
|
||||||
|
|
||||||
|
const embedLoomId = this.crud.addBlock(
|
||||||
|
'affine:embed-loom',
|
||||||
|
{
|
||||||
|
xywh,
|
||||||
|
style,
|
||||||
|
url,
|
||||||
|
caption,
|
||||||
|
videoId,
|
||||||
|
image,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
return embedLoomId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardEmbedYoutubeConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:embed-youtube';
|
||||||
|
|
||||||
|
override createBlock(youtubeEmbed: BlockSnapshot): string | null {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
const {
|
||||||
|
xywh,
|
||||||
|
style,
|
||||||
|
url,
|
||||||
|
caption,
|
||||||
|
videoId,
|
||||||
|
image,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
creator,
|
||||||
|
creatorUrl,
|
||||||
|
creatorImage,
|
||||||
|
} = youtubeEmbed.props;
|
||||||
|
|
||||||
|
const embedYoutubeId = this.crud.addBlock(
|
||||||
|
'affine:embed-youtube',
|
||||||
|
{
|
||||||
|
xywh,
|
||||||
|
style,
|
||||||
|
url,
|
||||||
|
caption,
|
||||||
|
videoId,
|
||||||
|
image,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
creator,
|
||||||
|
creatorUrl,
|
||||||
|
creatorImage,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
return embedYoutubeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardEmbedIframeConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:embed-iframe';
|
||||||
|
|
||||||
|
override createBlock(embedIframe: BlockSnapshot): string | null {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
const {
|
||||||
|
xywh,
|
||||||
|
caption,
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
iframeUrl,
|
||||||
|
scale,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} = embedIframe.props;
|
||||||
|
|
||||||
|
return this.crud.addBlock(
|
||||||
|
'affine:embed-iframe',
|
||||||
|
{
|
||||||
|
url,
|
||||||
|
iframeUrl,
|
||||||
|
xywh,
|
||||||
|
caption,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
scale,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardEmbedLinkedDocConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:embed-linked-doc';
|
||||||
|
|
||||||
|
override createBlock(linkedDocEmbed: BlockSnapshot): string | null {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
|
||||||
|
const { xywh, style, caption, pageId, params, title, description } =
|
||||||
|
linkedDocEmbed.props;
|
||||||
|
const referenceInfo = ReferenceInfoSchema.parse({
|
||||||
|
pageId,
|
||||||
|
params,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.crud.addBlock(
|
||||||
|
'affine:embed-linked-doc',
|
||||||
|
{
|
||||||
|
xywh,
|
||||||
|
style,
|
||||||
|
caption,
|
||||||
|
...referenceInfo,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EdgelessClipboardEmbedSyncedDocConfig extends EdgelessClipboardConfig {
|
||||||
|
static override readonly key = 'affine:embed-synced-doc';
|
||||||
|
|
||||||
|
override createBlock(syncedDocEmbed: BlockSnapshot): string | null {
|
||||||
|
if (!this.surface) return null;
|
||||||
|
|
||||||
|
const { xywh, style, caption, scale, pageId, params } =
|
||||||
|
syncedDocEmbed.props;
|
||||||
|
const referenceInfo = ReferenceInfoSchema.parse({ pageId, params });
|
||||||
|
|
||||||
|
return this.crud.addBlock(
|
||||||
|
'affine:embed-synced-doc',
|
||||||
|
{
|
||||||
|
xywh,
|
||||||
|
style,
|
||||||
|
caption,
|
||||||
|
scale,
|
||||||
|
...referenceInfo,
|
||||||
|
},
|
||||||
|
this.surface.model.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,22 @@ import { literal, unsafeStatic } from 'lit/static-html.js';
|
|||||||
import { CommonSpecs } from '../common-specs/index.js';
|
import { CommonSpecs } from '../common-specs/index.js';
|
||||||
import { edgelessNavigatorBgWidget } from '../widgets/edgeless-navigator-bg/index.js';
|
import { edgelessNavigatorBgWidget } from '../widgets/edgeless-navigator-bg/index.js';
|
||||||
import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from '../widgets/edgeless-zoom-toolbar/index.js';
|
import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from '../widgets/edgeless-zoom-toolbar/index.js';
|
||||||
|
import {
|
||||||
|
EdgelessClipboardAttachmentConfig,
|
||||||
|
EdgelessClipboardBookmarkConfig,
|
||||||
|
EdgelessClipboardEdgelessTextConfig,
|
||||||
|
EdgelessClipboardEmbedFigmaConfig,
|
||||||
|
EdgelessClipboardEmbedGithubConfig,
|
||||||
|
EdgelessClipboardEmbedHtmlConfig,
|
||||||
|
EdgelessClipboardEmbedIframeConfig,
|
||||||
|
EdgelessClipboardEmbedLinkedDocConfig,
|
||||||
|
EdgelessClipboardEmbedLoomConfig,
|
||||||
|
EdgelessClipboardEmbedSyncedDocConfig,
|
||||||
|
EdgelessClipboardEmbedYoutubeConfig,
|
||||||
|
EdgelessClipboardFrameConfig,
|
||||||
|
EdgelessClipboardImageConfig,
|
||||||
|
EdgelessClipboardNoteConfig,
|
||||||
|
} from './clipboard/config.js';
|
||||||
import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js';
|
import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js';
|
||||||
import { EDGELESS_DRAGGING_AREA_WIDGET } from './components/rects/edgeless-dragging-area-rect.js';
|
import { EDGELESS_DRAGGING_AREA_WIDGET } from './components/rects/edgeless-dragging-area-rect.js';
|
||||||
import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js';
|
import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js';
|
||||||
@@ -56,6 +72,23 @@ class EdgelessLocker extends LifeCycleWatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EdgelessClipboardConfigs: ExtensionType[] = [
|
||||||
|
EdgelessClipboardNoteConfig,
|
||||||
|
EdgelessClipboardEdgelessTextConfig,
|
||||||
|
EdgelessClipboardImageConfig,
|
||||||
|
EdgelessClipboardFrameConfig,
|
||||||
|
EdgelessClipboardAttachmentConfig,
|
||||||
|
EdgelessClipboardBookmarkConfig,
|
||||||
|
EdgelessClipboardEmbedFigmaConfig,
|
||||||
|
EdgelessClipboardEmbedGithubConfig,
|
||||||
|
EdgelessClipboardEmbedHtmlConfig,
|
||||||
|
EdgelessClipboardEmbedLoomConfig,
|
||||||
|
EdgelessClipboardEmbedYoutubeConfig,
|
||||||
|
EdgelessClipboardEmbedIframeConfig,
|
||||||
|
EdgelessClipboardEmbedLinkedDocConfig,
|
||||||
|
EdgelessClipboardEmbedSyncedDocConfig,
|
||||||
|
];
|
||||||
|
|
||||||
const EdgelessCommonExtension: ExtensionType[] = [
|
const EdgelessCommonExtension: ExtensionType[] = [
|
||||||
CommonSpecs,
|
CommonSpecs,
|
||||||
ToolController,
|
ToolController,
|
||||||
@@ -65,6 +98,7 @@ const EdgelessCommonExtension: ExtensionType[] = [
|
|||||||
ConnectorElementView,
|
ConnectorElementView,
|
||||||
...quickTools,
|
...quickTools,
|
||||||
...seniorTools,
|
...seniorTools,
|
||||||
|
...EdgelessClipboardConfigs,
|
||||||
].flat();
|
].flat();
|
||||||
|
|
||||||
export const EdgelessRootBlockSpec: ExtensionType[] = [
|
export const EdgelessRootBlockSpec: ExtensionType[] = [
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std';
|
||||||
|
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||||
|
import { BlockSuiteError } from '@blocksuite/global/exceptions';
|
||||||
|
import { type BlockSnapshot, Extension, type Store } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import { getSurfaceComponent } from '../utils/get-surface-block';
|
||||||
|
import { EdgelessCRUDIdentifier } from './crud-extension';
|
||||||
|
|
||||||
|
export type ClipboardConfigCreationContext = {
|
||||||
|
/**
|
||||||
|
* element old id to new id
|
||||||
|
*/
|
||||||
|
oldToNewIdMap: Map<string, string>;
|
||||||
|
/**
|
||||||
|
* element old id to new layer index
|
||||||
|
*/
|
||||||
|
originalIndexes: Map<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* frame old id to new presentation index
|
||||||
|
*/
|
||||||
|
newPresentationIndexes: Map<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EdgelessClipboardConfigIdentifier =
|
||||||
|
createIdentifier<EdgelessClipboardConfig>('edgeless-clipboard-config');
|
||||||
|
|
||||||
|
export abstract class EdgelessClipboardConfig extends Extension {
|
||||||
|
static key: string;
|
||||||
|
|
||||||
|
constructor(readonly std: BlockStdScope) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
get surface() {
|
||||||
|
return getSurfaceComponent(this.std);
|
||||||
|
}
|
||||||
|
|
||||||
|
get crud() {
|
||||||
|
return this.std.get(EdgelessCRUDIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlockSnapshotPaste = async (
|
||||||
|
snapshot: BlockSnapshot,
|
||||||
|
doc: Store,
|
||||||
|
parent?: string,
|
||||||
|
index?: number
|
||||||
|
) => {
|
||||||
|
const block = await this.std.clipboard.pasteBlockSnapshot(
|
||||||
|
snapshot,
|
||||||
|
doc,
|
||||||
|
parent,
|
||||||
|
index
|
||||||
|
);
|
||||||
|
return block?.id ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
abstract createBlock(
|
||||||
|
snapshot: BlockSnapshot,
|
||||||
|
context: ClipboardConfigCreationContext
|
||||||
|
): string | null | Promise<string | null>;
|
||||||
|
|
||||||
|
static override setup(di: Container) {
|
||||||
|
if (!this.key) {
|
||||||
|
throw new BlockSuiteError(
|
||||||
|
BlockSuiteError.ErrorCode.ValueNotExists,
|
||||||
|
'Key is not defined in the EdgelessClipboardConfig'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
di.add(
|
||||||
|
this as unknown as { new (std: BlockStdScope): EdgelessClipboardConfig },
|
||||||
|
[StdIdentifier]
|
||||||
|
);
|
||||||
|
|
||||||
|
di.addImpl(EdgelessClipboardConfigIdentifier(this.key), provider =>
|
||||||
|
provider.get(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './clipboard-config';
|
||||||
export * from './crud-extension';
|
export * from './crud-extension';
|
||||||
export * from './export-manager';
|
export * from './export-manager';
|
||||||
export * from './legacy-slot-extension';
|
export * from './legacy-slot-extension';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import rehypeParse from 'rehype-parse';
|
|||||||
import { unified } from 'unified';
|
import { unified } from 'unified';
|
||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
import { onlyContainImgElement } from '../clipboard/index.js';
|
import { onlyContainImgElement } from '../clipboard/utils.js';
|
||||||
|
|
||||||
describe('only contains img elements', () => {
|
describe('only contains img elements', () => {
|
||||||
test('normal with head', () => {
|
test('normal with head', () => {
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import type {
|
|||||||
TransformerMiddleware,
|
TransformerMiddleware,
|
||||||
} from '@blocksuite/store';
|
} from '@blocksuite/store';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import type { RootContentMap } from 'hast';
|
|
||||||
import * as lz from 'lz-string';
|
import * as lz from 'lz-string';
|
||||||
import rehypeParse from 'rehype-parse';
|
import rehypeParse from 'rehype-parse';
|
||||||
import { unified } from 'unified';
|
import { unified } from 'unified';
|
||||||
|
|
||||||
import { LifeCycleWatcher } from '../extension/index.js';
|
import { LifeCycleWatcher } from '../extension/index.js';
|
||||||
|
import { onlyContainImgElement } from './utils.js';
|
||||||
|
|
||||||
type AdapterConstructor<T extends BaseAdapter> =
|
type AdapterConstructor<T extends BaseAdapter> =
|
||||||
| { new (job: Transformer): T }
|
| { new (job: Transformer): T }
|
||||||
@@ -28,38 +28,6 @@ type AdapterMap = Map<
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type HastUnionType<
|
|
||||||
K extends keyof RootContentMap,
|
|
||||||
V extends RootContentMap[K],
|
|
||||||
> = V;
|
|
||||||
|
|
||||||
export function onlyContainImgElement(
|
|
||||||
ast: HastUnionType<keyof RootContentMap, RootContentMap[keyof RootContentMap]>
|
|
||||||
): 'yes' | 'no' | 'maybe' {
|
|
||||||
if (ast.type === 'element') {
|
|
||||||
switch (ast.tagName) {
|
|
||||||
case 'html':
|
|
||||||
case 'body':
|
|
||||||
return ast.children.map(onlyContainImgElement).reduce((a, b) => {
|
|
||||||
if (a === 'no' || b === 'no') {
|
|
||||||
return 'no';
|
|
||||||
}
|
|
||||||
if (a === 'maybe' && b === 'maybe') {
|
|
||||||
return 'maybe';
|
|
||||||
}
|
|
||||||
return 'yes';
|
|
||||||
}, 'maybe');
|
|
||||||
case 'img':
|
|
||||||
return 'yes';
|
|
||||||
case 'head':
|
|
||||||
return 'maybe';
|
|
||||||
default:
|
|
||||||
return 'no';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'maybe';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Clipboard extends LifeCycleWatcher {
|
export class Clipboard extends LifeCycleWatcher {
|
||||||
static override readonly key = 'clipboard';
|
static override readonly key = 'clipboard';
|
||||||
|
|
||||||
|
|||||||
33
blocksuite/framework/block-std/src/clipboard/utils.ts
Normal file
33
blocksuite/framework/block-std/src/clipboard/utils.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { RootContentMap } from 'hast';
|
||||||
|
|
||||||
|
type HastUnionType<
|
||||||
|
K extends keyof RootContentMap,
|
||||||
|
V extends RootContentMap[K],
|
||||||
|
> = V;
|
||||||
|
|
||||||
|
export function onlyContainImgElement(
|
||||||
|
ast: HastUnionType<keyof RootContentMap, RootContentMap[keyof RootContentMap]>
|
||||||
|
): 'yes' | 'no' | 'maybe' {
|
||||||
|
if (ast.type === 'element') {
|
||||||
|
switch (ast.tagName) {
|
||||||
|
case 'html':
|
||||||
|
case 'body':
|
||||||
|
return ast.children.map(onlyContainImgElement).reduce((a, b) => {
|
||||||
|
if (a === 'no' || b === 'no') {
|
||||||
|
return 'no';
|
||||||
|
}
|
||||||
|
if (a === 'maybe' && b === 'maybe') {
|
||||||
|
return 'maybe';
|
||||||
|
}
|
||||||
|
return 'yes';
|
||||||
|
}, 'maybe');
|
||||||
|
case 'img':
|
||||||
|
return 'yes';
|
||||||
|
case 'head':
|
||||||
|
return 'maybe';
|
||||||
|
default:
|
||||||
|
return 'no';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'maybe';
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ import {
|
|||||||
import { patchDatabaseBlockConfigService } from '../extensions/database-block-config-service';
|
import { patchDatabaseBlockConfigService } from '../extensions/database-block-config-service';
|
||||||
import { patchDocModeService } from '../extensions/doc-mode-service';
|
import { patchDocModeService } from '../extensions/doc-mode-service';
|
||||||
import { patchDocUrlExtensions } from '../extensions/doc-url';
|
import { patchDocUrlExtensions } from '../extensions/doc-url';
|
||||||
import { EdgelessClipboardWatcher } from '../extensions/edgeless-clipboard';
|
import { EdgelessClipboardAIChatConfig } from '../extensions/edgeless-clipboard';
|
||||||
import { patchForClipboardInElectron } from '../extensions/electron-clipboard';
|
import { patchForClipboardInElectron } from '../extensions/electron-clipboard';
|
||||||
import { enableEditorExtension } from '../extensions/entry/enable-editor';
|
import { enableEditorExtension } from '../extensions/entry/enable-editor';
|
||||||
import { enableMobileExtension } from '../extensions/entry/enable-mobile';
|
import { enableMobileExtension } from '../extensions/entry/enable-mobile';
|
||||||
@@ -169,7 +169,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
patchNotificationService(confirmModal),
|
patchNotificationService(confirmModal),
|
||||||
patchPeekViewService(peekViewService),
|
patchPeekViewService(peekViewService),
|
||||||
patchOpenDocExtension(),
|
patchOpenDocExtension(),
|
||||||
EdgelessClipboardWatcher,
|
EdgelessClipboardAIChatConfig,
|
||||||
patchDocUrlExtensions(framework),
|
patchDocUrlExtensions(framework),
|
||||||
patchQuickSearchService(framework),
|
patchQuickSearchService(framework),
|
||||||
patchSideBarService(framework),
|
patchSideBarService(framework),
|
||||||
|
|||||||
@@ -1,44 +1,26 @@
|
|||||||
import { AIChatBlockSchema } from '@affine/core/blocksuite/ai/blocks';
|
import { AIChatBlockSchema } from '@affine/core/blocksuite/ai/blocks';
|
||||||
import { LifeCycleWatcher } from '@blocksuite/affine/block-std';
|
import { EdgelessClipboardConfig } from '@blocksuite/affine/blocks/surface';
|
||||||
import { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks/root';
|
|
||||||
import type { BlockSnapshot } from '@blocksuite/affine/store';
|
import type { BlockSnapshot } from '@blocksuite/affine/store';
|
||||||
|
|
||||||
export class EdgelessClipboardWatcher extends LifeCycleWatcher {
|
export class EdgelessClipboardAIChatConfig extends EdgelessClipboardConfig {
|
||||||
static override key = 'edgeless-clipboard-watcher';
|
static override readonly key = AIChatBlockSchema.model.flavour;
|
||||||
|
|
||||||
override mounted() {
|
override createBlock(block: BlockSnapshot): null | string {
|
||||||
super.mounted();
|
if (!this.surface) return null;
|
||||||
const { view } = this.std;
|
const { xywh, scale, messages, sessionId, rootDocId, rootWorkspaceId } =
|
||||||
view.viewUpdated.subscribe(payload => {
|
block.props;
|
||||||
if (payload.type !== 'block' || payload.method !== 'add') {
|
const blockId = this.crud.addBlock(
|
||||||
return;
|
AIChatBlockSchema.model.flavour,
|
||||||
}
|
{
|
||||||
const component = payload.view;
|
xywh,
|
||||||
if (!(component instanceof EdgelessRootBlockComponent)) {
|
scale,
|
||||||
return;
|
messages,
|
||||||
}
|
sessionId,
|
||||||
const AIChatBlockFlavour = AIChatBlockSchema.model.flavour;
|
rootDocId,
|
||||||
const createFunc = (block: BlockSnapshot) => {
|
rootWorkspaceId,
|
||||||
const { xywh, scale, messages, sessionId, rootDocId, rootWorkspaceId } =
|
},
|
||||||
block.props;
|
this.surface.model.id
|
||||||
const blockId = component.service.crud.addBlock(
|
);
|
||||||
AIChatBlockFlavour,
|
return blockId;
|
||||||
{
|
|
||||||
xywh,
|
|
||||||
scale,
|
|
||||||
messages,
|
|
||||||
sessionId,
|
|
||||||
rootDocId,
|
|
||||||
rootWorkspaceId,
|
|
||||||
},
|
|
||||||
component.surface.model.id
|
|
||||||
);
|
|
||||||
return blockId;
|
|
||||||
};
|
|
||||||
component.clipboardController.registerBlock(
|
|
||||||
AIChatBlockFlavour,
|
|
||||||
createFunc
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user