chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -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 })]);
}
};