mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
|
||||
import { downloadBlob } from '@blocksuite/affine-shared/utils';
|
||||
|
||||
import type { EdgelessRootPreviewBlockComponent } from '../../edgeless/edgeless-root-preview-block.js';
|
||||
import type { SurfaceRefToolbarContext } from './context.js';
|
||||
import { edgelessToBlob, writeImageBlobToClipboard } from './utils.js';
|
||||
|
||||
export const BUILT_IN_GROUPS: MenuItemGroup<SurfaceRefToolbarContext>[] = [
|
||||
{
|
||||
type: 'clipboard',
|
||||
when: ctx => !!(ctx.blockComponent.referenceModel && ctx.doc.root),
|
||||
items: [
|
||||
{
|
||||
type: 'copy',
|
||||
label: 'Copy',
|
||||
icon: CopyIcon,
|
||||
action: ctx => {
|
||||
if (!(ctx.blockComponent.referenceModel && ctx.doc.root?.id)) {
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedModel = ctx.blockComponent.referenceModel;
|
||||
const editor = ctx.blockComponent.previewEditor;
|
||||
const edgelessRootElement = editor?.view.getBlock(ctx.doc.root.id);
|
||||
const surfaceRenderer = (
|
||||
edgelessRootElement as EdgelessRootPreviewBlockComponent
|
||||
)?.surface?.renderer;
|
||||
|
||||
if (!surfaceRenderer) {
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
edgelessToBlob(ctx.host, {
|
||||
surfaceRefBlock: ctx.blockComponent,
|
||||
surfaceRenderer,
|
||||
edgelessElement: referencedModel,
|
||||
})
|
||||
.then(blob => writeImageBlobToClipboard(blob))
|
||||
.then(() => toast(ctx.host, 'Copied image to clipboard'))
|
||||
.catch(console.error);
|
||||
|
||||
ctx.close();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'download',
|
||||
label: 'Download',
|
||||
icon: DownloadIcon,
|
||||
action: ctx => {
|
||||
if (!(ctx.blockComponent.referenceModel && ctx.doc.root?.id)) {
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedModel = ctx.blockComponent.referenceModel;
|
||||
const editor = ctx.blockComponent.previewEditor;
|
||||
const edgelessRootElement = editor?.view.getBlock(ctx.doc.root.id);
|
||||
const surfaceRenderer = (
|
||||
edgelessRootElement as EdgelessRootPreviewBlockComponent
|
||||
)?.surface?.renderer;
|
||||
|
||||
if (!surfaceRenderer) {
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
edgelessToBlob(ctx.host, {
|
||||
surfaceRefBlock: ctx.blockComponent,
|
||||
surfaceRenderer,
|
||||
edgelessElement: referencedModel,
|
||||
})
|
||||
.then(blob => {
|
||||
const fileName =
|
||||
'title' in referencedModel
|
||||
? (referencedModel.title?.toString() ?? 'Edgeless Content')
|
||||
: 'Edgeless Content';
|
||||
|
||||
downloadBlob(blob, fileName);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
ctx.close();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
items: [
|
||||
{
|
||||
type: 'delete',
|
||||
label: 'Delete',
|
||||
icon: DeleteIcon,
|
||||
disabled: ({ doc }) => doc.readonly,
|
||||
action: ({ blockComponent, doc, close }) => {
|
||||
doc.deleteBlock(blockComponent.model);
|
||||
close();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { SurfaceRefBlockComponent } from '../../../surface-ref-block/surface-ref-block.js';
|
||||
import { MenuContext } from '../../configs/toolbar.js';
|
||||
|
||||
export class SurfaceRefToolbarContext extends MenuContext {
|
||||
override close = () => {
|
||||
this.abortController.abort();
|
||||
};
|
||||
|
||||
get doc() {
|
||||
return this.blockComponent.doc;
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this.blockComponent.host;
|
||||
}
|
||||
|
||||
get selectedBlockModels() {
|
||||
if (this.blockComponent) return [this.blockComponent.model];
|
||||
return [];
|
||||
}
|
||||
|
||||
get std() {
|
||||
return this.host.std;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public blockComponent: SurfaceRefBlockComponent,
|
||||
public abortController: AbortController
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return !this.blockComponent;
|
||||
}
|
||||
|
||||
isMultiple() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSingle() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import { HoverController } from '@blocksuite/affine-components/hover';
|
||||
import {
|
||||
CaptionIcon,
|
||||
CenterPeekIcon,
|
||||
EdgelessModeIcon,
|
||||
MoreVerticalIcon,
|
||||
OpenIcon,
|
||||
SmallArrowDownIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { isPeekable, peek } from '@blocksuite/affine-components/peek';
|
||||
import {
|
||||
cloneGroups,
|
||||
type MenuItem,
|
||||
type MenuItemGroup,
|
||||
renderGroups,
|
||||
renderToolbarSeparator,
|
||||
} from '@blocksuite/affine-components/toolbar';
|
||||
import type { SurfaceRefBlockModel } from '@blocksuite/affine-model';
|
||||
import { WidgetComponent } from '@blocksuite/block-std';
|
||||
import { offset, shift } from '@floating-ui/dom';
|
||||
import { html, nothing } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { join } from 'lit/directives/join.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { PAGE_HEADER_HEIGHT } from '../../../_common/consts.js';
|
||||
import type { SurfaceRefBlockComponent } from '../../../surface-ref-block/index.js';
|
||||
import { getMoreMenuConfig } from '../../configs/toolbar.js';
|
||||
import { BUILT_IN_GROUPS } from './config.js';
|
||||
import { SurfaceRefToolbarContext } from './context.js';
|
||||
|
||||
export const AFFINE_SURFACE_REF_TOOLBAR = 'affine-surface-ref-toolbar';
|
||||
|
||||
export class AffineSurfaceRefToolbar extends WidgetComponent<
|
||||
SurfaceRefBlockModel,
|
||||
SurfaceRefBlockComponent
|
||||
> {
|
||||
/*
|
||||
* Caches the more menu items.
|
||||
* Currently only supports configuring more menu.
|
||||
*/
|
||||
moreGroups: MenuItemGroup<SurfaceRefToolbarContext>[] =
|
||||
cloneGroups(BUILT_IN_GROUPS);
|
||||
|
||||
private _hoverController = new HoverController(
|
||||
this,
|
||||
({ abortController }) => {
|
||||
const surfaceRefBlock = this.block;
|
||||
const selection = this.host.selection;
|
||||
|
||||
const textSelection = selection.find('text');
|
||||
if (
|
||||
!!textSelection &&
|
||||
(!!textSelection.to || !!textSelection.from.length)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blockSelections = selection.filter('block');
|
||||
if (
|
||||
blockSelections.length > 1 ||
|
||||
(blockSelections.length === 1 &&
|
||||
blockSelections[0].blockId !== surfaceRefBlock.blockId)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
template: SurfaceRefToolbarOptions({
|
||||
context: new SurfaceRefToolbarContext(this.block, abortController),
|
||||
groups: this.moreGroups,
|
||||
}),
|
||||
computePosition: {
|
||||
referenceElement: this.block,
|
||||
placement: 'top-start',
|
||||
middleware: [
|
||||
offset({
|
||||
mainAxis: 12,
|
||||
crossAxis: 10,
|
||||
}),
|
||||
shift({
|
||||
crossAxis: true,
|
||||
padding: {
|
||||
top: PAGE_HEADER_HEIGHT + 12,
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
},
|
||||
}),
|
||||
],
|
||||
autoUpdate: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.moreGroups = getMoreMenuConfig(this.std).configure(this.moreGroups);
|
||||
this._hoverController.setReference(this.block);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_SURFACE_REF_TOOLBAR]: AffineSurfaceRefToolbar;
|
||||
}
|
||||
}
|
||||
|
||||
function SurfaceRefToolbarOptions({
|
||||
context,
|
||||
groups,
|
||||
}: {
|
||||
context: SurfaceRefToolbarContext;
|
||||
groups: MenuItemGroup<SurfaceRefToolbarContext>[];
|
||||
}) {
|
||||
const { blockComponent, abortController } = context;
|
||||
const readonly = blockComponent.model.doc.readonly;
|
||||
const hasValidReference = !!blockComponent.referenceModel;
|
||||
|
||||
const openMenuActions: MenuItem[] = [];
|
||||
if (hasValidReference) {
|
||||
openMenuActions.push({
|
||||
type: 'open-in-edgeless',
|
||||
label: 'Open in edgeless',
|
||||
icon: EdgelessModeIcon,
|
||||
action: () => blockComponent.viewInEdgeless(),
|
||||
disabled: readonly,
|
||||
});
|
||||
|
||||
if (isPeekable(blockComponent)) {
|
||||
openMenuActions.push({
|
||||
type: 'open-in-center-peek',
|
||||
label: 'Open in center peek',
|
||||
icon: CenterPeekIcon,
|
||||
action: () => peek(blockComponent),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const moreMenuActions = renderGroups(groups, context);
|
||||
|
||||
const buttons = [
|
||||
openMenuActions.length
|
||||
? html`
|
||||
<editor-menu-button
|
||||
.contentPadding=${'8px'}
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Open doc"
|
||||
.justify=${'space-between'}
|
||||
.labelHeight=${'20px'}
|
||||
>
|
||||
${OpenIcon}${SmallArrowDownIcon}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="large" data-orientation="vertical">
|
||||
${repeat(
|
||||
openMenuActions,
|
||||
button => button.label,
|
||||
({ label, icon, action, disabled }) => html`
|
||||
<editor-menu-action
|
||||
aria-label=${ifDefined(label)}
|
||||
?disabled=${disabled}
|
||||
@click=${action}
|
||||
>
|
||||
${icon}<span class="label">${label}</span>
|
||||
</editor-menu-action>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`
|
||||
: nothing,
|
||||
|
||||
readonly
|
||||
? nothing
|
||||
: html`
|
||||
<editor-icon-button
|
||||
aria-label="Caption"
|
||||
.tooltip=${'Add Caption'}
|
||||
@click=${() => {
|
||||
abortController.abort();
|
||||
blockComponent.captionElement.show();
|
||||
}}
|
||||
>
|
||||
${CaptionIcon}
|
||||
</editor-icon-button>
|
||||
`,
|
||||
|
||||
html`
|
||||
<editor-menu-button
|
||||
.contentPadding=${'8px'}
|
||||
.button=${html`
|
||||
<editor-icon-button aria-label="More" .tooltip=${'More'}>
|
||||
${MoreVerticalIcon}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="large" data-orientation="vertical">
|
||||
${moreMenuActions}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`,
|
||||
];
|
||||
|
||||
return html`
|
||||
<editor-toolbar class="surface-ref-toolbar-container">
|
||||
${join(
|
||||
buttons.filter(button => button !== nothing),
|
||||
renderToolbarSeparator
|
||||
)}
|
||||
</editor-toolbar>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { CanvasRenderer } from '@blocksuite/affine-block-surface';
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import { assertExists, Bound } from '@blocksuite/global/utils';
|
||||
|
||||
import { ExportManager } from '../../../_common/export-manager/export-manager.js';
|
||||
import { isTopLevelBlock } from '../../../root-block/edgeless/utils/query.js';
|
||||
import type { SurfaceRefBlockComponent } from '../../../surface-ref-block/surface-ref-block.js';
|
||||
|
||||
export const edgelessToBlob = async (
|
||||
host: EditorHost,
|
||||
options: {
|
||||
surfaceRefBlock: SurfaceRefBlockComponent;
|
||||
surfaceRenderer: CanvasRenderer;
|
||||
edgelessElement: BlockSuite.EdgelessModel;
|
||||
}
|
||||
): Promise<Blob> => {
|
||||
const { edgelessElement } = options;
|
||||
const exportManager = host.std.get(ExportManager);
|
||||
const bound = Bound.deserialize(edgelessElement.xywh);
|
||||
const isBlock = isTopLevelBlock(edgelessElement);
|
||||
|
||||
return exportManager
|
||||
.edgelessToCanvas(
|
||||
options.surfaceRenderer,
|
||||
bound,
|
||||
undefined,
|
||||
isBlock ? [edgelessElement] : undefined,
|
||||
isBlock ? undefined : [edgelessElement],
|
||||
{ zoom: options.surfaceRenderer.viewport.zoom }
|
||||
)
|
||||
.then(canvas => {
|
||||
assertExists(canvas);
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
blob => (blob ? resolve(blob) : reject(null)),
|
||||
'image/png'
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const writeImageBlobToClipboard = async (blob: Blob) => {
|
||||
// @ts-expect-error FIXME: ts error
|
||||
if (window.apis?.clipboard?.copyAsImageFromString) {
|
||||
// @ts-expect-error FIXME: ts error
|
||||
await window.apis.clipboard?.copyAsImageFromString(blob);
|
||||
} else {
|
||||
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user