mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 14:56:59 +08:00
refactor(editor): use extension to register edgeless toolbar button (#11062)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 { 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);
|
||||
},
|
||||
})
|
||||
),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
Reference in New Issue
Block a user