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,49 @@
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { renderGroups } from '@blocksuite/affine-components/toolbar';
import { WithDisposable } from '@blocksuite/global/utils';
import { MoreHorizontalIcon, MoreVerticalIcon } from '@blocksuite/icons/lit';
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import type { EdgelessRootBlockComponent } from '../../../edgeless/edgeless-root-block.js';
import { ElementToolbarMoreMenuContext } from './context.js';
export class EdgelessMoreButton extends WithDisposable(LitElement) {
override render() {
const context = new ElementToolbarMoreMenuContext(this.edgeless);
const actions = renderGroups(this.groups, context);
return html`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button aria-label="More" .tooltip=${'More'}>
${this.vertical
? MoreVerticalIcon({ width: '20', height: '20' })
: MoreHorizontalIcon({ width: '20', height: '20' })}
</editor-icon-button>
`}
>
<div
class="more-actions-container"
data-size="large"
data-orientation="vertical"
>
${actions}
</div>
</editor-menu-button>
`;
}
@property({ attribute: false })
accessor edgeless!: EdgelessRootBlockComponent;
@property({ attribute: false })
accessor elements: BlockSuite.EdgelessModel[] = [];
@property({ attribute: false })
accessor groups!: MenuItemGroup<ElementToolbarMoreMenuContext>[];
@property({ attribute: false })
accessor vertical = false;
}
@@ -0,0 +1,398 @@
import type {
EmbedFigmaBlockComponent,
EmbedGithubBlockComponent,
EmbedLoomBlockComponent,
EmbedYoutubeBlockComponent,
} from '@blocksuite/affine-block-embed';
import { isPeekable, peek } from '@blocksuite/affine-components/peek';
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import { Bound, getCommonBoundWithRotation } from '@blocksuite/global/utils';
import {
ArrowDownBigBottomIcon,
ArrowDownBigIcon,
ArrowUpBigIcon,
ArrowUpBigTopIcon,
CenterPeekIcon,
CopyIcon,
DeleteIcon,
DuplicateIcon,
FrameIcon,
GroupIcon,
LinkedPageIcon,
OpenInNewIcon,
ResetIcon,
} from '@blocksuite/icons/lit';
import {
createLinkedDocFromEdgelessElements,
createLinkedDocFromNote,
notifyDocCreated,
promptDocTitle,
} from '../../../../_common/utils/render-linked-doc.js';
import type { AttachmentBlockComponent } from '../../../../attachment-block/attachment-block.js';
import type { BookmarkBlockComponent } from '../../../../bookmark-block/bookmark-block.js';
import type { ImageBlockComponent } from '../../../../image-block/image-block.js';
import { duplicate } from '../../../edgeless/utils/clipboard-utils.js';
import { getSortedCloneElements } from '../../../edgeless/utils/clone-utils.js';
import { moveConnectors } from '../../../edgeless/utils/connector.js';
import { deleteElements } from '../../../edgeless/utils/crud.js';
import type { ElementToolbarMoreMenuContext } from './context.js';
type EmbedLinkBlockComponent =
| EmbedGithubBlockComponent
| EmbedFigmaBlockComponent
| EmbedLoomBlockComponent
| EmbedYoutubeBlockComponent;
type RefreshableBlockComponent =
| EmbedLinkBlockComponent
| ImageBlockComponent
| AttachmentBlockComponent
| BookmarkBlockComponent;
// Section Group: frame & group
export const sectionGroup: MenuItemGroup<ElementToolbarMoreMenuContext> = {
type: 'section',
items: [
{
icon: FrameIcon({ width: '20', height: '20' }),
label: 'Frame section',
type: 'create-frame',
action: ({ service, edgeless, std }) => {
const frame = service.frame.createFrameOnSelected();
if (!frame) return;
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
control: 'context-menu',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'frame',
});
edgeless.surface.fitToViewport(Bound.deserialize(frame.xywh));
},
},
{
icon: GroupIcon({ width: '20', height: '20' }),
label: 'Group section',
type: 'create-group',
action: ({ service }) => {
service.createGroupFromSelected();
},
when: ctx => !ctx.hasFrame(),
},
],
};
// Reorder Group
export const reorderGroup: MenuItemGroup<ElementToolbarMoreMenuContext> = {
type: 'reorder',
items: [
{
icon: ArrowUpBigTopIcon({ width: '20', height: '20' }),
label: 'Bring to Front',
type: 'front',
action: ({ service, selectedElements }) => {
selectedElements.forEach(el => {
service.reorderElement(el, 'front');
});
},
},
{
icon: ArrowUpBigIcon({ width: '20', height: '20' }),
label: 'Bring Forward',
type: 'forward',
action: ({ service, selectedElements }) => {
selectedElements.forEach(el => {
service.reorderElement(el, 'forward');
});
},
},
{
icon: ArrowDownBigIcon({ width: '20', height: '20' }),
label: 'Send Backward',
type: 'backward',
action: ({ service, selectedElements }) => {
selectedElements.forEach(el => {
service.reorderElement(el, 'backward');
});
},
},
{
icon: ArrowDownBigBottomIcon({ width: '20', height: '20' }),
label: 'Send to Back',
type: 'back',
action: ({ service, selectedElements }) => {
selectedElements.forEach(el => {
service.reorderElement(el, 'back');
});
},
},
],
};
// Open Group
export const openGroup: MenuItemGroup<ElementToolbarMoreMenuContext> = {
type: 'open',
items: [
{
icon: OpenInNewIcon({ width: '20', height: '20' }),
label: 'Open this doc',
type: 'open',
generate: ctx => {
const linkedDocBlock = ctx.getLinkedDocBlock();
if (!linkedDocBlock) return;
const disabled = linkedDocBlock.pageId === ctx.doc.id;
return {
action: () => {
const blockComponent = ctx.firstBlockComponent;
if (!blockComponent) return;
if (!('open' in blockComponent)) return;
if (typeof blockComponent.open !== 'function') return;
blockComponent.open();
},
disabled,
};
},
},
{
icon: CenterPeekIcon({ width: '20', height: '20' }),
label: 'Open in center peek',
type: 'center-peek',
generate: ctx => {
const valid =
ctx.isSingle() &&
!!ctx.firstBlockComponent &&
isPeekable(ctx.firstBlockComponent);
if (!valid) return;
return {
action: () => {
if (!ctx.firstBlockComponent) return;
peek(ctx.firstBlockComponent);
},
};
},
},
],
};
// Clipboard Group
export const clipboardGroup: MenuItemGroup<ElementToolbarMoreMenuContext> = {
type: 'clipboard',
items: [
{
icon: CopyIcon({ width: '20', height: '20' }),
label: 'Copy',
type: 'copy',
action: ({ edgeless }) => edgeless.clipboardController.copy(),
},
{
icon: DuplicateIcon({ width: '20', height: '20' }),
label: 'Duplicate',
type: 'duplicate',
action: ({ edgeless, selectedElements }) =>
duplicate(edgeless, selectedElements),
},
{
icon: ResetIcon({ width: '20', height: '20' }),
label: 'Reload',
type: 'reload',
generate: ctx => {
if (ctx.hasFrame()) {
return;
}
const blocks = ctx.selection.surfaceSelections
.map(s => ctx.getBlockComponent(s.blockId))
.filter(block => !!block)
.filter(block => ctx.refreshable(block.model));
if (
!blocks.length ||
blocks.length !== ctx.selection.surfaceSelections.length
) {
return;
}
return {
action: () =>
blocks.forEach(block =>
(block as RefreshableBlockComponent).refreshData()
),
};
},
},
],
};
// Conversions Group
export const conversionsGroup: MenuItemGroup<ElementToolbarMoreMenuContext> = {
type: 'conversions',
items: [
{
icon: LinkedPageIcon({ width: '20', height: '20' }),
label: 'Turn into linked doc',
type: 'turn-into-linked-doc',
action: async ctx => {
const { doc, service, surface, host, std } = ctx;
const element = ctx.getNoteBlock();
if (!element) return;
const title = await promptDocTitle(host);
if (title === null) return;
const linkedDoc = createLinkedDocFromNote(doc, element, title);
// insert linked doc card
const cardId = service.addBlock(
'affine:embed-synced-doc',
{
xywh: element.xywh,
style: 'syncedDoc',
pageId: linkedDoc.id,
index: element.index,
},
surface.model.id
);
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
control: 'context-menu',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'embed-synced-doc',
});
std.getOptional(TelemetryProvider)?.track('DocCreated', {
control: 'turn into linked doc',
page: 'whiteboard editor',
module: 'format toolbar',
type: 'embed-linked-doc',
});
std.getOptional(TelemetryProvider)?.track('LinkedDocCreated', {
control: 'turn into linked doc',
page: 'whiteboard editor',
module: 'format toolbar',
type: 'embed-linked-doc',
other: 'new doc',
});
moveConnectors(element.id, cardId, service);
// delete selected note
doc.transact(() => {
doc.deleteBlock(element);
});
service.selection.set({
elements: [cardId],
editing: false,
});
},
when: ctx => !!ctx.getNoteBlock(),
},
{
icon: LinkedPageIcon({ width: '20', height: '20' }),
label: 'Create linked doc',
type: 'create-linked-doc',
action: async ({
doc,
selection,
service,
surface,
edgeless,
host,
std,
}) => {
const title = await promptDocTitle(host);
if (title === null) return;
const elements = getSortedCloneElements(selection.selectedElements);
const linkedDoc = createLinkedDocFromEdgelessElements(
host,
elements,
title
);
// delete selected elements
doc.transact(() => {
deleteElements(edgeless, elements);
});
// insert linked doc card
const width = 364;
const height = 390;
const bound = getCommonBoundWithRotation(elements);
const cardId = service.addBlock(
'affine:embed-linked-doc',
{
xywh: `[${bound.center[0] - width / 2}, ${bound.center[1] - height / 2}, ${width}, ${height}]`,
style: 'vertical',
pageId: linkedDoc.id,
},
surface.model.id
);
selection.set({
elements: [cardId],
editing: false,
});
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
control: 'context-menu',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'embed-linked-doc',
});
std.getOptional(TelemetryProvider)?.track('DocCreated', {
control: 'create linked doc',
page: 'whiteboard editor',
module: 'format toolbar',
type: 'embed-linked-doc',
});
std.getOptional(TelemetryProvider)?.track('LinkedDocCreated', {
control: 'create linked doc',
page: 'whiteboard editor',
module: 'format toolbar',
type: 'embed-linked-doc',
other: 'new doc',
});
notifyDocCreated(host, doc);
},
when: ctx => !(ctx.getLinkedDocBlock() || ctx.getNoteBlock()),
},
],
};
// Delete Group
export const deleteGroup: MenuItemGroup<ElementToolbarMoreMenuContext> = {
type: 'delete',
items: [
{
icon: DeleteIcon({ width: '20', height: '20' }),
label: 'Delete',
type: 'delete',
action: ({ doc, selection, selectedElements, edgeless }) => {
doc.captureSync();
deleteElements(edgeless, selectedElements);
selection.set({
elements: [],
editing: false,
});
},
},
],
};
export const BUILT_IN_GROUPS = [
sectionGroup,
reorderGroup,
openGroup,
clipboardGroup,
conversionsGroup,
deleteGroup,
];
@@ -0,0 +1,150 @@
import type { SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
import {
GfxPrimitiveElementModel,
type GfxSelectionManager,
} from '@blocksuite/block-std/gfx';
import type { BlockModel } from '@blocksuite/store';
import { MenuContext } from '../../../configs/toolbar.js';
import type { EdgelessRootBlockComponent } from '../../../edgeless/edgeless-root-block.js';
import type { EdgelessRootService } from '../../../edgeless/edgeless-root-service.js';
import {
isAttachmentBlock,
isBookmarkBlock,
isEmbeddedLinkBlock,
isEmbedLinkedDocBlock,
isEmbedSyncedDocBlock,
isFrameBlock,
isImageBlock,
isNoteBlock,
} from '../../../edgeless/utils/query.js';
export class ElementToolbarMoreMenuContext extends MenuContext {
#empty = true;
#includedFrame = false;
#multiple = false;
#single = false;
edgeless!: EdgelessRootBlockComponent;
get doc() {
return this.edgeless.doc;
}
get firstBlockComponent() {
return this.getBlockComponent(this.firstElement.id);
}
override get firstElement() {
return this.selection.firstElement;
}
get host() {
return this.edgeless.host;
}
get selectedBlockModels() {
const [result, { selectedModels }] = this.std.command
.chain()
.getSelectedModels()
.run();
if (!result) return [];
return selectedModels ?? [];
}
get selectedElements() {
return this.selection.selectedElements;
}
get selection(): GfxSelectionManager {
return this.service.selection;
}
get service(): EdgelessRootService {
return this.edgeless.service;
}
get std() {
return this.edgeless.host.std;
}
get surface(): SurfaceBlockComponent {
return this.edgeless.surface;
}
get view() {
return this.host.view;
}
constructor(edgeless: EdgelessRootBlockComponent) {
super();
this.edgeless = edgeless;
const selectedElements = this.selection.selectedElements;
const len = selectedElements.length;
this.#empty = len === 0;
this.#single = len === 1;
this.#multiple = !this.#empty && !this.#single;
this.#includedFrame = !this.#empty && selectedElements.some(isFrameBlock);
}
getBlockComponent(id: string) {
return this.view.getBlock(id);
}
getLinkedDocBlock() {
const valid =
this.#single &&
(isEmbedLinkedDocBlock(this.firstElement) ||
isEmbedSyncedDocBlock(this.firstElement));
if (!valid) return null;
return this.firstElement;
}
getNoteBlock() {
const valid = this.#single && isNoteBlock(this.firstElement);
if (!valid) return null;
return this.firstElement;
}
hasFrame() {
return this.#includedFrame;
}
override isElement() {
return (
this.#single && this.firstElement instanceof GfxPrimitiveElementModel
);
}
override isEmpty() {
return this.#empty;
}
isMultiple() {
return this.#multiple;
}
isSingle() {
return this.#single;
}
refreshable(model: BlockModel) {
return (
isImageBlock(model) ||
isBookmarkBlock(model) ||
isAttachmentBlock(model) ||
isEmbeddedLinkBlock(model)
);
}
}