mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 20:16:26 +08:00
feat(editor): extensionize clipboards (#11172)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
PreviewEdgelessRootBlockSpec,
|
||||
PreviewPageRootBlockSpec,
|
||||
ReadOnlyClipboard,
|
||||
} from '@blocksuite/affine-block-root';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
@@ -12,9 +13,11 @@ import {
|
||||
export const PreviewEdgelessEditorBlockSpecs: ExtensionType[] = [
|
||||
PreviewEdgelessRootBlockSpec,
|
||||
EdgelessFirstPartyBlockSpecs,
|
||||
ReadOnlyClipboard,
|
||||
].flat();
|
||||
|
||||
export const PreviewPageEditorBlockSpecs: ExtensionType[] = [
|
||||
PreviewPageRootBlockSpec,
|
||||
PageFirstPartyBlockSpecs,
|
||||
ReadOnlyClipboard,
|
||||
].flat();
|
||||
|
||||
@@ -25,23 +25,23 @@ import { ReadOnlyClipboard } from './readonly-clipboard';
|
||||
* It is supported to copy and paste models in the page root block.
|
||||
*/
|
||||
export class PageClipboard extends ReadOnlyClipboard {
|
||||
static override key = 'affine-page-clipboard';
|
||||
|
||||
protected _init = () => {
|
||||
this._initAdapters();
|
||||
const paste = pasteMiddleware(this._std);
|
||||
const paste = pasteMiddleware(this.std);
|
||||
// Use surfaceRefToEmbed middleware to convert surface-ref to embed-linked-doc
|
||||
// When pastina a surface-ref block to another doc
|
||||
const surfaceRefToEmbedMiddleware = surfaceRefToEmbed(this._std);
|
||||
const replaceId = replaceIdMiddleware(
|
||||
this._std.store.workspace.idGenerator
|
||||
);
|
||||
this._std.clipboard.use(paste);
|
||||
this._std.clipboard.use(surfaceRefToEmbedMiddleware);
|
||||
this._std.clipboard.use(replaceId);
|
||||
const surfaceRefToEmbedMiddleware = surfaceRefToEmbed(this.std);
|
||||
const replaceId = replaceIdMiddleware(this.std.store.workspace.idGenerator);
|
||||
this.std.clipboard.use(paste);
|
||||
this.std.clipboard.use(surfaceRefToEmbedMiddleware);
|
||||
this.std.clipboard.use(replaceId);
|
||||
this._disposables.add({
|
||||
dispose: () => {
|
||||
this._std.clipboard.unuse(paste);
|
||||
this._std.clipboard.unuse(surfaceRefToEmbedMiddleware);
|
||||
this._std.clipboard.unuse(replaceId);
|
||||
this.std.clipboard.unuse(paste);
|
||||
this.std.clipboard.unuse(surfaceRefToEmbedMiddleware);
|
||||
this.std.clipboard.unuse(replaceId);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -52,7 +52,7 @@ export class PageClipboard extends ReadOnlyClipboard {
|
||||
parent?: string,
|
||||
index?: number
|
||||
) => {
|
||||
const block = await this._std.clipboard.pasteBlockSnapshot(
|
||||
const block = await this.std.clipboard.pasteBlockSnapshot(
|
||||
snapshot,
|
||||
doc,
|
||||
parent,
|
||||
@@ -66,7 +66,7 @@ export class PageClipboard extends ReadOnlyClipboard {
|
||||
e.preventDefault();
|
||||
|
||||
this._copySelected(() => {
|
||||
this._std.command
|
||||
this.std.command
|
||||
.chain()
|
||||
.try<{}>(cmd => [
|
||||
cmd.pipe(getTextSelectionCommand).pipe(deleteTextCommand),
|
||||
@@ -80,8 +80,8 @@ export class PageClipboard extends ReadOnlyClipboard {
|
||||
const e = ctx.get('clipboardState').raw;
|
||||
e.preventDefault();
|
||||
|
||||
this._std.store.captureSync();
|
||||
this._std.command
|
||||
this.std.store.captureSync();
|
||||
this.std.command
|
||||
.chain()
|
||||
.try<{}>(cmd => [
|
||||
cmd.pipe(getTextSelectionCommand).pipe((ctx, next) => {
|
||||
@@ -91,7 +91,7 @@ export class PageClipboard extends ReadOnlyClipboard {
|
||||
}
|
||||
const { from, to } = currentTextSelection;
|
||||
if (to && from.blockId !== to.blockId) {
|
||||
this._std.command.exec(deleteTextCommand, {
|
||||
this.std.command.exec(deleteTextCommand, {
|
||||
currentTextSelection,
|
||||
});
|
||||
}
|
||||
@@ -139,10 +139,10 @@ export class PageClipboard extends ReadOnlyClipboard {
|
||||
if (!ctx.parentBlock) {
|
||||
return;
|
||||
}
|
||||
this._std.clipboard
|
||||
this.std.clipboard
|
||||
.paste(
|
||||
e,
|
||||
this._std.store,
|
||||
this.std.store,
|
||||
ctx.parentBlock.model.id,
|
||||
ctx.blockIndex ? ctx.blockIndex + 1 : 1
|
||||
)
|
||||
@@ -153,15 +153,19 @@ export class PageClipboard extends ReadOnlyClipboard {
|
||||
.run();
|
||||
};
|
||||
|
||||
override hostConnected() {
|
||||
override mounted() {
|
||||
if (!navigator.clipboard) {
|
||||
console.error(
|
||||
'navigator.clipboard is not supported in current environment.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this._disposables.disposed) {
|
||||
this._disposables = new DisposableGroup();
|
||||
}
|
||||
if (navigator.clipboard) {
|
||||
this.host.handleEvent('copy', this.onPageCopy);
|
||||
this.host.handleEvent('paste', this.onPagePaste);
|
||||
this.host.handleEvent('cut', this.onPageCut);
|
||||
this._init();
|
||||
}
|
||||
this.std.event.add('copy', this.onPageCopy);
|
||||
this.std.event.add('paste', this.onPagePaste);
|
||||
this.std.event.add('cut', this.onPageCut);
|
||||
this._init();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
getSelectedModelsCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import {
|
||||
type BlockComponent,
|
||||
ClipboardAdapterConfigExtension,
|
||||
LifeCycleWatcher,
|
||||
type UIEventHandler,
|
||||
} from '@blocksuite/block-std';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
@@ -81,9 +81,11 @@ export const clipboardConfigs: ExtensionType[] = [
|
||||
* ReadOnlyClipboard is a class that provides a read-only clipboard for the root block.
|
||||
* It is supported to copy models in the root block.
|
||||
*/
|
||||
export class ReadOnlyClipboard {
|
||||
export class ReadOnlyClipboard extends LifeCycleWatcher {
|
||||
static override key = 'affine-readonly-clipboard';
|
||||
|
||||
protected readonly _copySelected = (onCopy?: () => void) => {
|
||||
return this._std.command
|
||||
return this.std.command
|
||||
.chain()
|
||||
.with({ onCopy })
|
||||
.pipe(getSelectedModelsCommand)
|
||||
@@ -94,26 +96,24 @@ export class ReadOnlyClipboard {
|
||||
protected _disposables = new DisposableGroup();
|
||||
|
||||
protected _initAdapters = () => {
|
||||
const copy = copyMiddleware(this._std);
|
||||
this._std.clipboard.use(copy);
|
||||
this._std.clipboard.use(
|
||||
titleMiddleware(this._std.store.workspace.meta.docMetas)
|
||||
const copy = copyMiddleware(this.std);
|
||||
this.std.clipboard.use(copy);
|
||||
this.std.clipboard.use(
|
||||
titleMiddleware(this.std.store.workspace.meta.docMetas)
|
||||
);
|
||||
this._std.clipboard.use(defaultImageProxyMiddleware);
|
||||
this.std.clipboard.use(defaultImageProxyMiddleware);
|
||||
|
||||
this._disposables.add({
|
||||
dispose: () => {
|
||||
this._std.clipboard.unuse(copy);
|
||||
this._std.clipboard.unuse(
|
||||
titleMiddleware(this._std.store.workspace.meta.docMetas)
|
||||
this.std.clipboard.unuse(copy);
|
||||
this.std.clipboard.unuse(
|
||||
titleMiddleware(this.std.store.workspace.meta.docMetas)
|
||||
);
|
||||
this._std.clipboard.unuse(defaultImageProxyMiddleware);
|
||||
this.std.clipboard.unuse(defaultImageProxyMiddleware);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
host: BlockComponent;
|
||||
|
||||
onPageCopy: UIEventHandler = ctx => {
|
||||
const e = ctx.get('clipboardState').raw;
|
||||
e.preventDefault();
|
||||
@@ -121,25 +121,17 @@ export class ReadOnlyClipboard {
|
||||
this._copySelected().run();
|
||||
};
|
||||
|
||||
protected get _std() {
|
||||
return this.host.std;
|
||||
}
|
||||
|
||||
constructor(host: BlockComponent) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
override mounted(): void {
|
||||
if (!navigator.clipboard) {
|
||||
console.error(
|
||||
'navigator.clipboard is not supported in current environment.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this._disposables.disposed) {
|
||||
this._disposables = new DisposableGroup();
|
||||
}
|
||||
if (navigator.clipboard) {
|
||||
this.host.handleEvent('copy', this.onPageCopy);
|
||||
this._initAdapters();
|
||||
}
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this._disposables.dispose();
|
||||
this.std.event.add('copy', this.onPageCopy);
|
||||
this._initAdapters();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import {
|
||||
ClipboardAdapter,
|
||||
decodeClipboardBlobs,
|
||||
encodeClipboardBlobs,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
CANVAS_EXPORT_IGNORE_TAGS,
|
||||
@@ -42,8 +41,6 @@ import {
|
||||
referenceToNode,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type {
|
||||
BlockComponent,
|
||||
BlockStdScope,
|
||||
EditorHost,
|
||||
SurfaceSelection,
|
||||
UIEventStateContext,
|
||||
@@ -74,21 +71,18 @@ import {
|
||||
BlockSnapshotSchema,
|
||||
type SliceSnapshot,
|
||||
} from '@blocksuite/store';
|
||||
import DOMPurify from 'dompurify';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { PageClipboard } from '../../clipboard/index.js';
|
||||
import { edgelessElementsBoundFromRawData } from '../utils/bound-utils.js';
|
||||
import { createNewPresentationIndexes } from '../utils/clipboard-utils.js';
|
||||
import { getSortedCloneElements } from '../utils/clone-utils.js';
|
||||
import { isCanvasElementWithText } from '../utils/query.js';
|
||||
import {
|
||||
getSortedCloneElements,
|
||||
serializeElement,
|
||||
} from '../utils/clone-utils.js';
|
||||
import {
|
||||
isAttachmentBlock,
|
||||
isCanvasElementWithText,
|
||||
isImageBlock,
|
||||
} from '../utils/query.js';
|
||||
createNewPresentationIndexes,
|
||||
edgelessElementsBoundFromRawData,
|
||||
isPureFileInClipboard,
|
||||
prepareClipboardData,
|
||||
tryGetSvgFromClipboard,
|
||||
} from './utils.js';
|
||||
|
||||
const BLOCKSUITE_SURFACE = 'blocksuite/surface';
|
||||
|
||||
@@ -102,35 +96,25 @@ interface CanvasExportOptions {
|
||||
}
|
||||
|
||||
export class EdgelessClipboardController extends PageClipboard {
|
||||
static override key = 'affine-edgeless-clipboard';
|
||||
|
||||
private readonly _initEdgelessClipboard = () => {
|
||||
this.host.handleEvent(
|
||||
'copy',
|
||||
ctx => {
|
||||
const { surfaceSelections, selectedIds } = this.selectionManager;
|
||||
this.std.event.add('copy', ctx => {
|
||||
const { surfaceSelections, selectedIds } = this.selectionManager;
|
||||
|
||||
if (selectedIds.length === 0) return false;
|
||||
if (selectedIds.length === 0) return false;
|
||||
|
||||
this._onCopy(ctx, surfaceSelections).catch(console.error);
|
||||
return;
|
||||
},
|
||||
{ global: true }
|
||||
);
|
||||
this._onCopy(ctx, surfaceSelections).catch(console.error);
|
||||
return;
|
||||
});
|
||||
|
||||
this.host.handleEvent(
|
||||
'paste',
|
||||
ctx => {
|
||||
this._onPaste(ctx).catch(console.error);
|
||||
},
|
||||
{ global: true }
|
||||
);
|
||||
this.std.event.add('paste', ctx => {
|
||||
this._onPaste(ctx).catch(console.error);
|
||||
});
|
||||
|
||||
this.host.handleEvent(
|
||||
'cut',
|
||||
ctx => {
|
||||
this._onCut(ctx).catch(console.error);
|
||||
},
|
||||
{ global: true }
|
||||
);
|
||||
this.std.event.add('cut', ctx => {
|
||||
this._onCut(ctx).catch(console.error);
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _onCopy = async (
|
||||
@@ -373,21 +357,13 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
}
|
||||
|
||||
private get doc() {
|
||||
return this.host.doc;
|
||||
}
|
||||
|
||||
private get edgeless() {
|
||||
return this.host;
|
||||
return this.std.store;
|
||||
}
|
||||
|
||||
private get selectionManager() {
|
||||
return this.gfx.selection;
|
||||
}
|
||||
|
||||
private get std() {
|
||||
return this.host.std;
|
||||
}
|
||||
|
||||
private get surface() {
|
||||
return getSurfaceComponent(this.std);
|
||||
}
|
||||
@@ -523,7 +499,6 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
}
|
||||
|
||||
private async _edgelessToCanvas(
|
||||
edgeless: BlockComponent,
|
||||
bound: IBound,
|
||||
nodes?: GfxBlockElementModel[],
|
||||
canvasElements: GfxPrimitiveElementModel[] = [],
|
||||
@@ -533,7 +508,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
dpr = window.devicePixelRatio || 1,
|
||||
}: CanvasExportOptions = {}
|
||||
): Promise<HTMLCanvasElement | undefined> {
|
||||
const host = edgeless.host;
|
||||
const host = this.std.host;
|
||||
const rootModel = this.doc.root;
|
||||
if (!rootModel) return;
|
||||
|
||||
@@ -874,7 +849,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
originalIndexes: new Map<string, string>(),
|
||||
newPresentationIndexes: createNewPresentationIndexes(
|
||||
elementsRawData,
|
||||
this.edgeless
|
||||
this.std
|
||||
),
|
||||
};
|
||||
|
||||
@@ -960,7 +935,13 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
};
|
||||
}
|
||||
|
||||
override hostConnected() {
|
||||
override mounted() {
|
||||
if (!navigator.clipboard) {
|
||||
console.error(
|
||||
'navigator.clipboard is not supported in current environment.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this._disposables.disposed) {
|
||||
this._disposables = new DisposableGroup();
|
||||
}
|
||||
@@ -989,72 +970,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = await this._edgelessToCanvas(
|
||||
this.host,
|
||||
bound,
|
||||
blocks,
|
||||
shapes,
|
||||
options
|
||||
);
|
||||
const canvas = await this._edgelessToCanvas(bound, blocks, shapes, options);
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
|
||||
export async function prepareClipboardData(
|
||||
selectedAll: GfxModel[],
|
||||
std: BlockStdScope
|
||||
) {
|
||||
const job = std.store.getTransformer();
|
||||
const selected = await Promise.all(
|
||||
selectedAll.map(async selected => {
|
||||
const data = serializeElement(selected, selectedAll, job);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (isAttachmentBlock(selected) || isImageBlock(selected)) {
|
||||
await job.assetsManager.readFromBlob(data.props.sourceId as string);
|
||||
}
|
||||
return data;
|
||||
})
|
||||
);
|
||||
const blobs = await encodeClipboardBlobs(job.assetsManager.getAssets());
|
||||
return {
|
||||
snapshot: selected.filter(d => !!d),
|
||||
blobs,
|
||||
};
|
||||
}
|
||||
|
||||
function isPureFileInClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
return (
|
||||
(types.length === 1 && types[0] === 'Files') ||
|
||||
(types.length === 2 &&
|
||||
(types.includes('text/plain') || types.includes('text/html')) &&
|
||||
types.includes('Files'))
|
||||
);
|
||||
}
|
||||
|
||||
function tryGetSvgFromClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
|
||||
if (types.length === 1 && types[0] !== 'text/plain') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(
|
||||
clipboardData.getData('text/plain'),
|
||||
'image/svg+xml'
|
||||
);
|
||||
const svg = svgDoc.documentElement;
|
||||
|
||||
if (svg.tagName !== 'svg' || !svg.hasAttribute('xmlns')) {
|
||||
return null;
|
||||
}
|
||||
const svgContent = DOMPurify.sanitize(svgDoc.documentElement, {
|
||||
USE_PROFILES: { svg: true },
|
||||
});
|
||||
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
const file = new File([blob], 'pasted-image.svg', { type: 'image/svg+xml' });
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
EdgelessFrameManager,
|
||||
EdgelessFrameManagerIdentifier,
|
||||
} from '@blocksuite/affine-block-frame';
|
||||
import type { FrameBlockProps } from '@blocksuite/affine-model';
|
||||
import { encodeClipboardBlobs } from '@blocksuite/affine-shared/adapters';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import {
|
||||
generateKeyBetweenV2,
|
||||
type GfxModel,
|
||||
type SerializedElement,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import { Bound, getBoundWithRotation } from '@blocksuite/global/gfx';
|
||||
import { type BlockSnapshot, BlockSnapshotSchema } from '@blocksuite/store';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import { serializeElement } from '../utils/clone-utils';
|
||||
import { isAttachmentBlock, isImageBlock } from '../utils/query';
|
||||
|
||||
type FrameSnapshot = BlockSnapshot & {
|
||||
props: FrameBlockProps;
|
||||
};
|
||||
|
||||
export function createNewPresentationIndexes(
|
||||
raw: (SerializedElement | BlockSnapshot)[],
|
||||
std: BlockStdScope
|
||||
) {
|
||||
const frames = raw
|
||||
.filter((block): block is FrameSnapshot => {
|
||||
const { data } = BlockSnapshotSchema.safeParse(block);
|
||||
return data?.flavour === 'affine:frame';
|
||||
})
|
||||
.sort((a, b) => EdgelessFrameManager.framePresentationComparator(a, b));
|
||||
|
||||
const frameMgr = std.get(EdgelessFrameManagerIdentifier);
|
||||
let before = frameMgr.generatePresentationIndex();
|
||||
const result = new Map<string, string>();
|
||||
frames.forEach(frame => {
|
||||
result.set(frame.id, before);
|
||||
before = generateKeyBetweenV2(before, null);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function prepareClipboardData(
|
||||
selectedAll: GfxModel[],
|
||||
std: BlockStdScope
|
||||
) {
|
||||
const job = std.store.getTransformer();
|
||||
const selected = await Promise.all(
|
||||
selectedAll.map(async selected => {
|
||||
const data = serializeElement(selected, selectedAll, job);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (isAttachmentBlock(selected) || isImageBlock(selected)) {
|
||||
await job.assetsManager.readFromBlob(data.props.sourceId as string);
|
||||
}
|
||||
return data;
|
||||
})
|
||||
);
|
||||
const blobs = await encodeClipboardBlobs(job.assetsManager.getAssets());
|
||||
return {
|
||||
snapshot: selected.filter(d => !!d),
|
||||
blobs,
|
||||
};
|
||||
}
|
||||
|
||||
export function isPureFileInClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
return (
|
||||
(types.length === 1 && types[0] === 'Files') ||
|
||||
(types.length === 2 &&
|
||||
(types.includes('text/plain') || types.includes('text/html')) &&
|
||||
types.includes('Files'))
|
||||
);
|
||||
}
|
||||
|
||||
export function tryGetSvgFromClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
|
||||
if (types.length === 1 && types[0] !== 'text/plain') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(
|
||||
clipboardData.getData('text/plain'),
|
||||
'image/svg+xml'
|
||||
);
|
||||
const svg = svgDoc.documentElement;
|
||||
|
||||
if (svg.tagName !== 'svg' || !svg.hasAttribute('xmlns')) {
|
||||
return null;
|
||||
}
|
||||
const svgContent = DOMPurify.sanitize(svgDoc.documentElement, {
|
||||
USE_PROFILES: { svg: true },
|
||||
});
|
||||
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
const file = new File([blob], 'pasted-image.svg', { type: 'image/svg+xml' });
|
||||
return file;
|
||||
}
|
||||
|
||||
export function edgelessElementsBoundFromRawData(
|
||||
elementsRawData: (SerializedElement | BlockSnapshot)[]
|
||||
) {
|
||||
if (elementsRawData.length === 0) return new Bound();
|
||||
|
||||
let prev: Bound | null = null;
|
||||
|
||||
for (const data of elementsRawData) {
|
||||
const { data: blockSnapshot } = BlockSnapshotSchema.safeParse(data);
|
||||
const bound = blockSnapshot
|
||||
? getBoundFromGfxBlockSnapshot(blockSnapshot)
|
||||
: getBoundFromSerializedElement(data as SerializedElement);
|
||||
|
||||
if (!bound) continue;
|
||||
if (!prev) prev = bound;
|
||||
else prev = prev.unite(bound);
|
||||
}
|
||||
|
||||
return prev ?? new Bound();
|
||||
}
|
||||
|
||||
function getBoundFromSerializedElement(element: SerializedElement) {
|
||||
return Bound.from(
|
||||
getBoundWithRotation({
|
||||
...Bound.deserialize(element.xywh),
|
||||
rotate: typeof element.rotate === 'number' ? element.rotate : 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getBoundFromGfxBlockSnapshot(snapshot: BlockSnapshot) {
|
||||
if (typeof snapshot.props.xywh !== 'string') return null;
|
||||
return Bound.deserialize(snapshot.props.xywh);
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
|
||||
import { EdgelessClipboardController } from '../../clipboard/clipboard';
|
||||
import { duplicate } from '../../utils/clipboard-utils';
|
||||
import { getSortedCloneElements } from '../../utils/clone-utils';
|
||||
import { moveConnectors } from '../../utils/connector';
|
||||
@@ -154,10 +155,12 @@ export const moreActions = [
|
||||
const models = ctx.getSurfaceModels();
|
||||
if (!models.length) return;
|
||||
|
||||
const edgeless = getEdgelessWith(ctx);
|
||||
if (!edgeless) return;
|
||||
const edgelessClipboard = ctx.std.getOptional(
|
||||
EdgelessClipboardController
|
||||
);
|
||||
if (!edgelessClipboard) return;
|
||||
|
||||
edgeless.clipboardController.copy();
|
||||
edgelessClipboard.copy();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -48,7 +48,6 @@ import { query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { EdgelessRootBlockWidgetName } from '../types.js';
|
||||
import { EdgelessClipboardController } from './clipboard/clipboard.js';
|
||||
import type { EdgelessSelectedRectWidget } from './components/rects/edgeless-selected-rect.js';
|
||||
import { EdgelessPageKeyboardManager } from './edgeless-keyboard.js';
|
||||
import type { EdgelessRootService } from './edgeless-root-service.js';
|
||||
@@ -124,8 +123,6 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
|
||||
private _resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
clipboardController = new EdgelessClipboardController(this);
|
||||
|
||||
keyboardManager: EdgelessPageKeyboardManager | null = null;
|
||||
|
||||
get dispatcher() {
|
||||
@@ -473,7 +470,6 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
|
||||
this._initViewport();
|
||||
|
||||
this.clipboardController.hostConnected();
|
||||
this.keyboardManager = new EdgelessPageKeyboardManager(this);
|
||||
|
||||
this.handleEvent('selectionChange', () => {
|
||||
@@ -493,7 +489,6 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.clipboardController.hostDisconnected();
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
this._resizeObserver = null;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
import { CommonSpecs } from '../common-specs/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 { EdgelessClipboardController } from './clipboard/clipboard.js';
|
||||
import {
|
||||
EdgelessClipboardAttachmentConfig,
|
||||
EdgelessClipboardBookmarkConfig,
|
||||
@@ -113,6 +114,7 @@ export const EdgelessRootBlockSpec: ExtensionType[] = [
|
||||
edgelessNavigatorBgWidget,
|
||||
edgelessSelectedRectWidget,
|
||||
edgelessToolbarWidget,
|
||||
EdgelessClipboardController,
|
||||
];
|
||||
|
||||
export const PreviewEdgelessRootBlockSpec: ExtensionType[] = [
|
||||
|
||||
@@ -49,7 +49,7 @@ import { effect } from '@preact/signals-core';
|
||||
import clamp from 'lodash-es/clamp';
|
||||
import last from 'lodash-es/last';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../index.js';
|
||||
import { EdgelessClipboardController } from '../clipboard/clipboard.js';
|
||||
import { prepareCloneData } from '../utils/clone-utils.js';
|
||||
import { calPanDelta } from '../utils/panning-utils.js';
|
||||
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
|
||||
@@ -238,10 +238,9 @@ export class DefaultTool extends BaseTool {
|
||||
private async _cloneContent() {
|
||||
if (!this._edgeless) return;
|
||||
|
||||
// FIXME: edgeless clipboard should be an extension
|
||||
const clipboardController = (
|
||||
this._edgeless as EdgelessRootBlockComponent | null
|
||||
)?.clipboardController;
|
||||
const clipboardController = this.std.getOptional(
|
||||
EdgelessClipboardController
|
||||
);
|
||||
if (!clipboardController) return;
|
||||
const snapshot = prepareCloneData(this._toBeMoved, this.std);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './clipboard/clipboard';
|
||||
export { EdgelessTemplatePanel } from './components/toolbar/template/template-panel.js';
|
||||
export * from './components/toolbar/template/template-type.js';
|
||||
export * from './edgeless-root-block.js';
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { SerializedElement } from '@blocksuite/block-std/gfx';
|
||||
import { Bound, getBoundWithRotation } from '@blocksuite/global/gfx';
|
||||
import { type BlockSnapshot, BlockSnapshotSchema } from '@blocksuite/store';
|
||||
|
||||
export function getBoundFromSerializedElement(element: SerializedElement) {
|
||||
return Bound.from(
|
||||
getBoundWithRotation({
|
||||
...Bound.deserialize(element.xywh),
|
||||
rotate: typeof element.rotate === 'number' ? element.rotate : 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function getBoundFromGfxBlockSnapshot(snapshot: BlockSnapshot) {
|
||||
if (typeof snapshot.props.xywh !== 'string') return null;
|
||||
return Bound.deserialize(snapshot.props.xywh);
|
||||
}
|
||||
|
||||
export function edgelessElementsBoundFromRawData(
|
||||
elementsRawData: (SerializedElement | BlockSnapshot)[]
|
||||
) {
|
||||
if (elementsRawData.length === 0) return new Bound();
|
||||
|
||||
let prev: Bound | null = null;
|
||||
|
||||
for (const data of elementsRawData) {
|
||||
const { data: blockSnapshot } = BlockSnapshotSchema.safeParse(data);
|
||||
const bound = blockSnapshot
|
||||
? getBoundFromGfxBlockSnapshot(blockSnapshot)
|
||||
: getBoundFromSerializedElement(data as SerializedElement);
|
||||
|
||||
if (!bound) continue;
|
||||
if (!prev) prev = bound;
|
||||
else prev = prev.unite(bound);
|
||||
}
|
||||
|
||||
return prev ?? new Bound();
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { isFrameBlock } from '@blocksuite/affine-block-frame';
|
||||
import {
|
||||
EdgelessFrameManager,
|
||||
EdgelessFrameManagerIdentifier,
|
||||
isFrameBlock,
|
||||
} from '@blocksuite/affine-block-frame';
|
||||
import { isNoteBlock } from '@blocksuite/affine-block-surface';
|
||||
getSurfaceComponent,
|
||||
isNoteBlock,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type {
|
||||
EdgelessTextBlockModel,
|
||||
EmbedSyncedDocModel,
|
||||
FrameBlockModel,
|
||||
FrameBlockProps,
|
||||
ImageBlockModel,
|
||||
NoteBlockModel,
|
||||
ShapeElementModel,
|
||||
@@ -16,15 +14,13 @@ import type {
|
||||
import { getElementsWithoutGroup } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
import {
|
||||
generateKeyBetweenV2,
|
||||
GfxControllerIdentifier,
|
||||
type GfxModel,
|
||||
type SerializedElement,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import { getCommonBoundWithRotation } from '@blocksuite/global/gfx';
|
||||
import { type BlockSnapshot, BlockSnapshotSchema } from '@blocksuite/store';
|
||||
import groupBy from 'lodash-es/groupBy';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js';
|
||||
import { EdgelessClipboardController } from '../clipboard/clipboard.js';
|
||||
import { getSortedCloneElements, prepareCloneData } from './clone-utils.js';
|
||||
import {
|
||||
isEdgelessTextBlock,
|
||||
@@ -34,28 +30,33 @@ import {
|
||||
|
||||
const offset = 10;
|
||||
export async function duplicate(
|
||||
edgeless: EdgelessRootBlockComponent,
|
||||
edgeless: BlockComponent,
|
||||
elements: GfxModel[],
|
||||
select = true
|
||||
) {
|
||||
const { clipboardController } = edgeless;
|
||||
const edgelessClipboard = edgeless.std.get(EdgelessClipboardController);
|
||||
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
||||
|
||||
const surface = getSurfaceComponent(edgeless.std);
|
||||
if (!surface) return;
|
||||
|
||||
const copyElements = getSortedCloneElements(elements);
|
||||
const totalBound = getCommonBoundWithRotation(copyElements);
|
||||
totalBound.x += totalBound.w + offset;
|
||||
|
||||
const snapshot = prepareCloneData(copyElements, edgeless.std);
|
||||
const { canvasElements, blockModels } =
|
||||
await clipboardController.createElementsFromClipboardData(
|
||||
await edgelessClipboard.createElementsFromClipboardData(
|
||||
snapshot,
|
||||
totalBound.center
|
||||
);
|
||||
|
||||
const newElements = [...canvasElements, ...blockModels];
|
||||
|
||||
edgeless.surface.fitToViewport(totalBound);
|
||||
surface.fitToViewport(totalBound);
|
||||
|
||||
if (select) {
|
||||
edgeless.service.selection.set({
|
||||
gfx.selection.set({
|
||||
elements: newElements.map(e => e.id),
|
||||
editing: false,
|
||||
});
|
||||
@@ -94,29 +95,3 @@ export const splitElements = (elements: GfxModel[]) => {
|
||||
embedSyncedDocs: embedSyncedDocs ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
type FrameSnapshot = BlockSnapshot & {
|
||||
props: FrameBlockProps;
|
||||
};
|
||||
|
||||
export function createNewPresentationIndexes(
|
||||
raw: (SerializedElement | BlockSnapshot)[],
|
||||
edgeless: BlockComponent
|
||||
) {
|
||||
const frames = raw
|
||||
.filter((block): block is FrameSnapshot => {
|
||||
const { data } = BlockSnapshotSchema.safeParse(block);
|
||||
return data?.flavour === 'affine:frame';
|
||||
})
|
||||
.sort((a, b) => EdgelessFrameManager.framePresentationComparator(a, b));
|
||||
|
||||
const frameMgr = edgeless.std.get(EdgelessFrameManagerIdentifier);
|
||||
let before = frameMgr.generatePresentationIndex();
|
||||
const result = new Map<string, string>();
|
||||
frames.forEach(frame => {
|
||||
result.set(frame.id, before);
|
||||
before = generateKeyBetweenV2(before, null);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import { css, html } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { PageClipboard } from '../clipboard/index.js';
|
||||
import type { PageRootBlockWidgetName } from '../index.js';
|
||||
import { PageKeyboardManager } from '../keyboard/keyboard-manager.js';
|
||||
import type { PageRootService } from './page-root-service.js';
|
||||
@@ -114,8 +113,6 @@ export class PageRootBlockComponent extends BlockComponent<
|
||||
}
|
||||
`;
|
||||
|
||||
clipboardController = new PageClipboard(this);
|
||||
|
||||
/**
|
||||
* Focus the first paragraph in the default note block.
|
||||
* If there is no paragraph, create one.
|
||||
@@ -211,7 +208,6 @@ export class PageRootBlockComponent extends BlockComponent<
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.clipboardController.hostConnected();
|
||||
|
||||
this.keyboardManager = new PageKeyboardManager(this);
|
||||
|
||||
@@ -386,7 +382,6 @@ export class PageRootBlockComponent extends BlockComponent<
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.clipboardController.hostDisconnected();
|
||||
this._disposables.dispose();
|
||||
this.keyboardManager = null;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BlockViewExtension, WidgetViewExtension } from '@blocksuite/block-std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { PageClipboard } from '../clipboard/page-clipboard.js';
|
||||
import { CommonSpecs } from '../common-specs/index.js';
|
||||
import { AFFINE_KEYBOARD_TOOLBAR_WIDGET } from '../widgets/keyboard-toolbar/index.js';
|
||||
import { AFFINE_PAGE_DRAGGING_AREA_WIDGET } from '../widgets/page-dragging-area/page-dragging-area.js';
|
||||
@@ -31,6 +32,7 @@ export const PageRootBlockSpec: ExtensionType[] = [
|
||||
...PageCommonExtension,
|
||||
BlockViewExtension('affine:page', literal`affine-page-root`),
|
||||
keyboardToolbarWidget,
|
||||
PageClipboard,
|
||||
].flat();
|
||||
|
||||
export const PreviewPageRootBlockSpec: ExtensionType[] = [
|
||||
|
||||
@@ -4,8 +4,6 @@ import { BlockComponent } from '@blocksuite/block-std';
|
||||
import { css, html } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { ReadOnlyClipboard } from '../clipboard/readonly-clipboard';
|
||||
|
||||
export class PreviewRootBlockComponent extends BlockComponent {
|
||||
static override styles = css`
|
||||
affine-preview-root {
|
||||
@@ -13,16 +11,12 @@ export class PreviewRootBlockComponent extends BlockComponent {
|
||||
}
|
||||
`;
|
||||
|
||||
clipboardController = new ReadOnlyClipboard(this);
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.clipboardController.hostConnected();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.clipboardController.hostDisconnected();
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { PageRootService } from '../page/page-root-service.js';
|
||||
|
||||
export const PreviewPageSpec: ExtensionType[] = [
|
||||
FlavourExtension('affine:page'),
|
||||
PageRootService,
|
||||
BlockViewExtension('affine:page', literal`affine-preview-root`),
|
||||
];
|
||||
@@ -1,4 +1,7 @@
|
||||
import { splitElements } from '@blocksuite/affine/blocks/root';
|
||||
import {
|
||||
EdgelessClipboardController,
|
||||
splitElements,
|
||||
} from '@blocksuite/affine/blocks/root';
|
||||
import { AIStarIconWithAnimation } from '@blocksuite/affine/components/icons';
|
||||
import {
|
||||
MindmapElementModel,
|
||||
@@ -52,7 +55,6 @@ import { mindMapToMarkdown } from '../../utils/edgeless';
|
||||
import { canvasToBlob, randomSeed } from '../../utils/image';
|
||||
import {
|
||||
getCopilotSelectedElems,
|
||||
getEdgelessRootFromEditor,
|
||||
imageCustomInput,
|
||||
} from '../../utils/selection-utils';
|
||||
|
||||
@@ -312,17 +314,16 @@ const generateGroup: AIItemGroupConfig = {
|
||||
};
|
||||
}
|
||||
|
||||
// image to image
|
||||
const edgelessRoot = getEdgelessRootFromEditor(host);
|
||||
const canvas = await edgelessRoot.clipboardController.toCanvas(
|
||||
images,
|
||||
pureShapes,
|
||||
{
|
||||
dpr: 1,
|
||||
padding: 0,
|
||||
background: 'white',
|
||||
}
|
||||
const edgelessClipboard = host.std.getOptional(
|
||||
EdgelessClipboardController
|
||||
);
|
||||
if (!edgelessClipboard) return;
|
||||
// image to image
|
||||
const canvas = await edgelessClipboard.toCanvas(images, pureShapes, {
|
||||
dpr: 1,
|
||||
padding: 0,
|
||||
background: 'white',
|
||||
});
|
||||
if (!canvas) return;
|
||||
|
||||
const png = await canvasToBlob(canvas);
|
||||
@@ -445,8 +446,11 @@ const generateGroup: AIItemGroupConfig = {
|
||||
content = aiPanel.inputText?.trim() || '';
|
||||
}
|
||||
|
||||
const edgelessRoot = getEdgelessRootFromEditor(host);
|
||||
const canvas = await edgelessRoot.clipboardController.toCanvas(
|
||||
const edgelessClipboard = host.std.getOptional(
|
||||
EdgelessClipboardController
|
||||
);
|
||||
if (!edgelessClipboard) return;
|
||||
const canvas = await edgelessClipboard.toCanvas(
|
||||
[...notes, ...frames, ...images],
|
||||
shapes,
|
||||
{
|
||||
|
||||
@@ -3,7 +3,11 @@ import {
|
||||
GfxControllerIdentifier,
|
||||
type GfxModel,
|
||||
} from '@blocksuite/affine/block-std/gfx';
|
||||
import { isCanvasElement, splitElements } from '@blocksuite/affine/blocks/root';
|
||||
import {
|
||||
EdgelessClipboardController,
|
||||
isCanvasElement,
|
||||
splitElements,
|
||||
} from '@blocksuite/affine/blocks/root';
|
||||
import {
|
||||
getSurfaceBlock,
|
||||
type SurfaceBlockComponent,
|
||||
@@ -73,10 +77,9 @@ export async function elementsToCanvas(host: EditorHost, elements: GfxModel[]) {
|
||||
}
|
||||
|
||||
try {
|
||||
const canvas = await edgelessRoot.clipboardController.toCanvas(
|
||||
blockElements,
|
||||
shapes
|
||||
);
|
||||
const canvas = await edgelessRoot.std
|
||||
.get(EdgelessClipboardController)
|
||||
.toCanvas(blockElements, shapes);
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user