feat(editor): extensionize clipboards (#11172)

This commit is contained in:
Saul-Mirone
2025-03-25 12:09:24 +00:00
parent a4033f5596
commit 1871428021
18 changed files with 283 additions and 306 deletions
@@ -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;
}