From 7f34667b7823d228f2dc638f1b0431230e9310cd Mon Sep 17 00:00:00 2001 From: fundon Date: Wed, 19 Mar 2025 14:50:55 +0000 Subject: [PATCH] refactor(editor): edgeless connector toolbar config extension (#10798) --- .../blocks/block-note/src/configs/toolbar.ts | 14 +- .../src/edgeless/configs/toolbar/connector.ts | 375 ++++++++++++++++-- .../src/edgeless/configs/toolbar/consts.ts | 16 + .../src/edgeless/configs/toolbar/frame.ts | 6 +- .../src/edgeless/configs/toolbar/types.ts | 16 + .../src/edgeless/configs/toolbar/utils.ts | 61 +++ .../change-connector-button.ts | 8 +- .../src/surface-ref-block.ts | 4 +- .../line-styles-panel.ts | 10 +- .../affine/model/src/consts/connector.ts | 6 +- .../model/src/elements/connector/connector.ts | 3 +- .../affine/shared/src/utils/zod-schema.ts | 11 +- 12 files changed, 476 insertions(+), 54 deletions(-) create mode 100644 blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/consts.ts create mode 100644 blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/types.ts create mode 100644 blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/utils.ts diff --git a/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts b/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts index 9f4566cd25..da530b70a4 100644 --- a/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts @@ -1,5 +1,5 @@ import { - EdgelessCRUDExtension, + EdgelessCRUDIdentifier, EdgelessLegacySlotIdentifier, } from '@blocksuite/affine-block-surface'; import { @@ -172,7 +172,9 @@ const builtinSurfaceToolbarConfig = { const color = e.detail.value; for (const model of models) { const props = packColor(field, color); - ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, props); + ctx.std + .get(EdgelessCRUDIdentifier) + .updateElement(model.id, props); } return; } @@ -230,7 +232,7 @@ const builtinSurfaceToolbarConfig = { const shadowType = e.detail; for (const model of models) { const edgeless = model.props.edgeless; - ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, { + ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, { edgeless: { ...edgeless, style: { @@ -270,7 +272,7 @@ const builtinSurfaceToolbarConfig = { const borderSize = value; for (const model of models) { const edgeless = model.props.edgeless; - ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, { + ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, { edgeless: { ...edgeless, style: { @@ -286,7 +288,7 @@ const builtinSurfaceToolbarConfig = { const borderStyle = value; for (const model of models) { const edgeless = model.props.edgeless; - ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, { + ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, { edgeless: { ...edgeless, style: { @@ -328,7 +330,7 @@ const builtinSurfaceToolbarConfig = { const borderRadius = e.detail; for (const model of models) { const edgeless = model.props.edgeless; - ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, { + ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, { edgeless: { ...edgeless, style: { diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/connector.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/connector.ts index 2da3750680..67daeffe13 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/connector.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/connector.ts @@ -1,51 +1,372 @@ -import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; +import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface'; +import { + packColor, + type PickColorEvent, +} from '@blocksuite/affine-components/color-picker'; +import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel'; +import { + ConnectorElementModel, + type ConnectorElementProps, + type ConnectorLabelProps, + ConnectorMode, + DEFAULT_FRONT_ENDPOINT_STYLE, + DEFAULT_REAR_ENDPOINT_STYLE, + DefaultTheme, + LineWidth, + PointStyle, + resolveColor, + StrokeStyle, +} from '@blocksuite/affine-model'; +import { + FeatureFlagService, + type ToolbarContext, + type ToolbarModuleConfig, +} from '@blocksuite/affine-shared/services'; +import { + getMostCommonResolvedValue, + getMostCommonValue, +} from '@blocksuite/affine-shared/utils'; import { AddTextIcon, ConnectorCIcon, + ConnectorEIcon, + ConnectorLIcon, + EndPointArrowIcon, + EndPointCircleIcon, + EndPointDiamondIcon, + EndPointTriangleIcon, FlipDirectionIcon, + StartPointArrowIcon, + StartPointCircleIcon, + StartPointDiamondIcon, StartPointIcon, + StartPointTriangleIcon, } from '@blocksuite/icons/lit'; +import { html } from 'lit'; +import { styleMap } from 'lit/directives/style-map.js'; + +import { EdgelessRootBlockComponent } from '../..'; +import { mountConnectorLabelEditor } from '../../utils/text'; +import { LINE_STYLE_LIST } from './consts'; +import type { MenuItem } from './types'; +import { renderMenu } from './utils'; + +const FRONT_ENDPOINT_STYLE_LIST = [ + { + value: PointStyle.None, + icon: StartPointIcon(), + }, + { + value: PointStyle.Arrow, + icon: StartPointArrowIcon(), + }, + { + value: PointStyle.Triangle, + icon: StartPointTriangleIcon(), + }, + { + value: PointStyle.Circle, + icon: StartPointCircleIcon(), + }, + { + value: PointStyle.Diamond, + icon: StartPointDiamondIcon(), + }, +] as const satisfies MenuItem[]; + +const REAR_ENDPOINT_STYLE_LIST = [ + { + value: PointStyle.Diamond, + icon: EndPointDiamondIcon(), + }, + { + value: PointStyle.Circle, + icon: EndPointCircleIcon(), + }, + { + value: PointStyle.Triangle, + icon: EndPointTriangleIcon(), + }, + { + value: PointStyle.Arrow, + icon: EndPointArrowIcon(), + }, + { + value: PointStyle.None, + icon: StartPointIcon(), + }, +] as const satisfies MenuItem[]; + +const CONNECTOR_MODE_LIST = [ + { + key: 'Curve', + value: ConnectorMode.Curve, + icon: ConnectorCIcon(), + }, + { + key: 'Elbowed', + value: ConnectorMode.Orthogonal, + icon: ConnectorEIcon(), + }, + { + key: 'Straight', + value: ConnectorMode.Straight, + icon: ConnectorLIcon(), + }, +] as const satisfies MenuItem[]; export const builtinConnectorToolbarConfig = { actions: [ { id: 'a.stroke-color', tooltip: 'Stroke style', - run() {}, + content(ctx) { + const models = ctx.getSurfaceModelsByType(ConnectorElementModel); + if (!models.length) return null; + + const enableCustomColor = ctx.std + .get(FeatureFlagService) + .getFlag('enable_color_picker'); + const theme = ctx.themeProvider.edgelessTheme; + + const firstModel = models[0]; + const strokeWidth = + getMostCommonValue(models, 'strokeWidth') ?? LineWidth.Four; + const strokeStyle = + getMostCommonValue(models, 'strokeStyle') ?? StrokeStyle.Solid; + const stroke = + getMostCommonResolvedValue(models, 'stroke', stroke => + resolveColor(stroke, theme) + ) ?? resolveColor(DefaultTheme.connectorColor, theme); + + const onPickColor = (e: PickColorEvent) => { + const field = 'stroke'; + + if (e.type === 'pick') { + const color = e.detail.value; + for (const model of models) { + const props = packColor(field, color); + ctx.std + .get(EdgelessCRUDIdentifier) + .updateElement(model.id, props); + } + return; + } + + for (const model of models) { + model[e.type === 'start' ? 'stash' : 'pop'](field); + } + }; + + const onPickStrokeStyle = (e: CustomEvent) => { + e.stopPropagation(); + + const { type, value } = e.detail; + + if (type === 'size') { + updateModelsWith(ctx, models, 'strokeWidth', value); + return; + } + + updateModelsWith(ctx, models, 'strokeStyle', value); + }; + + return html` + + + + + `; + }, }, { id: 'b.style', - tooltip: 'Style', - run() {}, + // TODO(@fundon): should add a feature flag + when: false, + content(ctx) { + const models = ctx.getSurfaceModelsByType(ConnectorElementModel); + if (!models.length) return null; + + const field = 'rough'; + const rough = getMostCommonValue(models, field) ?? false; + const onPick = (value: boolean) => { + updateModelsWith(ctx, models, field, value); + }; + + return renderMenu({ + label: 'Style', + items: LINE_STYLE_LIST, + currentValue: rough, + onPick, + }); + }, }, { - 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: 'c.endpoint-style', + actions: [ + { + id: 'a.start-point-style', + content(ctx) { + const models = ctx.getSurfaceModelsByType(ConnectorElementModel); + if (!models.length) return null; + + const field = 'frontEndpointStyle'; + const pointStyle = + getMostCommonValue(models, field) ?? DEFAULT_FRONT_ENDPOINT_STYLE; + const onPick = (value: PointStyle) => { + updateModelsWith(ctx, models, field, value); + }; + + return renderMenu({ + label: 'Start point style', + items: FRONT_ENDPOINT_STYLE_LIST, + currentValue: pointStyle, + onPick, + }); + }, + }, + { + id: 'b.flip-direction', + icon: FlipDirectionIcon(), + tooltip: 'Flip direction', + run(ctx) { + const models = ctx.getSurfaceModelsByType(ConnectorElementModel); + if (!models.length) return; + + const frontEndpointStyle = + getMostCommonValue(models, 'frontEndpointStyle') ?? + DEFAULT_FRONT_ENDPOINT_STYLE; + const rearEndpointStyle = + getMostCommonValue(models, 'rearEndpointStyle') ?? + DEFAULT_REAR_ENDPOINT_STYLE; + + if (frontEndpointStyle === rearEndpointStyle) return; + + for (const model of models) { + ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, { + frontEndpointStyle: rearEndpointStyle, + rearEndpointStyle: frontEndpointStyle, + }); + } + }, + }, + { + id: 'c.end-point-style', + content(ctx) { + const models = ctx.getSurfaceModelsByType(ConnectorElementModel); + if (!models.length) return null; + + const field = 'rearEndpointStyle'; + const pointStyle = + getMostCommonValue(models, field) ?? DEFAULT_REAR_ENDPOINT_STYLE; + const onPick = (value: PointStyle) => { + updateModelsWith(ctx, models, field, value); + }; + + return renderMenu({ + label: 'End point style', + items: REAR_ENDPOINT_STYLE_LIST, + currentValue: pointStyle, + onPick, + }); + }, + }, + { + id: 'd.connector-shape', + content(ctx) { + const models = ctx.getSurfaceModelsByType(ConnectorElementModel); + if (!models.length) return null; + + const field = 'mode'; + const mode = + getMostCommonValue(models, field) ?? ConnectorMode.Orthogonal; + const onPick = (value: ConnectorMode) => { + updateModelsWith(ctx, models, field, value); + }; + + return renderMenu({ + label: 'Shape', + tooltip: 'Connector shape', + items: CONNECTOR_MODE_LIST, + currentValue: mode, + onPick, + }); + }, + }, + ], }, { id: 'g.add-text', icon: AddTextIcon(), - run() {}, + when(ctx) { + const models = ctx.getSurfaceModelsByType(ConnectorElementModel); + return models.length === 1 && !models[0].text; + }, + run(ctx) { + const model = ctx.getCurrentModelByType(ConnectorElementModel); + if (!model) return; + + const rootModel = ctx.store.root; + if (!rootModel) return; + + // TODO(@fundon): it should be simple + const edgeless = ctx.view.getBlock(rootModel.id); + if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) { + console.error('edgeless view is not found.'); + return; + } + + mountConnectorLabelEditor(model, edgeless); + }, + }, + { + id: 'g.text', + when(ctx) { + const models = ctx.getSurfaceModelsByType(ConnectorElementModel); + return models.length > 0 && !models.some(model => !model.text); + }, + // TODO(@fundon): text actoins }, ], + + when: ctx => ctx.getSurfaceModelsByType(ConnectorElementModel).length > 0, } as const satisfies ToolbarModuleConfig; + +function updateModelsWith< + T extends keyof Omit, +>( + ctx: ToolbarContext, + models: ConnectorElementModel[], + field: T, + value: ConnectorElementProps[T] +) { + ctx.store.captureSync(); + + for (const model of models) { + ctx.std + .get(EdgelessCRUDIdentifier) + .updateElement(model.id, { [field]: value }); + } +} diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/consts.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/consts.ts new file mode 100644 index 0000000000..bdad47d816 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/consts.ts @@ -0,0 +1,16 @@ +import { StyleGeneralIcon, StyleScribbleIcon } from '@blocksuite/icons/lit'; + +import type { MenuItem } from './types'; + +export const LINE_STYLE_LIST = [ + { + key: 'General', + value: false, + icon: StyleGeneralIcon(), + }, + { + key: 'Scribbled', + value: true, + icon: StyleScribbleIcon(), + }, +] as const satisfies MenuItem[]; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/frame.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/frame.ts index 89a8a2b4d4..5b1f6c0799 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/frame.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/frame.ts @@ -1,5 +1,5 @@ import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame'; -import { EdgelessCRUDExtension } from '@blocksuite/affine-block-surface'; +import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface'; import { packColor, type PickColorEvent, @@ -159,7 +159,9 @@ const builtinSurfaceToolbarConfig = { const color = e.detail.value; for (const model of models) { const props = packColor(field, color); - ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, props); + ctx.std + .get(EdgelessCRUDIdentifier) + .updateElement(model.id, props); } return; } diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/types.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/types.ts new file mode 100644 index 0000000000..5f419937e7 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/types.ts @@ -0,0 +1,16 @@ +import type { TemplateResult } from 'lit'; + +export type MenuItem = { + key?: string; + value: T; + icon?: TemplateResult; +}; + +export type Menu = { + label: string; + icon?: TemplateResult; + tooltip?: string; + items: MenuItem[]; + currentValue: T; + onPick: (value: T) => void; +}; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/utils.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/utils.ts new file mode 100644 index 0000000000..6291778fc7 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/utils.ts @@ -0,0 +1,61 @@ +import { ArrowDownSmallIcon } from '@blocksuite/icons/lit'; +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { repeat } from 'lit/directives/repeat.js'; + +import type { Menu, MenuItem } from './types'; + +export function renderCurrentMenuItemWith>( + items: MenuItem[], + currentValue: T, + field: F +) { + return items.find(({ value }) => value === currentValue)?.[field]; +} + +export function renderMenu({ + label, + tooltip, + icon, + items, + currentValue, + onPick, +}: Menu) { + return html` + + ${icon ?? renderCurrentMenuItemWith(items, currentValue, 'icon')} + ${ArrowDownSmallIcon()} + + `} + > + ${renderMenuItems(items, currentValue, onPick)} + + `; +} + +export function renderMenuItems( + items: MenuItem[], + currentValue: T, + onPick: (value: T) => void +) { + return repeat( + items, + item => item.value, + ({ key, value, icon }) => html` + onPick(value)} + > + ${icon} + + ` + ); +} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-connector-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-connector-button.ts index d2cba7ee10..245dcb832f 100644 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-connector-button.ts +++ b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-connector-button.ts @@ -12,8 +12,8 @@ import { ConnectorEndpoint, type ConnectorLabelProps, ConnectorMode, - DEFAULT_FRONT_END_POINT_STYLE, - DEFAULT_REAR_END_POINT_STYLE, + DEFAULT_FRONT_ENDPOINT_STYLE, + DEFAULT_REAR_ENDPOINT_STYLE, DefaultTheme, LineWidth, PointStyle, @@ -341,12 +341,12 @@ export class EdgelessChangeConnectorButton extends WithDisposable(LitElement) { const selectedStartPointStyle = getMostCommonEndpointStyle( elements, ConnectorEndpoint.Front, - DEFAULT_FRONT_END_POINT_STYLE + DEFAULT_FRONT_ENDPOINT_STYLE ); const selectedEndPointStyle = getMostCommonEndpointStyle( elements, ConnectorEndpoint.Rear, - DEFAULT_REAR_END_POINT_STYLE + DEFAULT_REAR_ENDPOINT_STYLE ); const enableCustomColor = this.edgeless.doc .get(FeatureFlagService) diff --git a/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts b/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts index 92400bbefe..025b8302e7 100644 --- a/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts +++ b/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts @@ -1,5 +1,5 @@ import { - EdgelessCRUDExtension, + EdgelessCRUDIdentifier, getSurfaceBlock, type SurfaceBlockModel, SurfaceElementModel, @@ -457,7 +457,7 @@ export class SurfaceRefBlockComponent extends BlockComponent lineStyles.includes(item.value)), item => item.value, - ({ name, icon, value }) => { + ({ key, icon, value }) => { const active = lineStyle === value; const classInfo = { 'line-style-button': true, @@ -74,7 +74,7 @@ export class EdgelessLineStylesPanel extends LitElement { return html` this.select({ type: 'style', value })} > diff --git a/blocksuite/affine/model/src/consts/connector.ts b/blocksuite/affine/model/src/consts/connector.ts index fd90c36ba9..42d0e3dce7 100644 --- a/blocksuite/affine/model/src/consts/connector.ts +++ b/blocksuite/affine/model/src/consts/connector.ts @@ -15,9 +15,9 @@ export enum PointStyle { export const PointStyleMap = createEnumMap(PointStyle); -export const DEFAULT_FRONT_END_POINT_STYLE = PointStyle.None; +export const DEFAULT_FRONT_ENDPOINT_STYLE = PointStyle.None; -export const DEFAULT_REAR_END_POINT_STYLE = PointStyle.Arrow; +export const DEFAULT_REAR_ENDPOINT_STYLE = PointStyle.Arrow; export const CONNECTOR_LABEL_MAX_WIDTH = 280; @@ -32,3 +32,5 @@ export enum ConnectorMode { Orthogonal, Curve, } + +export const DEFAULT_CONNECTOR_MODE = ConnectorMode.Curve; diff --git a/blocksuite/affine/model/src/elements/connector/connector.ts b/blocksuite/affine/model/src/elements/connector/connector.ts index a9e7e73008..d9b16056d3 100644 --- a/blocksuite/affine/model/src/elements/connector/connector.ts +++ b/blocksuite/affine/model/src/elements/connector/connector.ts @@ -29,6 +29,7 @@ import { CONNECTOR_LABEL_MAX_WIDTH, ConnectorLabelOffsetAnchor, ConnectorMode, + DEFAULT_CONNECTOR_MODE, DEFAULT_ROUGHNESS, FontFamily, FontStyle, @@ -460,7 +461,7 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel { const { x, y } = instance; diff --git a/blocksuite/affine/shared/src/utils/zod-schema.ts b/blocksuite/affine/shared/src/utils/zod-schema.ts index 5697101fc1..cffd70750a 100644 --- a/blocksuite/affine/shared/src/utils/zod-schema.ts +++ b/blocksuite/affine/shared/src/utils/zod-schema.ts @@ -1,8 +1,9 @@ import { ColorSchema, ConnectorMode, - DEFAULT_FRONT_END_POINT_STYLE, - DEFAULT_REAR_END_POINT_STYLE, + DEFAULT_CONNECTOR_MODE, + DEFAULT_FRONT_ENDPOINT_STYLE, + DEFAULT_REAR_ENDPOINT_STYLE, DEFAULT_ROUGHNESS, DefaultTheme, EdgelessTextZodSchema, @@ -61,13 +62,13 @@ export const ConnectorSchema = z }), }) .default({ - frontEndpointStyle: DEFAULT_FRONT_END_POINT_STYLE, - rearEndpointStyle: DEFAULT_REAR_END_POINT_STYLE, + frontEndpointStyle: DEFAULT_FRONT_ENDPOINT_STYLE, + rearEndpointStyle: DEFAULT_REAR_ENDPOINT_STYLE, stroke: DefaultTheme.connectorColor, strokeStyle: StrokeStyle.Solid, strokeWidth: LineWidth.Two, rough: false, - mode: ConnectorMode.Curve, + mode: DEFAULT_CONNECTOR_MODE, labelStyle: { color: DefaultTheme.black, fontSize: 16,