refactor(editor): edgeless element toolbar with new pattern (#10511)

This commit is contained in:
fundon
2025-03-18 15:36:25 +00:00
parent 3939cc1c52
commit cb37d25d7b
31 changed files with 838 additions and 367 deletions

View File

@@ -136,7 +136,6 @@ export const builtinToolbarConfig = {
id: 'a.rename',
content(cx) {
const component = cx.getCurrentBlockComponentBy(
BlockSelection,
AttachmentBlockComponent
);
if (!component) return null;
@@ -178,7 +177,6 @@ export const builtinToolbarConfig = {
icon: DownloadIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
AttachmentBlockComponent
);
component?.download();
@@ -190,7 +188,6 @@ export const builtinToolbarConfig = {
icon: CaptionIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
AttachmentBlockComponent
);
component?.captionEditor?.show();
@@ -212,7 +209,6 @@ export const builtinToolbarConfig = {
run(ctx) {
// TODO(@fundon): unify `clone` method
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
AttachmentBlockComponent
);
component?.copy();
@@ -224,7 +220,6 @@ export const builtinToolbarConfig = {
icon: DuplicateIcon(),
run(ctx) {
const model = ctx.getCurrentBlockComponentBy(
BlockSelection,
AttachmentBlockComponent
)?.model;
if (!model) return;
@@ -247,7 +242,6 @@ export const builtinToolbarConfig = {
icon: ResetIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
AttachmentBlockComponent
);
component?.refreshData();
@@ -260,7 +254,7 @@ export const builtinToolbarConfig = {
icon: DeleteIcon(),
variant: 'destructive',
run(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model) return;
ctx.store.deleteBlock(model);

View File

@@ -276,7 +276,6 @@ export const builtinToolbarConfig = {
icon: CaptionIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
BookmarkBlockComponent
);
component?.captionEditor?.show();
@@ -296,7 +295,7 @@ export const builtinToolbarConfig = {
label: 'Copy',
icon: CopyIcon(),
run(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model) return;
const slice = Slice.fromModels(ctx.store, [model]);
@@ -311,7 +310,7 @@ export const builtinToolbarConfig = {
label: 'Duplicate',
icon: DuplicateIcon(),
run(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model) return;
const { flavour, parent } = model;
@@ -330,7 +329,6 @@ export const builtinToolbarConfig = {
icon: ResetIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
BookmarkBlockComponent
);
component?.refreshData();
@@ -343,7 +341,7 @@ export const builtinToolbarConfig = {
icon: DeleteIcon(),
variant: 'destructive',
run(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model) return;
ctx.store.deleteBlock(model);

View File

@@ -52,7 +52,7 @@ export function createBuiltinToolbarConfigForExternal(
{
id: 'a.preview',
content(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return null;
const { url } = model.props;
@@ -72,7 +72,7 @@ export function createBuiltinToolbarConfigForExternal(
id: 'inline',
label: 'Inline view',
run(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return;
const { title, caption, url: link } = model.props;
@@ -105,7 +105,7 @@ export function createBuiltinToolbarConfigForExternal(
id: 'card',
label: 'Card view',
disabled(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return true;
const { url } = model.props;
@@ -116,7 +116,7 @@ export function createBuiltinToolbarConfigForExternal(
return options?.viewType === 'card';
},
run(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return;
const { url, caption } = model.props;
@@ -165,7 +165,7 @@ export function createBuiltinToolbarConfigForExternal(
id: 'embed',
label: 'Embed view',
disabled(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return false;
const { url } = model.props;
@@ -176,7 +176,7 @@ export function createBuiltinToolbarConfigForExternal(
return options?.viewType === 'embed';
},
when(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return false;
const { url } = model.props;
@@ -187,7 +187,7 @@ export function createBuiltinToolbarConfigForExternal(
return options?.viewType === 'embed';
},
run(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return;
const { url, caption } = model.props;
@@ -231,7 +231,7 @@ export function createBuiltinToolbarConfigForExternal(
},
],
content(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return null;
const { url } = model.props;
@@ -322,10 +322,7 @@ export function createBuiltinToolbarConfigForExternal(
tooltip: 'Caption',
icon: CaptionIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
klass
);
const component = ctx.getCurrentBlockComponentBy(klass);
if (!component) return;
component.captionEditor?.show();
@@ -378,10 +375,7 @@ export function createBuiltinToolbarConfigForExternal(
label: 'Reload',
icon: ResetIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
klass
);
const component = ctx.getCurrentBlockComponentBy(klass);
component?.refreshData();
},
},

View File

@@ -37,7 +37,6 @@ export const builtinToolbarConfig = {
tooltip: 'Open this doc',
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedHtmlBlockComponent
);
component?.open();
@@ -99,7 +98,6 @@ export const builtinToolbarConfig = {
icon: CaptionIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedHtmlBlockComponent
);
component?.captionEditor?.show();

View File

@@ -27,9 +27,6 @@ import * as Y from 'yjs';
import { EmbedIframeBlockComponent } from '../embed-iframe-block';
const trackBaseProps = {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: 'bookmark',
type: 'card view',
};
@@ -156,7 +153,6 @@ export const builtinToolbarConfig = {
icon: CaptionIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedIframeBlockComponent
);
component?.captionEditor?.show();
@@ -210,7 +206,6 @@ export const builtinToolbarConfig = {
icon: ResetIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedIframeBlockComponent
);
component?.refreshData().catch(console.error);

View File

@@ -38,7 +38,6 @@ export const builtinToolbarConfig = {
id: 'a.doc-title',
content(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedLinkedDocBlockComponent
);
if (!component) return null;
@@ -63,7 +62,6 @@ export const builtinToolbarConfig = {
label: 'Inline view',
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedLinkedDocBlockComponent
);
component?.covertToInline();
@@ -89,7 +87,6 @@ export const builtinToolbarConfig = {
label: 'Embed view',
disabled(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedLinkedDocBlockComponent
);
if (!component) return true;
@@ -108,7 +105,6 @@ export const builtinToolbarConfig = {
},
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedLinkedDocBlockComponent
);
component?.convertToEmbed();
@@ -208,7 +204,6 @@ export const builtinToolbarConfig = {
icon: CaptionIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedLinkedDocBlockComponent
);
component?.captionEditor?.show();

View File

@@ -50,7 +50,6 @@ export const builtinToolbarConfig = {
],
content(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedSyncedDocBlockComponent
);
if (!component) return null;
@@ -117,14 +116,13 @@ export const builtinToolbarConfig = {
label: 'Inline view',
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedSyncedDocBlockComponent
);
component?.covertToInline();
// Clears
ctx.reset();
ctx.select('note');
ctx.reset();
ctx.track('SelectedView', {
...trackBaseProps,
@@ -138,7 +136,6 @@ export const builtinToolbarConfig = {
label: 'Card view',
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedSyncedDocBlockComponent
);
component?.convertToCard();
@@ -192,7 +189,6 @@ export const builtinToolbarConfig = {
icon: CaptionIcon(),
run(ctx) {
const component = ctx.getCurrentBlockComponentBy(
BlockSelection,
EmbedSyncedDocBlockComponent
);
component?.captionEditor?.show();

View File

@@ -0,0 +1,11 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinAttachmentToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Attachment',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,11 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinBookmarkToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Bookmark',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,11 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinBrushToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Brush',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,51 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
import {
AddTextIcon,
ConnectorCIcon,
FlipDirectionIcon,
StartPointIcon,
} from '@blocksuite/icons/lit';
export const builtinConnectorToolbarConfig = {
actions: [
{
id: 'a.stroke-color',
tooltip: 'Stroke style',
run() {},
},
{
id: 'b.style',
tooltip: 'Style',
run() {},
},
{
id: 'c.start-point-style',
icon: StartPointIcon(),
tooltip: 'Start point style',
run() {},
},
{
id: 'd.flip-direction',
icon: FlipDirectionIcon(),
tooltip: 'Flip direction',
run() {},
},
{
id: 'e.end-point-style',
icon: StartPointIcon(),
tooltip: 'End point style',
run() {},
},
{
id: 'f.connector-shape',
icon: ConnectorCIcon(),
tooltip: 'Connector shape',
run() {},
},
{
id: 'g.add-text',
icon: AddTextIcon(),
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,11 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinEmbedToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Embed',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,11 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinFrameToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Frame',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,11 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinGroupToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Group',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,11 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinImageToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Image',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,84 @@
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
import { BlockFlavourIdentifier } from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import { builtinAttachmentToolbarConfig } from './attachment';
import { builtinBookmarkToolbarConfig } from './bookmark';
import { builtinBrushToolbarConfig } from './brush';
import { builtinConnectorToolbarConfig } from './connector';
import { builtinEmbedToolbarConfig } from './embed';
import { builtinFrameToolbarConfig } from './frame';
import { builtinGroupToolbarConfig } from './group';
import { builtinImageToolbarConfig } from './image';
import { builtinMindmapToolbarConfig } from './mindmap';
import { builtinMiscToolbarConfig } from './misc';
import { builtinNoteToolbarConfig } from './note';
import { builtinShapeToolbarConfig } from './shape';
import { builtinTextToolbarConfig } from './text';
export const EdgelessElementToolbarExtension: ExtensionType[] = [
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:attachment'),
config: builtinAttachmentToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:bookmark'),
config: builtinBookmarkToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:image'),
config: builtinImageToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:brush'),
config: builtinBrushToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:connector'),
config: builtinConnectorToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:embed'),
config: builtinEmbedToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:frame'),
config: builtinFrameToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:group'),
config: builtinGroupToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:mindmap'),
config: builtinMindmapToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:note'),
config: builtinNoteToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:shape'),
config: builtinShapeToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:text'),
config: builtinTextToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:*'),
config: builtinMiscToolbarConfig,
}),
];

View File

@@ -0,0 +1,11 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinMindmapToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Mindmap',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,41 @@
import {
ActionPlacement,
type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import {
ConnectorCIcon,
LockIcon,
ReleaseFromGroupIcon,
} from '@blocksuite/icons/lit';
export const builtinMiscToolbarConfig = {
actions: [
{
placement: ActionPlacement.Start,
id: 'a.release-from-group',
tooltip: 'Release from group',
icon: ReleaseFromGroupIcon(),
run() {},
},
{
placement: ActionPlacement.Start,
id: 'a.misc',
label: 'Misc',
run() {},
},
{
placement: ActionPlacement.End,
id: 'a.draw-connector',
icon: ConnectorCIcon(),
tooltip: 'Draw connector',
run() {},
},
{
placement: ActionPlacement.End,
id: 'b.lock',
icon: LockIcon(),
tooltip: 'Lock',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,11 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinNoteToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Note',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,39 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
import {
AddTextIcon,
ShapeIcon,
StyleGeneralIcon,
} from '@blocksuite/icons/lit';
export const builtinShapeToolbarConfig = {
actions: [
{
id: 'a.switch-type',
icon: ShapeIcon(),
tooltip: 'Switch type',
run() {},
},
{
id: 'b.style',
icon: StyleGeneralIcon(),
tooltip: 'Style',
run() {},
},
{
id: 'c.fill-color',
label: 'Fill color',
run() {},
},
{
id: 'd.border-style',
label: 'Border style',
run() {},
},
{
id: 'e.text',
icon: AddTextIcon(),
tooltip: 'Show add button or text menu',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -0,0 +1,11 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinTextToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Text',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -3,6 +3,7 @@ import { ConnectionOverlay } from '@blocksuite/affine-block-surface';
import { TextTool } from '@blocksuite/affine-gfx-text';
import type { ExtensionType } from '@blocksuite/store';
import { EdgelessElementToolbarExtension } from './configs/toolbar';
import { EdgelessRootBlockSpec } from './edgeless-root-spec.js';
import { BrushTool } from './gfx-tool/brush-tool.js';
import { ConnectorTool } from './gfx-tool/connector-tool.js';
@@ -40,7 +41,8 @@ export const EdgelessBuiltInManager: ExtensionType[] = [
MindMapIndicatorOverlay,
SnapManager,
EditPropsMiddlewareBuilder,
];
EdgelessElementToolbarExtension,
].flat();
export const EdgelessBuiltInSpecs: ExtensionType[] = [
EdgelessRootBlockSpec,

View File

@@ -27,13 +27,6 @@ export function mountShapeTextEditor(
);
}
if (!shapeElement.text) {
const text = new Y.Text();
edgeless.std
.get(EdgelessCRUDIdentifier)
.updateElement(shapeElement.id, { text });
}
const updatedElement = edgeless.service.crud.getElementById(shapeElement.id);
if (!(updatedElement instanceof ShapeElementModel)) {
@@ -41,17 +34,25 @@ export function mountShapeTextEditor(
return;
}
edgeless.gfx.tool.setTool('default');
edgeless.gfx.selection.set({
elements: [shapeElement.id],
editing: true,
});
if (!shapeElement.text) {
const text = new Y.Text();
edgeless.std
.get(EdgelessCRUDIdentifier)
.updateElement(shapeElement.id, { text });
}
const shapeEditor = new EdgelessShapeTextEditor();
shapeEditor.element = updatedElement;
shapeEditor.edgeless = edgeless;
shapeEditor.mountEditor = mountShapeTextEditor;
edgeless.mountElm.append(shapeEditor);
edgeless.gfx.tool.setTool('default');
edgeless.gfx.selection.set({
elements: [shapeElement.id],
editing: true,
});
}
export function mountFrameTitleEditor(
@@ -65,16 +66,17 @@ export function mountFrameTitleEditor(
);
}
const frameEditor = new EdgelessFrameTitleEditor();
frameEditor.frameModel = frame;
frameEditor.edgeless = edgeless;
edgeless.mountElm.append(frameEditor);
edgeless.gfx.tool.setTool('default');
edgeless.gfx.selection.set({
elements: [frame.id],
editing: true,
});
const frameEditor = new EdgelessFrameTitleEditor();
frameEditor.frameModel = frame;
frameEditor.edgeless = edgeless;
edgeless.mountElm.append(frameEditor);
}
export function mountGroupTitleEditor(
@@ -88,16 +90,17 @@ export function mountGroupTitleEditor(
);
}
const groupEditor = new EdgelessGroupTitleEditor();
groupEditor.group = group;
groupEditor.edgeless = edgeless;
edgeless.mountElm.append(groupEditor);
edgeless.gfx.tool.setTool('default');
edgeless.gfx.selection.set({
elements: [group.id],
editing: true,
});
const groupEditor = new EdgelessGroupTitleEditor();
groupEditor.group = group;
groupEditor.edgeless = edgeless;
edgeless.mountElm.append(groupEditor);
}
export function mountConnectorLabelEditor(
@@ -112,6 +115,12 @@ export function mountConnectorLabelEditor(
);
}
edgeless.gfx.tool.setTool('default');
edgeless.gfx.selection.set({
elements: [connector.id],
editing: true,
});
if (!connector.text) {
const text = new Y.Text();
const labelOffset = connector.labelOffset;
@@ -143,9 +152,4 @@ export function mountConnectorLabelEditor(
editor.inlineEditor?.focusEnd();
})
.catch(console.error);
edgeless.gfx.tool.setTool('default');
edgeless.gfx.selection.set({
elements: [connector.id],
editing: true,
});
}

View File

@@ -35,7 +35,9 @@ export class EditorToolbar extends WithDisposable(LitElement) {
e.stopPropagation();
e.preventDefault();
});
this._disposables.addFromEvent(this, 'wheel', stopPropagation);
this._disposables.addFromEvent(this, 'wheel', stopPropagation, {
passive: false,
});
}
override render() {

View File

@@ -4,6 +4,7 @@ import type { ToolbarContext } from './context';
export enum ActionPlacement {
Start = 0,
Normal = 1 << 0,
End = 1 << 1,
More = 1 << 2,
}

View File

@@ -2,6 +2,7 @@ import {
type BlockComponent,
BlockSelection,
type BlockStdScope,
SurfaceSelection,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { nextTick } from '@blocksuite/global/utils';
@@ -61,7 +62,8 @@ abstract class ToolbarContextBase {
if (this.flags.accept()) return true;
if (this.host.event.active) return true;
// Selects `embed-synced-doc-block`
return this.host.contains(document.activeElement);
if (this.host.contains(document.activeElement)) return true;
return this.isEdgelessMode;
}
get readonly() {
@@ -108,6 +110,26 @@ abstract class ToolbarContextBase {
return this.toolbarRegistry.message$;
}
getCurrentElement() {
const selection = this.selection.find(SurfaceSelection);
return selection?.elements.length
? this.gfx.getElementById(selection.elements[0])
: null;
}
getCurrentBlock(): Block | null {
return this.getCurrentBlockBy();
}
getCurrentBlockComponent(): BlockComponent | null {
const block = this.getCurrentBlock();
return block && this.view.getBlock(block.id);
}
getCurrentModel() {
return this.getCurrentBlock()?.model ?? null;
}
getCurrentBlockBy<T extends SelectionConstructor>(type?: T): Block | null {
const selection = this.selection.find(type ?? BlockSelection);
return (selection && this.store.getBlock(selection.blockId)) ?? null;
@@ -125,11 +147,10 @@ abstract class ToolbarContextBase {
return matchModels(model, [klass]) ? model : null;
}
getCurrentBlockComponentBy<
T extends SelectionConstructor,
K extends abstract new (...args: any) => any,
>(type: T, klass: K): InstanceType<K> | null {
const block = this.getCurrentBlockBy<T>(type);
getCurrentBlockComponentBy<K extends abstract new (...args: any) => any>(
klass: K
): InstanceType<K> | null {
const block = this.getCurrentBlockBy();
const component = block && this.view.getBlock(block.id);
return this.blockComponentIs(component, klass) ? component : null;
}

View File

@@ -62,6 +62,10 @@ export class Flags {
});
}
isSurface() {
return this.check(Flag.Surface);
}
isText() {
return this.check(Flag.Text);
}
@@ -74,6 +78,10 @@ export class Flags {
return this.check(Flag.Native);
}
isHovering() {
return this.check(Flag.Hovering);
}
accept() {
return this.check(Flag.Accepting);
}

View File

@@ -7,10 +7,6 @@ import {
ListBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import {
getBlockSelectionsCommand,
getSelectedBlocksCommand,
} from '@blocksuite/affine-shared/commands';
import {
ToolbarContext,
ToolbarFlag as Flag,
@@ -18,20 +14,31 @@ import {
} from '@blocksuite/affine-shared/services';
import { matchModels } from '@blocksuite/affine-shared/utils';
import {
type BlockComponent,
BlockSelection,
SurfaceSelection,
TextSelection,
WidgetComponent,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, getCommonBound } from '@blocksuite/global/gfx';
import {
GfxBlockElementModel,
type GfxController,
type GfxModel,
GfxPrimitiveElementModel,
} from '@blocksuite/block-std/gfx';
import {
Bound,
getCommonBound,
getCommonBoundWithRotation,
} from '@blocksuite/global/gfx';
import { nextTick } from '@blocksuite/global/utils';
import type { Placement, ReferenceElement } from '@floating-ui/dom';
import type { Placement, ReferenceElement, SideObject } from '@floating-ui/dom';
import { batch, effect, signal } from '@preact/signals-core';
import { css } from 'lit';
import groupBy from 'lodash-es/groupBy';
import throttle from 'lodash-es/throttle';
import toPairs from 'lodash-es/toPairs';
import { autoUpdatePosition, renderToolbar } from './utils';
import { autoUpdatePosition, renderToolbar, sideMap } from './utils';
export const AFFINE_TOOLBAR_WIDGET = 'affine-toolbar-widget';
@@ -47,7 +54,7 @@ export class AffineToolbarWidget extends WidgetComponent {
backface-visibility: hidden;
z-index: var(--affine-z-index-popover);
will-change: opacity, transform;
will-change: opacity, overlay, display, transform;
transition-property: opacity, overlay, display;
transition-duration: 120ms;
transition-timing-function: ease-out;
@@ -65,10 +72,63 @@ export class AffineToolbarWidget extends WidgetComponent {
}
`;
range$ = signal<Range | null>(null);
flavour$ = signal('affine:note');
placement$ = signal<Placement>('top');
sideOptions$ = signal<Partial<SideObject> | null>(null);
referenceElement$ = signal<(() => ReferenceElement | null) | null>(null);
setReferenceElementWithRange(range: Range | null) {
this.referenceElement$.value = range
? () => ({
getBoundingClientRect: () => range.getBoundingClientRect(),
getClientRects: () =>
Array.from(range.getClientRects()).filter(rect =>
Math.round(rect.width)
),
})
: null;
}
setReferenceElementWithHtmlElement(element: Element | null) {
this.referenceElement$.value = element ? () => element : null;
}
setReferenceElementWithBlocks(blocks: BlockComponent[]) {
const getClientRects = () => blocks.map(e => e.getBoundingClientRect());
this.referenceElement$.value = blocks.length
? () => ({
getBoundingClientRect: () => {
const rects = getClientRects();
const bounds = getCommonBound(rects.map(Bound.fromDOMRect));
if (!bounds) return rects[0];
return new DOMRect(bounds.x, bounds.y, bounds.w, bounds.h);
},
getClientRects,
})
: null;
}
setReferenceElementWithElements(gfx: GfxController, elements: GfxModel[]) {
const getBoundingClientRect = () => {
const bounds = getCommonBoundWithRotation(elements);
const { x: offsetX, y: offsetY } = this.getBoundingClientRect();
const [x, y, w, h] = gfx.viewport.toViewBound(bounds).toXYWH();
const rect = new DOMRect(x + offsetX, y + offsetY, w, h);
return rect;
};
this.referenceElement$.value = elements.length
? () => ({
getBoundingClientRect,
getClientRects: () => [getBoundingClientRect()],
})
: null;
}
toolbar = new EditorToolbar();
get toolbarRegistry() {
@@ -80,7 +140,9 @@ export class AffineToolbarWidget extends WidgetComponent {
const {
flavour$,
range$,
placement$,
sideOptions$,
referenceElement$,
disposables,
toolbar,
toolbarRegistry,
@@ -98,22 +160,25 @@ export class AffineToolbarWidget extends WidgetComponent {
// Selects text in note.
disposables.add(
std.selection.find$(TextSelection).subscribe(result => {
const activated =
const range = std.range.value ?? null;
const activated = Boolean(
context.activated &&
Boolean(
range &&
result &&
!result.isCollapsed() &&
result.from.length + (result.to?.length ?? 0)
);
!result.isCollapsed() &&
result.from.length + (result.to?.length ?? 0)
);
batch(() => {
flags.toggle(Flag.Text, activated);
if (!activated) return;
const range = std.range.value ?? null;
range$.value = activated ? range : null;
this.setReferenceElementWithRange(range);
sideOptions$.value = null;
flavour$.value = 'affine:note';
placement$.value = 'top';
flags.refresh(Flag.Text);
});
})
@@ -124,54 +189,68 @@ export class AffineToolbarWidget extends WidgetComponent {
disposables.addFromEvent(document, 'selectionchange', () => {
const range = std.range.value ?? null;
let activated = context.activated && Boolean(range && !range.collapsed);
let isNative = false;
if (activated) {
const result = std.selection.find(DatabaseSelection);
const viewSelection = result?.viewSelection;
activated = Boolean(
viewSelection &&
((viewSelection.selectionType === 'area' &&
if (viewSelection) {
isNative =
(viewSelection.selectionType === 'area' &&
viewSelection.isEditing) ||
(viewSelection.selectionType === 'cell' &&
viewSelection.isEditing))
);
(viewSelection.selectionType === 'cell' && viewSelection.isEditing);
}
if (!activated) {
if (!isNative) {
const result = std.selection.find(TableSelection);
const viewSelection = result?.data;
activated = Boolean(viewSelection && viewSelection.type === 'area');
if (viewSelection) {
isNative = viewSelection.type === 'area';
}
}
}
batch(() => {
activated &&= isNative;
// Focues outside: `doc-title`
if (
flags.check(Flag.Text) &&
!std.host.contains(range?.commonAncestorContainer ?? null)
) {
flags.toggle(Flag.Text, false);
}
flags.toggle(Flag.Native, activated);
if (!activated) return;
range$.value = activated ? range : null;
flavour$.value = 'affine:note';
this.setReferenceElementWithRange(range);
sideOptions$.value = null;
flavour$.value = 'affine:note';
placement$.value = 'top';
flags.refresh(Flag.Native);
});
});
// Selects blocks in note.
disposables.add(
std.selection.filter$(BlockSelection).subscribe(result => {
const count = result.length;
std.selection.filter$(BlockSelection).subscribe(selections => {
const blockIds = selections.map(s => s.blockId);
const count = blockIds.length;
let flavour = 'affine:note';
let activated = context.activated && Boolean(count);
if (activated) {
// Handles a signal block.
const block = count === 1 && std.store.getBlock(result[0].blockId);
const block = count === 1 && std.store.getBlock(blockIds[0]);
// Chencks if block's config exists.
if (block) {
const modelFlavour = block.model.flavour;
const existed =
toolbarRegistry.modules.has(modelFlavour) ||
toolbarRegistry.modules.has(modelFlavour) ??
toolbarRegistry.modules.has(`custom:${modelFlavour}`);
if (existed) {
flavour = modelFlavour;
@@ -187,12 +266,19 @@ export class AffineToolbarWidget extends WidgetComponent {
}
batch(() => {
flavour$.value = flavour;
flags.toggle(Flag.Block, activated);
if (!activated) return;
this.setReferenceElementWithBlocks(
blockIds
.map(id => std.view.getBlock(id))
.filter(block => block !== null)
);
sideOptions$.value = null;
flavour$.value = flavour;
placement$.value = flavour === 'affine:note' ? 'top' : 'top-start';
flags.refresh(Flag.Block);
});
})
@@ -201,12 +287,78 @@ export class AffineToolbarWidget extends WidgetComponent {
// Selects elements in edgeless.
// Triggered only when not in editing state.
disposables.add(
std.selection.filter$(SurfaceSelection).subscribe(result => {
context.gfx.selection.slots.updated.subscribe(selections => {
// TODO(@fundon): should remove it when edgeless element toolbar is removed
if (context.isEdgelessMode) return;
const elementIds = selections
.map(s => (s.editing || s.inoperable ? [] : s.elements))
.flat();
const count = elementIds.length;
const gfx = context.gfx;
const surface = gfx.surface;
const activated =
context.activated &&
Boolean(result.length) &&
!result.some(e => e.editing);
flags.toggle(Flag.Surface, activated);
context.activated && Boolean(surface) && Boolean(count);
let flavour = 'affine:surface';
let elements: GfxModel[] = [];
let hasLocked = false;
let sideOptions = null;
if (activated && surface) {
elements = elementIds
.map(id => gfx.getElementById(id))
.filter(model => model !== null) as GfxModel[];
hasLocked = elements.some(e => e.isLocked());
const grouped = groupBy(
elements.map(model => {
let flavour = surface.flavour;
if (model instanceof GfxBlockElementModel) {
flavour += `:${model.flavour.split(':').pop()}`;
} else if (model instanceof GfxPrimitiveElementModel) {
flavour += `:${model.type}`;
}
return { model, flavour };
}),
e => e.flavour
);
const paired = toPairs(grouped);
if (paired.length === 1) {
flavour = paired[0][0];
if (
flavour === 'affine:surface:shape' &&
paired[0][1].length === 1
) {
sideOptions = sideMap.get(flavour) ?? null;
}
}
if (!sideOptions) {
const flavours = new Set(paired.map(([f]) => f));
if (flavours.has('affine:surface:frame')) {
sideOptions = sideMap.get('affine:surface:frame') ?? null;
} else if (flavours.has('affine:surface:group')) {
sideOptions = sideMap.get('affine:surface:group') ?? null;
}
}
}
batch(() => {
flags.toggle(Flag.Surface, activated);
if (!activated || !flavour) return;
this.setReferenceElementWithElements(gfx, elements);
sideOptions$.value = sideOptions;
flavour$.value = flavour;
placement$.value = hasLocked ? 'top' : 'top-start';
flags.refresh(Flag.Surface);
});
})
);
@@ -223,7 +375,8 @@ export class AffineToolbarWidget extends WidgetComponent {
if (!hasTextSelection) return;
const range = std.range.value ?? null;
range$.value = range && !range.collapsed ? range : null;
this.setReferenceElementWithRange(range);
// TODO(@fundon): maybe here can be further optimized
// 1. Prevents flickering effects.
@@ -236,21 +389,27 @@ export class AffineToolbarWidget extends WidgetComponent {
);
// TODO(@fundon): improve these cases
// When switch the view mode, wait until the view is created
// Waits until the view is created when switching the view mode.
// `card view` or `embed view`
disposables.add(
std.view.viewUpdated.subscribe(record => {
if (
record.type === 'block' &&
flags.isBlock() &&
std.selection
.filter$(BlockSelection)
.peek()
.find(s => s.blockId === record.id)
) {
if (record.method === 'add') {
if (record.type !== 'block') return;
if (!flags.isBlock()) return;
const blockIds = std.selection
.filter$(BlockSelection)
.peek()
.map(s => s.blockId);
if (record.method === 'add' && blockIds.includes(record.id)) {
batch(() => {
this.setReferenceElementWithBlocks(
blockIds
.map(id => std.view.getBlock(id))
.filter(block => block !== null)
);
flags.refresh(Flag.Block);
}
});
return;
}
})
@@ -302,9 +461,9 @@ export class AffineToolbarWidget extends WidgetComponent {
eventOptions
);
// Handles hover elements
// Handles element when hovering
disposables.add(
toolbarRegistry.message$.subscribe(data => {
message$.subscribe(data => {
if (
!context.activated ||
flags.contains(Flag.Text | Flag.Native | Flag.Block)
@@ -324,130 +483,85 @@ export class AffineToolbarWidget extends WidgetComponent {
setFloating(toolbar);
flavour$.value = flavour;
this.setReferenceElementWithHtmlElement(data.element);
sideOptions$.value = null;
flavour$.value = flavour;
placement$.value = 'top';
flags.refresh(Flag.Hovering);
});
})
);
// Should update position of notes' toolbar in edgeless
disposables.add(
this.std
.get(GfxControllerIdentifier)
.viewport.viewportUpdated.subscribe(() => {
if (!context.activated) return;
if (flags.value === Flag.None || flags.check(Flag.Hiding)) {
return;
}
if (flags.isText()) {
flags.refresh(Flag.Text);
return;
}
if (flags.isNative()) {
flags.refresh(Flag.Native);
return;
}
if (flags.isBlock()) {
flags.refresh(Flag.Block);
return;
}
})
);
disposables.add(
flags.value$.subscribe(value => {
// Hides toolbar
if (value === Flag.None || flags.check(Flag.Hiding, value)) {
delete toolbar.dataset.open;
return;
}
// Shows toolbar
// 1. `Flag.Text`: formatting in note
// 2. `Flag.Native`: formating in database
// 3. `Flag.Block`: blocks in note
// 4. `Flag.Hovering`: inline links in note/database
if (
flags.contains(
Flag.Hovering | Flag.Text | Flag.Native | Flag.Block,
value
)
) {
renderToolbar(toolbar, context, flavour$.peek());
toolbar.dataset.open = 'true';
return;
}
// Shows toolbar in edgeles
// TODO(@fundon): handles edgeless toolbar
})
);
disposables.add(
effect(() => {
const value = flags.value$.value;
// Hides toolbar
if (value === Flag.None || flags.check(Flag.Hiding, value)) {
if (toolbar.dataset.open) delete toolbar.dataset.open;
return;
}
const flavour = flavour$.value;
if (!context.activated || flags.contains(Flag.Hiding, value)) return;
// Shows toolbar
// 1. `Flag.Text`: formatting in note
// 2. `Flag.Native`: formating in database/table
// 3. `Flag.Block`: blocks in note
// 4. `Flag.Hovering`: inline links in note/database/table
// 5. `Flag.Surface`: elements in edgeless
renderToolbar(toolbar, context, flavour);
if (toolbar.dataset.open) return;
toolbar.dataset.open = 'true';
})
);
let abortController = new AbortController();
disposables.add(
effect(() => {
if (!abortController.signal.aborted) {
abortController.abort();
}
const value = flags.value$.value;
if (
!flags.contains(
Flag.Hovering | Flag.Text | Flag.Native | Flag.Block,
value
)
!context.activated ||
Flag.None === value ||
flags.contains(Flag.Hiding, value)
)
return;
// TODO(@fundon): improves here
const isNote = flavour === 'affine:note';
let placement = isNote ? ('top' as Placement) : undefined;
let virtualEl: ReferenceElement | null = null;
const build = referenceElement$.value;
const referenceElement = build?.();
if (!referenceElement) return;
if (flags.check(Flag.Hovering, value)) {
const message = message$.value;
if (!message) return;
const flavour = flavour$.value;
const placement = placement$.value;
const sideOptions = sideOptions$.value;
const { element } = message;
virtualEl = element;
placement = 'top';
} else if (flags.check(Flag.Block, value)) {
const [ok, { selectedBlocks }] = context.chain
.pipe(getBlockSelectionsCommand)
.pipe(getSelectedBlocksCommand, { types: ['block'] })
.run();
if (!ok || !selectedBlocks?.length) return;
virtualEl = {
getBoundingClientRect: () => {
const rects = selectedBlocks.map(e => e.getBoundingClientRect());
const bounds = getCommonBound(rects.map(Bound.fromDOMRect));
if (!bounds) return rects[0];
return new DOMRect(bounds.x, bounds.y, bounds.w, bounds.h);
},
getClientRects: () =>
selectedBlocks.map(e => e.getBoundingClientRect()),
};
} else {
const range = range$.value;
if (!range) return;
virtualEl = {
getBoundingClientRect: () => range.getBoundingClientRect(),
getClientRects: () =>
Array.from(range.getClientRects()).filter(rect =>
Math.round(rect.width)
),
};
if (abortController.signal.aborted) {
abortController = new AbortController();
}
const signal = abortController.signal;
if (!virtualEl) return;
const cleanup = autoUpdatePosition(
signal,
toolbar,
referenceElement,
flavour,
placement,
sideOptions
);
return autoUpdatePosition(virtualEl, toolbar, placement);
signal.addEventListener('abort', cleanup, { once: true });
return () => {
if (signal.aborted) return;
abortController.abort();
};
})
);
}

View File

@@ -7,15 +7,14 @@ import {
type ToolbarAction,
type ToolbarActions,
type ToolbarContext,
type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { BlockSelection } from '@blocksuite/block-std';
import { nextTick } from '@blocksuite/global/utils';
import { MoreVerticalIcon } from '@blocksuite/icons/lit';
import type {
AutoUpdateOptions,
Placement,
ReferenceElement,
SideObject,
} from '@floating-ui/dom';
import {
autoUpdate,
@@ -38,54 +37,83 @@ import orderBy from 'lodash-es/orderBy';
import partition from 'lodash-es/partition';
import toPairs from 'lodash-es/toPairs';
export const sideMap = new Map([
// includes frame element
['affine:surface:frame', { top: 28 }],
// includes group element
['affine:surface:group', { top: 20 }],
// only one shape element
['affine:surface:shape', { top: 26, bottom: -26 }],
]);
export function autoUpdatePosition(
referenceElement: ReferenceElement,
signal: AbortSignal,
toolbar: EditorToolbar,
placement: Placement = 'top-start',
referenceElement: ReferenceElement,
flavour: string,
placement: Placement,
sideOptions: Partial<SideObject> | null,
options: AutoUpdateOptions = { elementResize: false, animationFrame: true }
) {
const abortController = new AbortController();
const signal = abortController.signal;
const isInline = flavour === 'affine:note';
const hasSurfaceScope = flavour.includes('surface');
const offsetTop = sideOptions?.top ?? 0;
const offsetBottom = sideOptions?.bottom ?? 0;
const offsetY = offsetTop + (hasSurfaceScope ? 2 : 0);
const config = {
placement,
middleware: [
offset(10 + offsetY),
isInline ? inline() : undefined,
shift(state => ({
padding: {
top: 10,
right: 10,
bottom: 150,
left: 10,
},
crossAxis: state.placement.includes('bottom'),
limiter: limitShift(),
})),
flip({ padding: 10 }),
hide(),
],
};
const update = async () => {
await Promise.race([
new Promise(resolve => {
const listener = () => resolve(signal.reason);
signal.addEventListener('abort', listener, { once: true });
signal.addEventListener('abort', () => resolve(signal.reason), {
once: true,
});
if (signal.aborted) return;
signal.removeEventListener('abort', listener);
resolve(null);
}),
toolbar.updateComplete.then(nextTick),
isInline ? toolbar.updateComplete.then(nextTick) : toolbar.updateComplete,
]);
if (signal.aborted) return;
const { x, y } = await computePosition(referenceElement, toolbar, {
placement,
middleware: [
offset(10),
inline(),
shift(state => ({
padding: {
top: 10,
right: 10,
bottom: 150,
left: 10,
},
crossAxis: state.placement.includes('bottom'),
limiter: limitShift(),
})),
flip({ padding: 10 }),
hide(),
],
});
const result = await computePosition(referenceElement, toolbar, config);
const { x, middlewareData, placement: currentPlacement } = result;
const y =
result.y -
(currentPlacement.includes('top') ? 0 : offsetTop + offsetBottom);
toolbar.style.transform = `translate3d(${x}px, ${y}px, 0)`;
if (toolbar.dataset.open) {
if (middlewareData.hide?.referenceHidden) {
delete toolbar.dataset.open;
}
} else {
toolbar.dataset.open = 'true';
}
};
const cleanup = autoUpdate(
return autoUpdate(
referenceElement,
toolbar,
() => {
@@ -93,15 +121,37 @@ export function autoUpdatePosition(
},
options
);
return () => {
cleanup();
if (signal.aborted) return;
abortController.abort();
};
}
function group(actions: ToolbarAction[]) {
export function combine(actions: ToolbarActions, context: ToolbarContext) {
const grouped = group(actions);
const generated = grouped.map(action => {
const newAction = {
...action,
placement: action.placement ?? ActionPlacement.Normal,
};
if ('generate' in action && typeof action.generate === 'function') {
// TODO(@fundon): should delete `generate` fn
return {
...newAction,
...action.generate(context),
};
}
return newAction;
});
const filtered = generated.filter(action => {
if (typeof action.when === 'function') return action.when(context);
return action.when ?? true;
});
return filtered;
}
function group(actions: ToolbarAction[]): ToolbarAction[] {
const grouped = groupBy(actions, a => a.id);
const paired = toPairs(grouped).map(([_, items]) => {
@@ -114,28 +164,6 @@ function group(actions: ToolbarAction[]) {
return paired;
}
export function combine(actions: ToolbarActions, context: ToolbarContext) {
const grouped = group(actions);
const generated = grouped.map(action => {
if ('generate' in action && action.generate) {
// TODO(@fundon): should delete `generate` fn
return {
...action,
...action.generate(context),
};
}
return action;
});
const filtered = generated.filter(action => {
if (typeof action.when === 'function') return action.when(context);
return action.when ?? true;
});
return filtered;
}
const merge = (a: any, b: any) =>
mergeWith(a, b, (obj, src) =>
Array.isArray(obj) ? group(obj.concat(src)) : src
@@ -155,27 +183,23 @@ export function renderToolbar(
context: ToolbarContext,
flavour: string
) {
const hasSurfaceScope = flavour.includes('surface');
const toolbarRegistry = context.toolbarRegistry;
const module = toolbarRegistry.modules.get(flavour);
if (!module) return;
const customModule = toolbarRegistry.modules.get(`custom:${flavour}`);
const customWildcardModule = toolbarRegistry.modules.get(`custom:affine:*`);
const config = module.config satisfies ToolbarModuleConfig;
const customConfig = (customModule?.config ?? {
actions: [],
}) satisfies ToolbarModuleConfig;
const customWildcardConfig = (customWildcardModule?.config ?? {
actions: [],
}) satisfies ToolbarModuleConfig;
const combined = combine(
[
...config.actions,
...customConfig.actions,
...customWildcardConfig.actions,
],
context
);
const actions = [
flavour,
`custom:${flavour}`,
hasSurfaceScope ? ['affine:surface:*', 'custom:affine:surface:*'] : [],
'affine:*',
'custom:affine:*',
]
.flat()
.map(key => toolbarRegistry.modules.get(key))
.filter(module => !!module)
.map<ToolbarActions>(module => module.config.actions)
.flat();
const combined = combine(actions, context);
const ordered = orderBy(
combined,
@@ -194,40 +218,36 @@ export function renderToolbar(
context,
renderMenuActionItem
);
if (moreMenuItems.length) {
// TODO(@fundon): edgeless case needs to be considered
const key = `${flavour}:${context.getCurrentModelBy(BlockSelection)?.id}`;
// if (moreMenuItems.length) {
// TODO(@fundon): edgeless case needs to be considered
const key = `${context.getCurrentModel()?.id ?? context.getCurrentElement()?.id}`;
primaryActionGroup.push({
id: 'more',
content: html`${keyed(
key,
html`
<editor-menu-button
class="more-menu"
.contentPadding="${'8px'}"
.button=${html`
<editor-icon-button aria-label="More" .tooltip="${'More'}">
${MoreVerticalIcon()}
</editor-icon-button>
`}
>
<div data-size="large" data-orientation="vertical">
${join(moreMenuItems, () =>
renderToolbarSeparator('horizontal')
)}
</div>
</editor-menu-button>
`
)}`,
});
}
primaryActionGroup.push({
id: 'more',
content: html`${keyed(
`${flavour}:${key}`,
html`
<editor-menu-button
class="more-menu"
.contentPadding="${'8px'}"
.button=${html`
<editor-icon-button aria-label="More" .tooltip="${'More'}">
${MoreVerticalIcon()}
</editor-icon-button>
`}
>
<div data-size="large" data-orientation="vertical">
${join(moreMenuItems, renderToolbarSeparator('horizontal'))}
</div>
</editor-menu-button>
`
)}`,
});
// }
}
render(
join(renderActions(primaryActionGroup, context), () =>
renderToolbarSeparator()
),
join(renderActions(primaryActionGroup, context), renderToolbarSeparator()),
toolbar
);
}