From 51d89edb02858e15f94dd8501154356a0f0fcdda Mon Sep 17 00:00:00 2001 From: Saul-Mirone Date: Fri, 21 Mar 2025 11:45:31 +0000 Subject: [PATCH] refactor(editor): use extension to register edgeless toolbar button (#11062) --- .../components/toolbar/common/type.ts | 7 +- .../toolbar/connector/connector-dense-menu.ts | 6 +- .../components/toolbar/edgeless-toolbar.ts | 54 +++-- .../components/toolbar/extension/index.ts | 60 +++++ .../toolbar/frame/frame-dense-menu.ts | 14 +- .../toolbar/lasso/lasso-dense-menu.ts | 6 +- .../src/edgeless/components/toolbar/tools.ts | 218 +++++++----------- .../src/edgeless/edgeless-root-spec.ts | 3 + 8 files changed, 201 insertions(+), 167 deletions(-) create mode 100644 blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/extension/index.ts diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/common/type.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/common/type.ts index ffa167a366..747d420d2a 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/common/type.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/common/type.ts @@ -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; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/connector/connector-dense-menu.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/connector/connector-dense-menu.ts index 6fdaed5c47..d8ad2c9958 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/connector/connector-dense-menu.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/connector/connector-dense-menu.ts @@ -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 && diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/edgeless-toolbar.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/edgeless-toolbar.ts index 6e0803bc70..929f380604 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/edgeless-toolbar.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/edgeless-toolbar.ts @@ -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 { 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(); } diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/extension/index.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/extension/index.ts new file mode 100644 index 0000000000..2aab0e7bdb --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/extension/index.ts @@ -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 = (options: { + block: BlockComponent; + gfx: GfxController; + toolbarContainer: HTMLElement; +}) => T; + +export const QuickToolIdentifier = createIdentifier>( + 'edgeless-quick-tool' +); +export const SeniorToolIdentifier = createIdentifier>( + 'edgeless-senior-tool' +); + +export const QuickToolExtension = ( + id: string, + builder: ToolBuilder +): ExtensionType => { + return { + setup: di => { + di.addImpl(QuickToolIdentifier(id), () => builder); + }, + }; +}; + +export const SeniorToolExtension = ( + id: string, + builder: ToolBuilder +): ExtensionType => { + return { + setup: di => { + di.addImpl(SeniorToolIdentifier(id), () => builder); + }, + }; +}; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/frame/frame-dense-menu.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/frame/frame-dense-menu.ts index 4e199e0570..ae12d61c23 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/frame/frame-dense-menu.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/frame/frame-dense-menu.ts @@ -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); }, }) ), diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/lasso/lasso-dense-menu.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/lasso/lasso-dense-menu.ts index 162cfe38c7..a64f9ce496 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/lasso/lasso-dense-menu.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/lasso/lasso-dense-menu.ts @@ -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({ diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/tools.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/tools.ts index ca748e62df..225d0019fb 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/tools.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/tools.ts @@ -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``, - // 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``, - // menu: buildLassoDenseMenu(edgeless), - // }); - // } +const frameQuickTool = QuickToolExtension('frame', ({ block, gfx }) => { + return { + type: 'frame', + content: html``, + menu: buildFrameDenseMenu(block, gfx), + enable: !block.doc.readonly, + }; +}); - // 🔧 Frame - if (!doc.readonly) { - quickTools.push({ - type: 'frame', - content: html``, - menu: buildFrameDenseMenu(edgeless), - }); - } - - // 🔧 Connector - quickTools.push({ +const connectorQuickTool = QuickToolExtension('connector', ({ block }) => { + return { type: 'connector', content: html``, - menu: buildConnectorDenseMenu(edgeless), - }); + }; +}); - // 🔧 Present - // quickTools.push({ - // type: 'frameNavigator', - // content: html``, - // }); - - // 🔧 Note - // if (!doc.readonly) { - // quickTools.push({ - // type: 'affine:note', - // content: html` - // - // `, - // }); - // } - - // Link - quickTools.push({ +const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => { + return { content: html``, - 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``, + }; +}); - if (!doc.readonly) { - tools.push({ - name: 'Note', - content: html` - `, - }); - } - - // Brush / Eraser - tools.push({ +const penSeniorTool = SeniorToolExtension('pen', ({ block }) => { + return { name: 'Pen', content: html`
`, - }); + }; +}); - // Shape - tools.push({ - name: 'Shape', - content: html``, - }); +const shapeSeniorTool = SeniorToolExtension( + 'shape', + ({ block, toolbarContainer }) => { + return { + name: 'Shape', + content: html``, + }; + } +); - tools.push({ - name: 'Mind Map', - content: html``, - }); +const mindMapSeniorTool = SeniorToolExtension( + 'mindMap', + ({ block, toolbarContainer }) => { + return { + name: 'Mind Map', + content: html``, + }; + } +); - // Template - tools.push({ +const templateSeniorTool = SeniorToolExtension('template', ({ block }) => { + return { name: 'Template', - content: html` + content: html` `, - }); + }; +}); - return tools; -}; +export const quickTools = [ + defaultQuickTool, + frameQuickTool, + connectorQuickTool, + linkQuickTool, +]; + +export const seniorTools = [ + noteSeniorTool, + penSeniorTool, + shapeSeniorTool, + mindMapSeniorTool, + templateSeniorTool, +]; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-spec.ts b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-spec.ts index f8d031aafe..06627b4162 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-spec.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-spec.ts @@ -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[] = [