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
@@ -1,10 +1,11 @@
import type { MenuConfig } from '@blocksuite/affine-components/context-menu'; import type { MenuConfig } from '@blocksuite/affine-components/context-menu';
import type { BlockComponent } from '@blocksuite/block-std';
import type { EdgelessRootBlockComponent } from '../../../edgeless-root-block.js'; import type { GfxController } from '@blocksuite/block-std/gfx';
/** /**
* Helper function to build a menu configuration for a tool in dense mode * Helper function to build a menu configuration for a tool in dense mode
*/ */
export type DenseMenuBuilder = ( export type DenseMenuBuilder = (
edgeless: EdgelessRootBlockComponent edgeless: BlockComponent,
gfx: GfxController
) => MenuConfig; ) => MenuConfig;
@@ -9,16 +9,16 @@ import {
import type { DenseMenuBuilder } from '../common/type.js'; import type { DenseMenuBuilder } from '../common/type.js';
export const buildConnectorDenseMenu: DenseMenuBuilder = edgeless => { export const buildConnectorDenseMenu: DenseMenuBuilder = (edgeless, gfx) => {
const prevMode = const prevMode =
edgeless.std.get(EditPropsStore).lastProps$.value.connector.mode; 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 = const createSelect =
(mode: ConnectorMode, record = true) => (mode: ConnectorMode, record = true) =>
() => { () => {
edgeless.gfx.tool.setTool('connector', { gfx.tool.setTool('connector', {
mode, mode,
}); });
record && record &&
@@ -1,4 +1,5 @@
/* oxlint-disable @typescript-eslint/no-non-null-assertion */ /* oxlint-disable @typescript-eslint/no-non-null-assertion */
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import { import {
type MenuHandler, type MenuHandler,
popMenu, popMenu,
@@ -31,7 +32,6 @@ import { cache } from 'lit/directives/cache.js';
import debounce from 'lodash-es/debounce'; import debounce from 'lodash-es/debounce';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
import type { MenuPopper } from './common/create-popper.js'; import type { MenuPopper } from './common/create-popper.js';
import { import {
edgelessToolbarContext, edgelessToolbarContext,
@@ -39,7 +39,10 @@ import {
edgelessToolbarSlotsContext, edgelessToolbarSlotsContext,
edgelessToolbarThemeContext, edgelessToolbarThemeContext,
} from './context.js'; } from './context.js';
import { getQuickTools, getSeniorTools } from './tools.js'; import {
QuickToolIdentifier,
SeniorToolIdentifier,
} from './extension/index.js';
const TOOLBAR_PADDING_X = 12; const TOOLBAR_PADDING_X = 12;
const TOOLBAR_HEIGHT = 64; const TOOLBAR_HEIGHT = 64;
@@ -54,10 +57,7 @@ const DIVIDER_SPACE = 8;
const SAFE_AREA_WIDTH = 64; const SAFE_AREA_WIDTH = 64;
export const EDGELESS_TOOLBAR_WIDGET = 'edgeless-toolbar-widget'; export const EDGELESS_TOOLBAR_WIDGET = 'edgeless-toolbar-widget';
export class EdgelessToolbarWidget extends WidgetComponent< export class EdgelessToolbarWidget extends WidgetComponent<RootBlockModel> {
RootBlockModel,
EdgelessRootBlockComponent
> {
static override styles = css` static override styles = css`
:host { :host {
font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
@@ -335,10 +335,19 @@ export class EdgelessToolbarWidget extends WidgetComponent<
} }
private get _quickTools() { private get _quickTools() {
if (!this.block) { const block = this.block;
if (!block) {
return []; 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() { private get _quickToolsWidthTotal() {
@@ -379,13 +388,19 @@ export class EdgelessToolbarWidget extends WidgetComponent<
} }
private get _seniorTools() { private get _seniorTools() {
if (!this.block) { const block = this.block;
if (!block) {
return []; return [];
} }
return getSeniorTools({ const seniorTools = Array.from(
edgeless: this.block, this.std.provider.getAll(SeniorToolIdentifier).values()
toolbarContainer: this.toolbarContainer, );
}); const gfx = this.std.get(GfxControllerIdentifier);
return seniorTools
.map(tool =>
tool({ block, gfx, toolbarContainer: this.toolbarContainer })
)
.filter(({ enable = true }) => enable);
} }
private get _seniorToolsWidthTotal() { private get _seniorToolsWidthTotal() {
@@ -612,27 +627,28 @@ export class EdgelessToolbarWidget extends WidgetComponent<
override firstUpdated() { override firstUpdated() {
const { _disposables, block, gfx } = this; const { _disposables, block, gfx } = this;
if (!block) { if (!block) return;
return;
} const slots = this.std.get(EdgelessLegacySlotIdentifier);
const editPropsStore = this.std.get(EditPropsStore);
_disposables.add( _disposables.add(
gfx.viewport.viewportUpdated.subscribe(() => this.requestUpdate()) gfx.viewport.viewportUpdated.subscribe(() => this.requestUpdate())
); );
_disposables.add( _disposables.add(
block.slots.readonlyUpdated.subscribe(() => { slots.readonlyUpdated.subscribe(() => {
this.requestUpdate(); this.requestUpdate();
}) })
); );
_disposables.add( _disposables.add(
block.slots.toolbarLocked.subscribe(disabled => { slots.toolbarLocked.subscribe(disabled => {
this.toggleAttribute('disabled', disabled); this.toggleAttribute('disabled', disabled);
}) })
); );
// This state from `editPropsStore` is not reactive, // This state from `editPropsStore` is not reactive,
// if the value is updated outside of this component, it will not be reflected. // if the value is updated outside of this component, it will not be reflected.
_disposables.add( _disposables.add(
this.std.get(EditPropsStore).slots.storageUpdated.subscribe(({ key }) => { editPropsStore.slots.storageUpdated.subscribe(({ key }) => {
if (key === 'presentHideToolbar') { if (key === 'presentHideToolbar') {
this.requestUpdate(); this.requestUpdate();
} }
@@ -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);
},
};
};
@@ -1,27 +1,29 @@
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
import { menu } from '@blocksuite/affine-components/context-menu'; import { menu } from '@blocksuite/affine-components/context-menu';
import { FrameIcon } from '@blocksuite/icons/lit'; import { FrameIcon } from '@blocksuite/icons/lit';
import type { DenseMenuBuilder } from '../common/type.js'; import type { DenseMenuBuilder } from '../common/type.js';
import { FrameConfig } from './config.js'; import { FrameConfig } from './config.js';
export const buildFrameDenseMenu: DenseMenuBuilder = edgeless => export const buildFrameDenseMenu: DenseMenuBuilder = (edgeless, gfx) =>
menu.subMenu({ menu.subMenu({
name: 'Frame', name: 'Frame',
prefix: FrameIcon({ width: '20px', height: '20px' }), prefix: FrameIcon({ width: '20px', height: '20px' }),
select: () => edgeless.gfx.tool.setTool({ type: 'frame' }), select: () => gfx.tool.setTool({ type: 'frame' }),
isSelected: edgeless.gfx.tool.currentToolName$.peek() === 'frame', isSelected: gfx.tool.currentToolName$.peek() === 'frame',
options: { options: {
items: [ items: [
menu.action({ menu.action({
name: 'Custom', name: 'Custom',
select: () => edgeless.gfx.tool.setTool({ type: 'frame' }), select: () => gfx.tool.setTool({ type: 'frame' }),
}), }),
...FrameConfig.map(config => ...FrameConfig.map(config =>
menu.action({ menu.action({
name: `Slide ${config.name}`, name: `Slide ${config.name}`,
select: () => { select: () => {
edgeless.gfx.tool.setTool('default'); const frame = edgeless.std.get(EdgelessFrameManagerIdentifier);
edgeless.service.frame.createFrameOnViewportCenter(config.wh); gfx.tool.setTool('default');
frame.createFrameOnViewportCenter(config.wh);
}, },
}) })
), ),
@@ -4,16 +4,16 @@ import { LassoMode } from '@blocksuite/affine-shared/types';
import type { DenseMenuBuilder } from '../common/type.js'; import type { DenseMenuBuilder } from '../common/type.js';
import { LassoFreeHandIcon, LassoPolygonalIcon } from './icons.js'; import { LassoFreeHandIcon, LassoPolygonalIcon } from './icons.js';
export const buildLassoDenseMenu: DenseMenuBuilder = edgeless => { export const buildLassoDenseMenu: DenseMenuBuilder = (_, gfx) => {
// TODO: active state // TODO: active state
// const prevMode = // const prevMode =
// edgeless.service.editPropsStore.getLastProps('lasso').mode ?? // edgeless.service.editPropsStore.getLastProps('lasso').mode ??
// LassoMode.FreeHand; // LassoMode.FreeHand;
const isActive = edgeless.gfx.tool.currentToolName$.peek() === 'lasso'; const isActive = gfx.tool.currentToolName$.peek() === 'lasso';
const createSelect = (mode: LassoMode) => () => { const createSelect = (mode: LassoMode) => () => {
edgeless.gfx.tool.setTool('lasso', { mode }); gfx.tool.setTool('lasso', { mode });
}; };
return menu.subMenu({ return menu.subMenu({
@@ -1,164 +1,116 @@
import type { MenuConfig } from '@blocksuite/affine-components/context-menu'; import { html } from 'lit';
import type { GfxToolsMap } from '@blocksuite/block-std/gfx';
import { html, type TemplateResult } from 'lit';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js'; import { QuickToolExtension, SeniorToolExtension } from './extension/index.js';
import { buildConnectorDenseMenu } from './connector/connector-dense-menu.js';
import { buildFrameDenseMenu } from './frame/frame-dense-menu.js'; import { buildFrameDenseMenu } from './frame/frame-dense-menu.js';
import { buildLinkDenseMenu } from './link/link-dense-menu.js'; import { buildLinkDenseMenu } from './link/link-dense-menu.js';
export interface QuickTool { const defaultQuickTool = QuickToolExtension('default', ({ block }) => {
type?: keyof GfxToolsMap; return {
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({
type: 'default', type: 'default',
content: html`<edgeless-default-tool-button content: html`<edgeless-default-tool-button
.edgeless=${edgeless} .edgeless=${block}
></edgeless-default-tool-button>`, ></edgeless-default-tool-button>`,
// menu: will never show because the first tool will never hide };
}); });
// 🔧 Lasso const frameQuickTool = QuickToolExtension('frame', ({ block, gfx }) => {
// if (doc.awarenessStore.getFlag('enable_lasso_tool')) { return {
// quickTools.push({ type: 'frame',
// type: 'lasso', content: html`<edgeless-frame-tool-button
// content: html`<edgeless-lasso-tool-button .edgeless=${block}
// .edgeless=${edgeless} ></edgeless-frame-tool-button>`,
// ></edgeless-lasso-tool-button>`, menu: buildFrameDenseMenu(block, gfx),
// menu: buildLassoDenseMenu(edgeless), enable: !block.doc.readonly,
// }); };
// } });
// 🔧 Frame const connectorQuickTool = QuickToolExtension('connector', ({ block }) => {
if (!doc.readonly) { return {
quickTools.push({
type: 'frame',
content: html`<edgeless-frame-tool-button
.edgeless=${edgeless}
></edgeless-frame-tool-button>`,
menu: buildFrameDenseMenu(edgeless),
});
}
// 🔧 Connector
quickTools.push({
type: 'connector', type: 'connector',
content: html`<edgeless-connector-tool-button content: html`<edgeless-connector-tool-button
.edgeless=${edgeless} .edgeless=${block}
></edgeless-connector-tool-button>`, ></edgeless-connector-tool-button>`,
menu: buildConnectorDenseMenu(edgeless), };
}); });
// 🔧 Present const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => {
// quickTools.push({ return {
// 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({
content: html`<edgeless-link-tool-button content: html`<edgeless-link-tool-button
.edgeless=${edgeless} .edgeless=${block}
></edgeless-link-tool-button>`, ></edgeless-link-tool-button>`,
menu: buildLinkDenseMenu(edgeless), menu: buildLinkDenseMenu(block, gfx),
}); };
return quickTools; });
};
export const getSeniorTools = ({ const noteSeniorTool = SeniorToolExtension('note', ({ block }) => {
edgeless, return {
toolbarContainer, name: 'Note',
}: { content: html`<edgeless-note-senior-button
edgeless: EdgelessRootBlockComponent; .edgeless=${block}
toolbarContainer: HTMLElement; ></edgeless-note-senior-button>`,
}): SeniorTool[] => { };
const { doc } = edgeless; });
const tools: SeniorTool[] = [];
if (!doc.readonly) { const penSeniorTool = SeniorToolExtension('pen', ({ block }) => {
tools.push({ return {
name: 'Note',
content: html`<edgeless-note-senior-button .edgeless=${edgeless}>
</edgeless-note-senior-button>`,
});
}
// Brush / Eraser
tools.push({
name: 'Pen', name: 'Pen',
content: html`<div class="brush-and-eraser"> content: html`<div class="brush-and-eraser">
<edgeless-brush-tool-button <edgeless-brush-tool-button
.edgeless=${edgeless} .edgeless=${block}
></edgeless-brush-tool-button> ></edgeless-brush-tool-button>
<edgeless-eraser-tool-button <edgeless-eraser-tool-button
.edgeless=${edgeless} .edgeless=${block}
></edgeless-eraser-tool-button> ></edgeless-eraser-tool-button>
</div> `, </div> `,
}); };
});
// Shape const shapeSeniorTool = SeniorToolExtension(
tools.push({ 'shape',
name: 'Shape', ({ block, toolbarContainer }) => {
content: html`<edgeless-shape-tool-button return {
.edgeless=${edgeless} name: 'Shape',
.toolbarContainer=${toolbarContainer} content: html`<edgeless-shape-tool-button
></edgeless-shape-tool-button>`, .edgeless=${block}
}); .toolbarContainer=${toolbarContainer}
></edgeless-shape-tool-button>`,
};
}
);
tools.push({ const mindMapSeniorTool = SeniorToolExtension(
name: 'Mind Map', 'mindMap',
content: html`<edgeless-mindmap-tool-button ({ block, toolbarContainer }) => {
.edgeless=${edgeless} return {
.toolbarContainer=${toolbarContainer} name: 'Mind Map',
></edgeless-mindmap-tool-button>`, content: html`<edgeless-mindmap-tool-button
}); .edgeless=${block}
.toolbarContainer=${toolbarContainer}
></edgeless-mindmap-tool-button>`,
};
}
);
// Template const templateSeniorTool = SeniorToolExtension('template', ({ block }) => {
tools.push({ return {
name: 'Template', name: 'Template',
content: html`<edgeless-template-button .edgeless=${edgeless}> content: html`<edgeless-template-button .edgeless=${block}>
</edgeless-template-button>`, </edgeless-template-button>`,
}); };
});
return tools; export const quickTools = [
}; defaultQuickTool,
frameQuickTool,
connectorQuickTool,
linkQuickTool,
];
export const seniorTools = [
noteSeniorTool,
penSeniorTool,
shapeSeniorTool,
mindMapSeniorTool,
templateSeniorTool,
];
@@ -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_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_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js';
import { EDGELESS_TOOLBAR_WIDGET } from './components/toolbar/edgeless-toolbar.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'; import { EdgelessRootService } from './edgeless-root-service.js';
export const edgelessZoomToolbarWidget = WidgetViewExtension( export const edgelessZoomToolbarWidget = WidgetViewExtension(
@@ -63,6 +64,8 @@ const EdgelessCommonExtension: ExtensionType[] = [
ToolController, ToolController,
EdgelessRootService, EdgelessRootService,
ViewportElementExtension('.affine-edgeless-viewport'), ViewportElementExtension('.affine-edgeless-viewport'),
...quickTools,
...seniorTools,
].flat(); ].flat();
export const EdgelessRootBlockSpec: ExtensionType[] = [ export const EdgelessRootBlockSpec: ExtensionType[] = [