refactor(editor): use extension to register edgeless toolbar button (#11062)

This commit is contained in:
Saul-Mirone
2025-03-21 11:45:31 +00:00
parent 5dc6fabdaf
commit 51d89edb02
8 changed files with 201 additions and 167 deletions

View File

@@ -1,10 +1,11 @@
import type { MenuConfig } from '@blocksuite/affine-components/context-menu';
import type { EdgelessRootBlockComponent } from '../../../edgeless-root-block.js';
import type { BlockComponent } from '@blocksuite/block-std';
import type { GfxController } from '@blocksuite/block-std/gfx';
/**
* Helper function to build a menu configuration for a tool in dense mode
*/
export type DenseMenuBuilder = (
edgeless: EdgelessRootBlockComponent
edgeless: BlockComponent,
gfx: GfxController
) => MenuConfig;

View File

@@ -9,16 +9,16 @@ import {
import type { DenseMenuBuilder } from '../common/type.js';
export const buildConnectorDenseMenu: DenseMenuBuilder = edgeless => {
export const buildConnectorDenseMenu: DenseMenuBuilder = (edgeless, gfx) => {
const prevMode =
edgeless.std.get(EditPropsStore).lastProps$.value.connector.mode;
const isSelected = edgeless.gfx.tool.currentToolName$.peek() === 'connector';
const isSelected = gfx.tool.currentToolName$.peek() === 'connector';
const createSelect =
(mode: ConnectorMode, record = true) =>
() => {
edgeless.gfx.tool.setTool('connector', {
gfx.tool.setTool('connector', {
mode,
});
record &&

View File

@@ -1,4 +1,5 @@
/* oxlint-disable @typescript-eslint/no-non-null-assertion */
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import {
type MenuHandler,
popMenu,
@@ -31,7 +32,6 @@ import { cache } from 'lit/directives/cache.js';
import debounce from 'lodash-es/debounce';
import { Subject } from 'rxjs';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
import type { MenuPopper } from './common/create-popper.js';
import {
edgelessToolbarContext,
@@ -39,7 +39,10 @@ import {
edgelessToolbarSlotsContext,
edgelessToolbarThemeContext,
} from './context.js';
import { getQuickTools, getSeniorTools } from './tools.js';
import {
QuickToolIdentifier,
SeniorToolIdentifier,
} from './extension/index.js';
const TOOLBAR_PADDING_X = 12;
const TOOLBAR_HEIGHT = 64;
@@ -54,10 +57,7 @@ const DIVIDER_SPACE = 8;
const SAFE_AREA_WIDTH = 64;
export const EDGELESS_TOOLBAR_WIDGET = 'edgeless-toolbar-widget';
export class EdgelessToolbarWidget extends WidgetComponent<
RootBlockModel,
EdgelessRootBlockComponent
> {
export class EdgelessToolbarWidget extends WidgetComponent<RootBlockModel> {
static override styles = css`
:host {
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
@@ -335,10 +335,19 @@ export class EdgelessToolbarWidget extends WidgetComponent<
}
private get _quickTools() {
if (!this.block) {
const block = this.block;
if (!block) {
return [];
}
return getQuickTools({ edgeless: this.block });
const quickTools = Array.from(
this.std.provider.getAll(QuickToolIdentifier).values()
);
const gfx = this.std.get(GfxControllerIdentifier);
return quickTools
.map(tool =>
tool({ block, gfx, toolbarContainer: this.toolbarContainer })
)
.filter(({ enable = true }) => enable);
}
private get _quickToolsWidthTotal() {
@@ -379,13 +388,19 @@ export class EdgelessToolbarWidget extends WidgetComponent<
}
private get _seniorTools() {
if (!this.block) {
const block = this.block;
if (!block) {
return [];
}
return getSeniorTools({
edgeless: this.block,
toolbarContainer: this.toolbarContainer,
});
const seniorTools = Array.from(
this.std.provider.getAll(SeniorToolIdentifier).values()
);
const gfx = this.std.get(GfxControllerIdentifier);
return seniorTools
.map(tool =>
tool({ block, gfx, toolbarContainer: this.toolbarContainer })
)
.filter(({ enable = true }) => enable);
}
private get _seniorToolsWidthTotal() {
@@ -612,27 +627,28 @@ export class EdgelessToolbarWidget extends WidgetComponent<
override firstUpdated() {
const { _disposables, block, gfx } = this;
if (!block) {
return;
}
if (!block) return;
const slots = this.std.get(EdgelessLegacySlotIdentifier);
const editPropsStore = this.std.get(EditPropsStore);
_disposables.add(
gfx.viewport.viewportUpdated.subscribe(() => this.requestUpdate())
);
_disposables.add(
block.slots.readonlyUpdated.subscribe(() => {
slots.readonlyUpdated.subscribe(() => {
this.requestUpdate();
})
);
_disposables.add(
block.slots.toolbarLocked.subscribe(disabled => {
slots.toolbarLocked.subscribe(disabled => {
this.toggleAttribute('disabled', disabled);
})
);
// This state from `editPropsStore` is not reactive,
// if the value is updated outside of this component, it will not be reflected.
_disposables.add(
this.std.get(EditPropsStore).slots.storageUpdated.subscribe(({ key }) => {
editPropsStore.slots.storageUpdated.subscribe(({ key }) => {
if (key === 'presentHideToolbar') {
this.requestUpdate();
}

View File

@@ -0,0 +1,60 @@
import type { MenuConfig } from '@blocksuite/affine-components/context-menu';
import type { BlockComponent } from '@blocksuite/block-std';
import type { GfxController, GfxToolsMap } from '@blocksuite/block-std/gfx';
import { createIdentifier } from '@blocksuite/global/di';
import type { ExtensionType } from '@blocksuite/store';
import { type TemplateResult } from 'lit';
export interface QuickTool {
type?: keyof GfxToolsMap;
enable?: boolean;
content: TemplateResult;
/**
* if not configured, the tool will not be shown in dense mode
*/
menu?: MenuConfig;
}
export interface SeniorTool {
/**
* Used to show in nav-button's tooltip
*/
name: string;
content: TemplateResult;
enable?: boolean;
}
export type ToolBuilder<T> = (options: {
block: BlockComponent;
gfx: GfxController;
toolbarContainer: HTMLElement;
}) => T;
export const QuickToolIdentifier = createIdentifier<ToolBuilder<QuickTool>>(
'edgeless-quick-tool'
);
export const SeniorToolIdentifier = createIdentifier<ToolBuilder<SeniorTool>>(
'edgeless-senior-tool'
);
export const QuickToolExtension = (
id: string,
builder: ToolBuilder<QuickTool>
): ExtensionType => {
return {
setup: di => {
di.addImpl(QuickToolIdentifier(id), () => builder);
},
};
};
export const SeniorToolExtension = (
id: string,
builder: ToolBuilder<SeniorTool>
): ExtensionType => {
return {
setup: di => {
di.addImpl(SeniorToolIdentifier(id), () => builder);
},
};
};

View File

@@ -1,27 +1,29 @@
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
import { menu } from '@blocksuite/affine-components/context-menu';
import { FrameIcon } from '@blocksuite/icons/lit';
import type { DenseMenuBuilder } from '../common/type.js';
import { FrameConfig } from './config.js';
export const buildFrameDenseMenu: DenseMenuBuilder = edgeless =>
export const buildFrameDenseMenu: DenseMenuBuilder = (edgeless, gfx) =>
menu.subMenu({
name: 'Frame',
prefix: FrameIcon({ width: '20px', height: '20px' }),
select: () => edgeless.gfx.tool.setTool({ type: 'frame' }),
isSelected: edgeless.gfx.tool.currentToolName$.peek() === 'frame',
select: () => gfx.tool.setTool({ type: 'frame' }),
isSelected: gfx.tool.currentToolName$.peek() === 'frame',
options: {
items: [
menu.action({
name: 'Custom',
select: () => edgeless.gfx.tool.setTool({ type: 'frame' }),
select: () => gfx.tool.setTool({ type: 'frame' }),
}),
...FrameConfig.map(config =>
menu.action({
name: `Slide ${config.name}`,
select: () => {
edgeless.gfx.tool.setTool('default');
edgeless.service.frame.createFrameOnViewportCenter(config.wh);
const frame = edgeless.std.get(EdgelessFrameManagerIdentifier);
gfx.tool.setTool('default');
frame.createFrameOnViewportCenter(config.wh);
},
})
),

View File

@@ -4,16 +4,16 @@ import { LassoMode } from '@blocksuite/affine-shared/types';
import type { DenseMenuBuilder } from '../common/type.js';
import { LassoFreeHandIcon, LassoPolygonalIcon } from './icons.js';
export const buildLassoDenseMenu: DenseMenuBuilder = edgeless => {
export const buildLassoDenseMenu: DenseMenuBuilder = (_, gfx) => {
// TODO: active state
// const prevMode =
// edgeless.service.editPropsStore.getLastProps('lasso').mode ??
// LassoMode.FreeHand;
const isActive = edgeless.gfx.tool.currentToolName$.peek() === 'lasso';
const isActive = gfx.tool.currentToolName$.peek() === 'lasso';
const createSelect = (mode: LassoMode) => () => {
edgeless.gfx.tool.setTool('lasso', { mode });
gfx.tool.setTool('lasso', { mode });
};
return menu.subMenu({

View File

@@ -1,164 +1,116 @@
import type { MenuConfig } from '@blocksuite/affine-components/context-menu';
import type { GfxToolsMap } from '@blocksuite/block-std/gfx';
import { html, type TemplateResult } from 'lit';
import { html } from 'lit';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
import { buildConnectorDenseMenu } from './connector/connector-dense-menu.js';
import { QuickToolExtension, SeniorToolExtension } from './extension/index.js';
import { buildFrameDenseMenu } from './frame/frame-dense-menu.js';
import { buildLinkDenseMenu } from './link/link-dense-menu.js';
export interface QuickTool {
type?: keyof GfxToolsMap;
content: TemplateResult;
/**
* if not configured, the tool will not be shown in dense mode
*/
menu?: MenuConfig;
}
export interface SeniorTool {
/**
* Used to show in nav-button's tooltip
*/
name: string;
content: TemplateResult;
}
/**
* Get quick-tool list
*/
export const getQuickTools = ({
edgeless,
}: {
edgeless: EdgelessRootBlockComponent;
}) => {
const { doc } = edgeless;
const quickTools: QuickTool[] = [];
// 🔧 Hands / Pointer
quickTools.push({
const defaultQuickTool = QuickToolExtension('default', ({ block }) => {
return {
type: 'default',
content: html`<edgeless-default-tool-button
.edgeless=${edgeless}
.edgeless=${block}
></edgeless-default-tool-button>`,
// menu: will never show because the first tool will never hide
});
};
});
// 🔧 Lasso
// if (doc.awarenessStore.getFlag('enable_lasso_tool')) {
// quickTools.push({
// type: 'lasso',
// content: html`<edgeless-lasso-tool-button
// .edgeless=${edgeless}
// ></edgeless-lasso-tool-button>`,
// menu: buildLassoDenseMenu(edgeless),
// });
// }
const frameQuickTool = QuickToolExtension('frame', ({ block, gfx }) => {
return {
type: 'frame',
content: html`<edgeless-frame-tool-button
.edgeless=${block}
></edgeless-frame-tool-button>`,
menu: buildFrameDenseMenu(block, gfx),
enable: !block.doc.readonly,
};
});
// 🔧 Frame
if (!doc.readonly) {
quickTools.push({
type: 'frame',
content: html`<edgeless-frame-tool-button
.edgeless=${edgeless}
></edgeless-frame-tool-button>`,
menu: buildFrameDenseMenu(edgeless),
});
}
// 🔧 Connector
quickTools.push({
const connectorQuickTool = QuickToolExtension('connector', ({ block }) => {
return {
type: 'connector',
content: html`<edgeless-connector-tool-button
.edgeless=${edgeless}
.edgeless=${block}
></edgeless-connector-tool-button>`,
menu: buildConnectorDenseMenu(edgeless),
});
};
});
// 🔧 Present
// quickTools.push({
// type: 'frameNavigator',
// content: html`<edgeless-present-button
// .edgeless=${edgeless}
// ></edgeless-present-button>`,
// });
// 🔧 Note
// if (!doc.readonly) {
// quickTools.push({
// type: 'affine:note',
// content: html`
// <edgeless-note-tool-button
// .edgeless=${edgeless}
// ></edgeless-note-tool-button>
// `,
// });
// }
// Link
quickTools.push({
const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => {
return {
content: html`<edgeless-link-tool-button
.edgeless=${edgeless}
.edgeless=${block}
></edgeless-link-tool-button>`,
menu: buildLinkDenseMenu(edgeless),
});
return quickTools;
};
menu: buildLinkDenseMenu(block, gfx),
};
});
export const getSeniorTools = ({
edgeless,
toolbarContainer,
}: {
edgeless: EdgelessRootBlockComponent;
toolbarContainer: HTMLElement;
}): SeniorTool[] => {
const { doc } = edgeless;
const tools: SeniorTool[] = [];
const noteSeniorTool = SeniorToolExtension('note', ({ block }) => {
return {
name: 'Note',
content: html`<edgeless-note-senior-button
.edgeless=${block}
></edgeless-note-senior-button>`,
};
});
if (!doc.readonly) {
tools.push({
name: 'Note',
content: html`<edgeless-note-senior-button .edgeless=${edgeless}>
</edgeless-note-senior-button>`,
});
}
// Brush / Eraser
tools.push({
const penSeniorTool = SeniorToolExtension('pen', ({ block }) => {
return {
name: 'Pen',
content: html`<div class="brush-and-eraser">
<edgeless-brush-tool-button
.edgeless=${edgeless}
.edgeless=${block}
></edgeless-brush-tool-button>
<edgeless-eraser-tool-button
.edgeless=${edgeless}
.edgeless=${block}
></edgeless-eraser-tool-button>
</div> `,
});
};
});
// Shape
tools.push({
name: 'Shape',
content: html`<edgeless-shape-tool-button
.edgeless=${edgeless}
.toolbarContainer=${toolbarContainer}
></edgeless-shape-tool-button>`,
});
const shapeSeniorTool = SeniorToolExtension(
'shape',
({ block, toolbarContainer }) => {
return {
name: 'Shape',
content: html`<edgeless-shape-tool-button
.edgeless=${block}
.toolbarContainer=${toolbarContainer}
></edgeless-shape-tool-button>`,
};
}
);
tools.push({
name: 'Mind Map',
content: html`<edgeless-mindmap-tool-button
.edgeless=${edgeless}
.toolbarContainer=${toolbarContainer}
></edgeless-mindmap-tool-button>`,
});
const mindMapSeniorTool = SeniorToolExtension(
'mindMap',
({ block, toolbarContainer }) => {
return {
name: 'Mind Map',
content: html`<edgeless-mindmap-tool-button
.edgeless=${block}
.toolbarContainer=${toolbarContainer}
></edgeless-mindmap-tool-button>`,
};
}
);
// Template
tools.push({
const templateSeniorTool = SeniorToolExtension('template', ({ block }) => {
return {
name: 'Template',
content: html`<edgeless-template-button .edgeless=${edgeless}>
content: html`<edgeless-template-button .edgeless=${block}>
</edgeless-template-button>`,
});
};
});
return tools;
};
export const quickTools = [
defaultQuickTool,
frameQuickTool,
connectorQuickTool,
linkQuickTool,
];
export const seniorTools = [
noteSeniorTool,
penSeniorTool,
shapeSeniorTool,
mindMapSeniorTool,
templateSeniorTool,
];

View File

@@ -21,6 +21,7 @@ import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js';
import { EDGELESS_DRAGGING_AREA_WIDGET } from './components/rects/edgeless-dragging-area-rect.js';
import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js';
import { EDGELESS_TOOLBAR_WIDGET } from './components/toolbar/edgeless-toolbar.js';
import { quickTools, seniorTools } from './components/toolbar/tools.js';
import { EdgelessRootService } from './edgeless-root-service.js';
export const edgelessZoomToolbarWidget = WidgetViewExtension(
@@ -63,6 +64,8 @@ const EdgelessCommonExtension: ExtensionType[] = [
ToolController,
EdgelessRootService,
ViewportElementExtension('.affine-edgeless-viewport'),
...quickTools,
...seniorTools,
].flat();
export const EdgelessRootBlockSpec: ExtensionType[] = [