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