diff --git a/blocksuite/affine/blocks/block-attachment/src/configs/toolbar.ts b/blocksuite/affine/blocks/block-attachment/src/configs/toolbar.ts index 2db47aaa06..110d66d15e 100644 --- a/blocksuite/affine/blocks/block-attachment/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-attachment/src/configs/toolbar.ts @@ -136,7 +136,6 @@ export const builtinToolbarConfig = { id: 'a.rename', content(cx) { const component = cx.getCurrentBlockComponentBy( - BlockSelection, AttachmentBlockComponent ); if (!component) return null; @@ -178,7 +177,6 @@ export const builtinToolbarConfig = { icon: DownloadIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, AttachmentBlockComponent ); component?.download(); @@ -190,7 +188,6 @@ export const builtinToolbarConfig = { icon: CaptionIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, AttachmentBlockComponent ); component?.captionEditor?.show(); @@ -212,7 +209,6 @@ export const builtinToolbarConfig = { run(ctx) { // TODO(@fundon): unify `clone` method const component = ctx.getCurrentBlockComponentBy( - BlockSelection, AttachmentBlockComponent ); component?.copy(); @@ -224,7 +220,6 @@ export const builtinToolbarConfig = { icon: DuplicateIcon(), run(ctx) { const model = ctx.getCurrentBlockComponentBy( - BlockSelection, AttachmentBlockComponent )?.model; if (!model) return; @@ -247,7 +242,6 @@ export const builtinToolbarConfig = { icon: ResetIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, AttachmentBlockComponent ); component?.refreshData(); @@ -260,7 +254,7 @@ export const builtinToolbarConfig = { icon: DeleteIcon(), variant: 'destructive', run(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model) return; ctx.store.deleteBlock(model); diff --git a/blocksuite/affine/blocks/block-bookmark/src/configs/toolbar.ts b/blocksuite/affine/blocks/block-bookmark/src/configs/toolbar.ts index c7d7b94bf6..2ba2877b2e 100644 --- a/blocksuite/affine/blocks/block-bookmark/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-bookmark/src/configs/toolbar.ts @@ -276,7 +276,6 @@ export const builtinToolbarConfig = { icon: CaptionIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, BookmarkBlockComponent ); component?.captionEditor?.show(); @@ -296,7 +295,7 @@ export const builtinToolbarConfig = { label: 'Copy', icon: CopyIcon(), run(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model) return; const slice = Slice.fromModels(ctx.store, [model]); @@ -311,7 +310,7 @@ export const builtinToolbarConfig = { label: 'Duplicate', icon: DuplicateIcon(), run(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model) return; const { flavour, parent } = model; @@ -330,7 +329,6 @@ export const builtinToolbarConfig = { icon: ResetIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, BookmarkBlockComponent ); component?.refreshData(); @@ -343,7 +341,7 @@ export const builtinToolbarConfig = { icon: DeleteIcon(), variant: 'destructive', run(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model) return; ctx.store.deleteBlock(model); diff --git a/blocksuite/affine/blocks/block-embed/src/configs/toolbar.ts b/blocksuite/affine/blocks/block-embed/src/configs/toolbar.ts index 35bb9faafd..7765db24bd 100644 --- a/blocksuite/affine/blocks/block-embed/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-embed/src/configs/toolbar.ts @@ -52,7 +52,7 @@ export function createBuiltinToolbarConfigForExternal( { id: 'a.preview', content(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model || !isExternalEmbedModel(model)) return null; const { url } = model.props; @@ -72,7 +72,7 @@ export function createBuiltinToolbarConfigForExternal( id: 'inline', label: 'Inline view', run(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model || !isExternalEmbedModel(model)) return; const { title, caption, url: link } = model.props; @@ -105,7 +105,7 @@ export function createBuiltinToolbarConfigForExternal( id: 'card', label: 'Card view', disabled(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model || !isExternalEmbedModel(model)) return true; const { url } = model.props; @@ -116,7 +116,7 @@ export function createBuiltinToolbarConfigForExternal( return options?.viewType === 'card'; }, run(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model || !isExternalEmbedModel(model)) return; const { url, caption } = model.props; @@ -165,7 +165,7 @@ export function createBuiltinToolbarConfigForExternal( id: 'embed', label: 'Embed view', disabled(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model || !isExternalEmbedModel(model)) return false; const { url } = model.props; @@ -176,7 +176,7 @@ export function createBuiltinToolbarConfigForExternal( return options?.viewType === 'embed'; }, when(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model || !isExternalEmbedModel(model)) return false; const { url } = model.props; @@ -187,7 +187,7 @@ export function createBuiltinToolbarConfigForExternal( return options?.viewType === 'embed'; }, run(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model || !isExternalEmbedModel(model)) return; const { url, caption } = model.props; @@ -231,7 +231,7 @@ export function createBuiltinToolbarConfigForExternal( }, ], content(ctx) { - const model = ctx.getCurrentBlockBy(BlockSelection)?.model; + const model = ctx.getCurrentModel(); if (!model || !isExternalEmbedModel(model)) return null; const { url } = model.props; @@ -322,10 +322,7 @@ export function createBuiltinToolbarConfigForExternal( tooltip: 'Caption', icon: CaptionIcon(), run(ctx) { - const component = ctx.getCurrentBlockComponentBy( - BlockSelection, - klass - ); + const component = ctx.getCurrentBlockComponentBy(klass); if (!component) return; component.captionEditor?.show(); @@ -378,10 +375,7 @@ export function createBuiltinToolbarConfigForExternal( label: 'Reload', icon: ResetIcon(), run(ctx) { - const component = ctx.getCurrentBlockComponentBy( - BlockSelection, - klass - ); + const component = ctx.getCurrentBlockComponentBy(klass); component?.refreshData(); }, }, diff --git a/blocksuite/affine/blocks/block-embed/src/embed-html-block/configs/toolbar.ts b/blocksuite/affine/blocks/block-embed/src/embed-html-block/configs/toolbar.ts index 977641d35a..43f9d6152a 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-html-block/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-html-block/configs/toolbar.ts @@ -37,7 +37,6 @@ export const builtinToolbarConfig = { tooltip: 'Open this doc', run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedHtmlBlockComponent ); component?.open(); @@ -99,7 +98,6 @@ export const builtinToolbarConfig = { icon: CaptionIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedHtmlBlockComponent ); component?.captionEditor?.show(); diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/toolbar.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/toolbar.ts index 833cfeb9e4..f6f9296097 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/toolbar.ts @@ -27,9 +27,6 @@ import * as Y from 'yjs'; import { EmbedIframeBlockComponent } from '../embed-iframe-block'; const trackBaseProps = { - segment: 'doc', - page: 'doc editor', - module: 'toolbar', category: 'bookmark', type: 'card view', }; @@ -156,7 +153,6 @@ export const builtinToolbarConfig = { icon: CaptionIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedIframeBlockComponent ); component?.captionEditor?.show(); @@ -210,7 +206,6 @@ export const builtinToolbarConfig = { icon: ResetIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedIframeBlockComponent ); component?.refreshData().catch(console.error); diff --git a/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/configs/toolbar.ts b/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/configs/toolbar.ts index f4a7d5924a..e8b4fb5481 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/configs/toolbar.ts @@ -38,7 +38,6 @@ export const builtinToolbarConfig = { id: 'a.doc-title', content(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedLinkedDocBlockComponent ); if (!component) return null; @@ -63,7 +62,6 @@ export const builtinToolbarConfig = { label: 'Inline view', run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedLinkedDocBlockComponent ); component?.covertToInline(); @@ -89,7 +87,6 @@ export const builtinToolbarConfig = { label: 'Embed view', disabled(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedLinkedDocBlockComponent ); if (!component) return true; @@ -108,7 +105,6 @@ export const builtinToolbarConfig = { }, run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedLinkedDocBlockComponent ); component?.convertToEmbed(); @@ -208,7 +204,6 @@ export const builtinToolbarConfig = { icon: CaptionIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedLinkedDocBlockComponent ); component?.captionEditor?.show(); diff --git a/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/configs/toolbar.ts b/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/configs/toolbar.ts index 75039c8f99..f4a272ba4f 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/configs/toolbar.ts @@ -50,7 +50,6 @@ export const builtinToolbarConfig = { ], content(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedSyncedDocBlockComponent ); if (!component) return null; @@ -117,14 +116,13 @@ export const builtinToolbarConfig = { label: 'Inline view', run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedSyncedDocBlockComponent ); component?.covertToInline(); // Clears - ctx.reset(); ctx.select('note'); + ctx.reset(); ctx.track('SelectedView', { ...trackBaseProps, @@ -138,7 +136,6 @@ export const builtinToolbarConfig = { label: 'Card view', run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedSyncedDocBlockComponent ); component?.convertToCard(); @@ -192,7 +189,6 @@ export const builtinToolbarConfig = { icon: CaptionIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedSyncedDocBlockComponent ); component?.captionEditor?.show(); diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/attachment.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/attachment.ts new file mode 100644 index 0000000000..6f23da1c5a --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/attachment.ts @@ -0,0 +1,11 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const builtinAttachmentToolbarConfig = { + actions: [ + { + id: 'a.test', + label: 'Attachment', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/bookmark.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/bookmark.ts new file mode 100644 index 0000000000..d38962d349 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/bookmark.ts @@ -0,0 +1,11 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const builtinBookmarkToolbarConfig = { + actions: [ + { + id: 'a.test', + label: 'Bookmark', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/brush.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/brush.ts new file mode 100644 index 0000000000..53fa542f40 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/brush.ts @@ -0,0 +1,11 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const builtinBrushToolbarConfig = { + actions: [ + { + id: 'a.test', + label: 'Brush', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; 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 new file mode 100644 index 0000000000..2da3750680 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/connector.ts @@ -0,0 +1,51 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; +import { + AddTextIcon, + ConnectorCIcon, + FlipDirectionIcon, + StartPointIcon, +} from '@blocksuite/icons/lit'; + +export const builtinConnectorToolbarConfig = { + actions: [ + { + id: 'a.stroke-color', + tooltip: 'Stroke style', + run() {}, + }, + { + id: 'b.style', + tooltip: 'Style', + run() {}, + }, + { + 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: 'g.add-text', + icon: AddTextIcon(), + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/embed.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/embed.ts new file mode 100644 index 0000000000..af6fe62867 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/embed.ts @@ -0,0 +1,11 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const builtinEmbedToolbarConfig = { + actions: [ + { + id: 'a.test', + label: 'Embed', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; 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 new file mode 100644 index 0000000000..fccce2bd52 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/frame.ts @@ -0,0 +1,11 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const builtinFrameToolbarConfig = { + actions: [ + { + id: 'a.test', + label: 'Frame', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/group.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/group.ts new file mode 100644 index 0000000000..119af71ef9 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/group.ts @@ -0,0 +1,11 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const builtinGroupToolbarConfig = { + actions: [ + { + id: 'a.test', + label: 'Group', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/image.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/image.ts new file mode 100644 index 0000000000..1a0e5030eb --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/image.ts @@ -0,0 +1,11 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const builtinImageToolbarConfig = { + actions: [ + { + id: 'a.test', + label: 'Image', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts new file mode 100644 index 0000000000..453cdeede8 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts @@ -0,0 +1,84 @@ +import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services'; +import { BlockFlavourIdentifier } from '@blocksuite/block-std'; +import type { ExtensionType } from '@blocksuite/store'; + +import { builtinAttachmentToolbarConfig } from './attachment'; +import { builtinBookmarkToolbarConfig } from './bookmark'; +import { builtinBrushToolbarConfig } from './brush'; +import { builtinConnectorToolbarConfig } from './connector'; +import { builtinEmbedToolbarConfig } from './embed'; +import { builtinFrameToolbarConfig } from './frame'; +import { builtinGroupToolbarConfig } from './group'; +import { builtinImageToolbarConfig } from './image'; +import { builtinMindmapToolbarConfig } from './mindmap'; +import { builtinMiscToolbarConfig } from './misc'; +import { builtinNoteToolbarConfig } from './note'; +import { builtinShapeToolbarConfig } from './shape'; +import { builtinTextToolbarConfig } from './text'; + +export const EdgelessElementToolbarExtension: ExtensionType[] = [ + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:attachment'), + config: builtinAttachmentToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:bookmark'), + config: builtinBookmarkToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:image'), + config: builtinImageToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:brush'), + config: builtinBrushToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:connector'), + config: builtinConnectorToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:embed'), + config: builtinEmbedToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:frame'), + config: builtinFrameToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:group'), + config: builtinGroupToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:mindmap'), + config: builtinMindmapToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:note'), + config: builtinNoteToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:shape'), + config: builtinShapeToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:text'), + config: builtinTextToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('affine:surface:*'), + config: builtinMiscToolbarConfig, + }), +]; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/mindmap.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/mindmap.ts new file mode 100644 index 0000000000..c025aa4e96 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/mindmap.ts @@ -0,0 +1,11 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const builtinMindmapToolbarConfig = { + actions: [ + { + id: 'a.test', + label: 'Mindmap', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/misc.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/misc.ts new file mode 100644 index 0000000000..223162e25f --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/misc.ts @@ -0,0 +1,41 @@ +import { + ActionPlacement, + type ToolbarModuleConfig, +} from '@blocksuite/affine-shared/services'; +import { + ConnectorCIcon, + LockIcon, + ReleaseFromGroupIcon, +} from '@blocksuite/icons/lit'; + +export const builtinMiscToolbarConfig = { + actions: [ + { + placement: ActionPlacement.Start, + id: 'a.release-from-group', + tooltip: 'Release from group', + icon: ReleaseFromGroupIcon(), + run() {}, + }, + { + placement: ActionPlacement.Start, + id: 'a.misc', + label: 'Misc', + run() {}, + }, + { + placement: ActionPlacement.End, + id: 'a.draw-connector', + icon: ConnectorCIcon(), + tooltip: 'Draw connector', + run() {}, + }, + { + placement: ActionPlacement.End, + id: 'b.lock', + icon: LockIcon(), + tooltip: 'Lock', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/note.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/note.ts new file mode 100644 index 0000000000..66866d08d0 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/note.ts @@ -0,0 +1,11 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const builtinNoteToolbarConfig = { + actions: [ + { + id: 'a.test', + label: 'Note', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/shape.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/shape.ts new file mode 100644 index 0000000000..e5530aa9b0 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/shape.ts @@ -0,0 +1,39 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; +import { + AddTextIcon, + ShapeIcon, + StyleGeneralIcon, +} from '@blocksuite/icons/lit'; + +export const builtinShapeToolbarConfig = { + actions: [ + { + id: 'a.switch-type', + icon: ShapeIcon(), + tooltip: 'Switch type', + run() {}, + }, + { + id: 'b.style', + icon: StyleGeneralIcon(), + tooltip: 'Style', + run() {}, + }, + { + id: 'c.fill-color', + label: 'Fill color', + run() {}, + }, + { + id: 'd.border-style', + label: 'Border style', + run() {}, + }, + { + id: 'e.text', + icon: AddTextIcon(), + tooltip: 'Show add button or text menu', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/text.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/text.ts new file mode 100644 index 0000000000..bd87943e85 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/text.ts @@ -0,0 +1,11 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const builtinTextToolbarConfig = { + actions: [ + { + id: 'a.test', + label: 'Text', + run() {}, + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-builtin-spec.ts b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-builtin-spec.ts index 83b9c291c4..8c550e5e24 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-builtin-spec.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-builtin-spec.ts @@ -3,6 +3,7 @@ import { ConnectionOverlay } from '@blocksuite/affine-block-surface'; import { TextTool } from '@blocksuite/affine-gfx-text'; import type { ExtensionType } from '@blocksuite/store'; +import { EdgelessElementToolbarExtension } from './configs/toolbar'; import { EdgelessRootBlockSpec } from './edgeless-root-spec.js'; import { BrushTool } from './gfx-tool/brush-tool.js'; import { ConnectorTool } from './gfx-tool/connector-tool.js'; @@ -40,7 +41,8 @@ export const EdgelessBuiltInManager: ExtensionType[] = [ MindMapIndicatorOverlay, SnapManager, EditPropsMiddlewareBuilder, -]; + EdgelessElementToolbarExtension, +].flat(); export const EdgelessBuiltInSpecs: ExtensionType[] = [ EdgelessRootBlockSpec, diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/utils/text.ts b/blocksuite/affine/blocks/block-root/src/edgeless/utils/text.ts index 7af647ce5a..4268550c59 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/utils/text.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/utils/text.ts @@ -27,13 +27,6 @@ export function mountShapeTextEditor( ); } - if (!shapeElement.text) { - const text = new Y.Text(); - edgeless.std - .get(EdgelessCRUDIdentifier) - .updateElement(shapeElement.id, { text }); - } - const updatedElement = edgeless.service.crud.getElementById(shapeElement.id); if (!(updatedElement instanceof ShapeElementModel)) { @@ -41,17 +34,25 @@ export function mountShapeTextEditor( return; } + edgeless.gfx.tool.setTool('default'); + edgeless.gfx.selection.set({ + elements: [shapeElement.id], + editing: true, + }); + + if (!shapeElement.text) { + const text = new Y.Text(); + edgeless.std + .get(EdgelessCRUDIdentifier) + .updateElement(shapeElement.id, { text }); + } + const shapeEditor = new EdgelessShapeTextEditor(); shapeEditor.element = updatedElement; shapeEditor.edgeless = edgeless; shapeEditor.mountEditor = mountShapeTextEditor; edgeless.mountElm.append(shapeEditor); - edgeless.gfx.tool.setTool('default'); - edgeless.gfx.selection.set({ - elements: [shapeElement.id], - editing: true, - }); } export function mountFrameTitleEditor( @@ -65,16 +66,17 @@ export function mountFrameTitleEditor( ); } - const frameEditor = new EdgelessFrameTitleEditor(); - frameEditor.frameModel = frame; - frameEditor.edgeless = edgeless; - - edgeless.mountElm.append(frameEditor); edgeless.gfx.tool.setTool('default'); edgeless.gfx.selection.set({ elements: [frame.id], editing: true, }); + + const frameEditor = new EdgelessFrameTitleEditor(); + frameEditor.frameModel = frame; + frameEditor.edgeless = edgeless; + + edgeless.mountElm.append(frameEditor); } export function mountGroupTitleEditor( @@ -88,16 +90,17 @@ export function mountGroupTitleEditor( ); } - const groupEditor = new EdgelessGroupTitleEditor(); - groupEditor.group = group; - groupEditor.edgeless = edgeless; - - edgeless.mountElm.append(groupEditor); edgeless.gfx.tool.setTool('default'); edgeless.gfx.selection.set({ elements: [group.id], editing: true, }); + + const groupEditor = new EdgelessGroupTitleEditor(); + groupEditor.group = group; + groupEditor.edgeless = edgeless; + + edgeless.mountElm.append(groupEditor); } export function mountConnectorLabelEditor( @@ -112,6 +115,12 @@ export function mountConnectorLabelEditor( ); } + edgeless.gfx.tool.setTool('default'); + edgeless.gfx.selection.set({ + elements: [connector.id], + editing: true, + }); + if (!connector.text) { const text = new Y.Text(); const labelOffset = connector.labelOffset; @@ -143,9 +152,4 @@ export function mountConnectorLabelEditor( editor.inlineEditor?.focusEnd(); }) .catch(console.error); - edgeless.gfx.tool.setTool('default'); - edgeless.gfx.selection.set({ - elements: [connector.id], - editing: true, - }); } diff --git a/blocksuite/affine/components/src/toolbar/toolbar.ts b/blocksuite/affine/components/src/toolbar/toolbar.ts index 4bcbb87c4c..bbf91b15a5 100644 --- a/blocksuite/affine/components/src/toolbar/toolbar.ts +++ b/blocksuite/affine/components/src/toolbar/toolbar.ts @@ -35,7 +35,9 @@ export class EditorToolbar extends WithDisposable(LitElement) { e.stopPropagation(); e.preventDefault(); }); - this._disposables.addFromEvent(this, 'wheel', stopPropagation); + this._disposables.addFromEvent(this, 'wheel', stopPropagation, { + passive: false, + }); } override render() { diff --git a/blocksuite/affine/shared/src/services/toolbar-service/action.ts b/blocksuite/affine/shared/src/services/toolbar-service/action.ts index 2594531bd6..58cfc7a330 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/action.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/action.ts @@ -4,6 +4,7 @@ import type { ToolbarContext } from './context'; export enum ActionPlacement { Start = 0, + Normal = 1 << 0, End = 1 << 1, More = 1 << 2, } diff --git a/blocksuite/affine/shared/src/services/toolbar-service/context.ts b/blocksuite/affine/shared/src/services/toolbar-service/context.ts index 56c4a5f857..6ad031aebf 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/context.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/context.ts @@ -2,6 +2,7 @@ import { type BlockComponent, BlockSelection, type BlockStdScope, + SurfaceSelection, } from '@blocksuite/block-std'; import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import { nextTick } from '@blocksuite/global/utils'; @@ -61,7 +62,8 @@ abstract class ToolbarContextBase { if (this.flags.accept()) return true; if (this.host.event.active) return true; // Selects `embed-synced-doc-block` - return this.host.contains(document.activeElement); + if (this.host.contains(document.activeElement)) return true; + return this.isEdgelessMode; } get readonly() { @@ -108,6 +110,26 @@ abstract class ToolbarContextBase { return this.toolbarRegistry.message$; } + getCurrentElement() { + const selection = this.selection.find(SurfaceSelection); + return selection?.elements.length + ? this.gfx.getElementById(selection.elements[0]) + : null; + } + + getCurrentBlock(): Block | null { + return this.getCurrentBlockBy(); + } + + getCurrentBlockComponent(): BlockComponent | null { + const block = this.getCurrentBlock(); + return block && this.view.getBlock(block.id); + } + + getCurrentModel() { + return this.getCurrentBlock()?.model ?? null; + } + getCurrentBlockBy(type?: T): Block | null { const selection = this.selection.find(type ?? BlockSelection); return (selection && this.store.getBlock(selection.blockId)) ?? null; @@ -125,11 +147,10 @@ abstract class ToolbarContextBase { return matchModels(model, [klass]) ? model : null; } - getCurrentBlockComponentBy< - T extends SelectionConstructor, - K extends abstract new (...args: any) => any, - >(type: T, klass: K): InstanceType | null { - const block = this.getCurrentBlockBy(type); + getCurrentBlockComponentBy any>( + klass: K + ): InstanceType | null { + const block = this.getCurrentBlockBy(); const component = block && this.view.getBlock(block.id); return this.blockComponentIs(component, klass) ? component : null; } diff --git a/blocksuite/affine/shared/src/services/toolbar-service/flags.ts b/blocksuite/affine/shared/src/services/toolbar-service/flags.ts index 45a3f25e79..58d1291191 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/flags.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/flags.ts @@ -62,6 +62,10 @@ export class Flags { }); } + isSurface() { + return this.check(Flag.Surface); + } + isText() { return this.check(Flag.Text); } @@ -74,6 +78,10 @@ export class Flags { return this.check(Flag.Native); } + isHovering() { + return this.check(Flag.Hovering); + } + accept() { return this.check(Flag.Accepting); } diff --git a/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts b/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts index 64df593e77..b87f2b70b3 100644 --- a/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts +++ b/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts @@ -7,10 +7,6 @@ import { ListBlockModel, ParagraphBlockModel, } from '@blocksuite/affine-model'; -import { - getBlockSelectionsCommand, - getSelectedBlocksCommand, -} from '@blocksuite/affine-shared/commands'; import { ToolbarContext, ToolbarFlag as Flag, @@ -18,20 +14,31 @@ import { } from '@blocksuite/affine-shared/services'; import { matchModels } from '@blocksuite/affine-shared/utils'; import { + type BlockComponent, BlockSelection, - SurfaceSelection, TextSelection, WidgetComponent, } from '@blocksuite/block-std'; -import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; -import { Bound, getCommonBound } from '@blocksuite/global/gfx'; +import { + GfxBlockElementModel, + type GfxController, + type GfxModel, + GfxPrimitiveElementModel, +} from '@blocksuite/block-std/gfx'; +import { + Bound, + getCommonBound, + getCommonBoundWithRotation, +} from '@blocksuite/global/gfx'; import { nextTick } from '@blocksuite/global/utils'; -import type { Placement, ReferenceElement } from '@floating-ui/dom'; +import type { Placement, ReferenceElement, SideObject } from '@floating-ui/dom'; import { batch, effect, signal } from '@preact/signals-core'; import { css } from 'lit'; +import groupBy from 'lodash-es/groupBy'; import throttle from 'lodash-es/throttle'; +import toPairs from 'lodash-es/toPairs'; -import { autoUpdatePosition, renderToolbar } from './utils'; +import { autoUpdatePosition, renderToolbar, sideMap } from './utils'; export const AFFINE_TOOLBAR_WIDGET = 'affine-toolbar-widget'; @@ -47,7 +54,7 @@ export class AffineToolbarWidget extends WidgetComponent { backface-visibility: hidden; z-index: var(--affine-z-index-popover); - will-change: opacity, transform; + will-change: opacity, overlay, display, transform; transition-property: opacity, overlay, display; transition-duration: 120ms; transition-timing-function: ease-out; @@ -65,10 +72,63 @@ export class AffineToolbarWidget extends WidgetComponent { } `; - range$ = signal(null); - flavour$ = signal('affine:note'); + placement$ = signal('top'); + + sideOptions$ = signal | null>(null); + + referenceElement$ = signal<(() => ReferenceElement | null) | null>(null); + + setReferenceElementWithRange(range: Range | null) { + this.referenceElement$.value = range + ? () => ({ + getBoundingClientRect: () => range.getBoundingClientRect(), + getClientRects: () => + Array.from(range.getClientRects()).filter(rect => + Math.round(rect.width) + ), + }) + : null; + } + + setReferenceElementWithHtmlElement(element: Element | null) { + this.referenceElement$.value = element ? () => element : null; + } + + setReferenceElementWithBlocks(blocks: BlockComponent[]) { + const getClientRects = () => blocks.map(e => e.getBoundingClientRect()); + + this.referenceElement$.value = blocks.length + ? () => ({ + getBoundingClientRect: () => { + const rects = getClientRects(); + const bounds = getCommonBound(rects.map(Bound.fromDOMRect)); + if (!bounds) return rects[0]; + return new DOMRect(bounds.x, bounds.y, bounds.w, bounds.h); + }, + getClientRects, + }) + : null; + } + + setReferenceElementWithElements(gfx: GfxController, elements: GfxModel[]) { + const getBoundingClientRect = () => { + const bounds = getCommonBoundWithRotation(elements); + const { x: offsetX, y: offsetY } = this.getBoundingClientRect(); + const [x, y, w, h] = gfx.viewport.toViewBound(bounds).toXYWH(); + const rect = new DOMRect(x + offsetX, y + offsetY, w, h); + return rect; + }; + + this.referenceElement$.value = elements.length + ? () => ({ + getBoundingClientRect, + getClientRects: () => [getBoundingClientRect()], + }) + : null; + } + toolbar = new EditorToolbar(); get toolbarRegistry() { @@ -80,7 +140,9 @@ export class AffineToolbarWidget extends WidgetComponent { const { flavour$, - range$, + placement$, + sideOptions$, + referenceElement$, disposables, toolbar, toolbarRegistry, @@ -98,22 +160,25 @@ export class AffineToolbarWidget extends WidgetComponent { // Selects text in note. disposables.add( std.selection.find$(TextSelection).subscribe(result => { - const activated = + const range = std.range.value ?? null; + const activated = Boolean( context.activated && - Boolean( + range && result && - !result.isCollapsed() && - result.from.length + (result.to?.length ?? 0) - ); + !result.isCollapsed() && + result.from.length + (result.to?.length ?? 0) + ); batch(() => { flags.toggle(Flag.Text, activated); if (!activated) return; - const range = std.range.value ?? null; - range$.value = activated ? range : null; + this.setReferenceElementWithRange(range); + sideOptions$.value = null; + flavour$.value = 'affine:note'; + placement$.value = 'top'; flags.refresh(Flag.Text); }); }) @@ -124,54 +189,68 @@ export class AffineToolbarWidget extends WidgetComponent { disposables.addFromEvent(document, 'selectionchange', () => { const range = std.range.value ?? null; let activated = context.activated && Boolean(range && !range.collapsed); + let isNative = false; if (activated) { const result = std.selection.find(DatabaseSelection); const viewSelection = result?.viewSelection; - - activated = Boolean( - viewSelection && - ((viewSelection.selectionType === 'area' && + if (viewSelection) { + isNative = + (viewSelection.selectionType === 'area' && viewSelection.isEditing) || - (viewSelection.selectionType === 'cell' && - viewSelection.isEditing)) - ); + (viewSelection.selectionType === 'cell' && viewSelection.isEditing); + } - if (!activated) { + if (!isNative) { const result = std.selection.find(TableSelection); const viewSelection = result?.data; - activated = Boolean(viewSelection && viewSelection.type === 'area'); + if (viewSelection) { + isNative = viewSelection.type === 'area'; + } } } batch(() => { + activated &&= isNative; + + // Focues outside: `doc-title` + if ( + flags.check(Flag.Text) && + !std.host.contains(range?.commonAncestorContainer ?? null) + ) { + flags.toggle(Flag.Text, false); + } + flags.toggle(Flag.Native, activated); if (!activated) return; - range$.value = activated ? range : null; - flavour$.value = 'affine:note'; + this.setReferenceElementWithRange(range); + sideOptions$.value = null; + flavour$.value = 'affine:note'; + placement$.value = 'top'; flags.refresh(Flag.Native); }); }); // Selects blocks in note. disposables.add( - std.selection.filter$(BlockSelection).subscribe(result => { - const count = result.length; + std.selection.filter$(BlockSelection).subscribe(selections => { + const blockIds = selections.map(s => s.blockId); + const count = blockIds.length; let flavour = 'affine:note'; let activated = context.activated && Boolean(count); if (activated) { // Handles a signal block. - const block = count === 1 && std.store.getBlock(result[0].blockId); + const block = count === 1 && std.store.getBlock(blockIds[0]); // Chencks if block's config exists. if (block) { const modelFlavour = block.model.flavour; const existed = - toolbarRegistry.modules.has(modelFlavour) || + toolbarRegistry.modules.has(modelFlavour) ?? toolbarRegistry.modules.has(`custom:${modelFlavour}`); if (existed) { flavour = modelFlavour; @@ -187,12 +266,19 @@ export class AffineToolbarWidget extends WidgetComponent { } batch(() => { - flavour$.value = flavour; - flags.toggle(Flag.Block, activated); if (!activated) return; + this.setReferenceElementWithBlocks( + blockIds + .map(id => std.view.getBlock(id)) + .filter(block => block !== null) + ); + + sideOptions$.value = null; + flavour$.value = flavour; + placement$.value = flavour === 'affine:note' ? 'top' : 'top-start'; flags.refresh(Flag.Block); }); }) @@ -201,12 +287,78 @@ export class AffineToolbarWidget extends WidgetComponent { // Selects elements in edgeless. // Triggered only when not in editing state. disposables.add( - std.selection.filter$(SurfaceSelection).subscribe(result => { + context.gfx.selection.slots.updated.subscribe(selections => { + // TODO(@fundon): should remove it when edgeless element toolbar is removed + if (context.isEdgelessMode) return; + + const elementIds = selections + .map(s => (s.editing || s.inoperable ? [] : s.elements)) + .flat(); + const count = elementIds.length; + const gfx = context.gfx; + const surface = gfx.surface; const activated = - context.activated && - Boolean(result.length) && - !result.some(e => e.editing); - flags.toggle(Flag.Surface, activated); + context.activated && Boolean(surface) && Boolean(count); + let flavour = 'affine:surface'; + let elements: GfxModel[] = []; + let hasLocked = false; + let sideOptions = null; + + if (activated && surface) { + elements = elementIds + .map(id => gfx.getElementById(id)) + .filter(model => model !== null) as GfxModel[]; + + hasLocked = elements.some(e => e.isLocked()); + + const grouped = groupBy( + elements.map(model => { + let flavour = surface.flavour; + + if (model instanceof GfxBlockElementModel) { + flavour += `:${model.flavour.split(':').pop()}`; + } else if (model instanceof GfxPrimitiveElementModel) { + flavour += `:${model.type}`; + } + + return { model, flavour }; + }), + e => e.flavour + ); + + const paired = toPairs(grouped); + + if (paired.length === 1) { + flavour = paired[0][0]; + if ( + flavour === 'affine:surface:shape' && + paired[0][1].length === 1 + ) { + sideOptions = sideMap.get(flavour) ?? null; + } + } + if (!sideOptions) { + const flavours = new Set(paired.map(([f]) => f)); + if (flavours.has('affine:surface:frame')) { + sideOptions = sideMap.get('affine:surface:frame') ?? null; + } else if (flavours.has('affine:surface:group')) { + sideOptions = sideMap.get('affine:surface:group') ?? null; + } + } + } + + batch(() => { + flags.toggle(Flag.Surface, activated); + + if (!activated || !flavour) return; + + this.setReferenceElementWithElements(gfx, elements); + + sideOptions$.value = sideOptions; + flavour$.value = flavour; + placement$.value = hasLocked ? 'top' : 'top-start'; + flags.refresh(Flag.Surface); + }); }) ); @@ -223,7 +375,8 @@ export class AffineToolbarWidget extends WidgetComponent { if (!hasTextSelection) return; const range = std.range.value ?? null; - range$.value = range && !range.collapsed ? range : null; + + this.setReferenceElementWithRange(range); // TODO(@fundon): maybe here can be further optimized // 1. Prevents flickering effects. @@ -236,21 +389,27 @@ export class AffineToolbarWidget extends WidgetComponent { ); // TODO(@fundon): improve these cases - // When switch the view mode, wait until the view is created + // Waits until the view is created when switching the view mode. // `card view` or `embed view` disposables.add( std.view.viewUpdated.subscribe(record => { - if ( - record.type === 'block' && - flags.isBlock() && - std.selection - .filter$(BlockSelection) - .peek() - .find(s => s.blockId === record.id) - ) { - if (record.method === 'add') { + if (record.type !== 'block') return; + if (!flags.isBlock()) return; + + const blockIds = std.selection + .filter$(BlockSelection) + .peek() + .map(s => s.blockId); + + if (record.method === 'add' && blockIds.includes(record.id)) { + batch(() => { + this.setReferenceElementWithBlocks( + blockIds + .map(id => std.view.getBlock(id)) + .filter(block => block !== null) + ); flags.refresh(Flag.Block); - } + }); return; } }) @@ -302,9 +461,9 @@ export class AffineToolbarWidget extends WidgetComponent { eventOptions ); - // Handles hover elements + // Handles element when hovering disposables.add( - toolbarRegistry.message$.subscribe(data => { + message$.subscribe(data => { if ( !context.activated || flags.contains(Flag.Text | Flag.Native | Flag.Block) @@ -324,130 +483,85 @@ export class AffineToolbarWidget extends WidgetComponent { setFloating(toolbar); - flavour$.value = flavour; + this.setReferenceElementWithHtmlElement(data.element); + sideOptions$.value = null; + flavour$.value = flavour; + placement$.value = 'top'; flags.refresh(Flag.Hovering); }); }) ); - // Should update position of notes' toolbar in edgeless - disposables.add( - this.std - .get(GfxControllerIdentifier) - .viewport.viewportUpdated.subscribe(() => { - if (!context.activated) return; - - if (flags.value === Flag.None || flags.check(Flag.Hiding)) { - return; - } - - if (flags.isText()) { - flags.refresh(Flag.Text); - return; - } - - if (flags.isNative()) { - flags.refresh(Flag.Native); - return; - } - - if (flags.isBlock()) { - flags.refresh(Flag.Block); - return; - } - }) - ); - - disposables.add( - flags.value$.subscribe(value => { - // Hides toolbar - if (value === Flag.None || flags.check(Flag.Hiding, value)) { - delete toolbar.dataset.open; - return; - } - - // Shows toolbar - // 1. `Flag.Text`: formatting in note - // 2. `Flag.Native`: formating in database - // 3. `Flag.Block`: blocks in note - // 4. `Flag.Hovering`: inline links in note/database - if ( - flags.contains( - Flag.Hovering | Flag.Text | Flag.Native | Flag.Block, - value - ) - ) { - renderToolbar(toolbar, context, flavour$.peek()); - toolbar.dataset.open = 'true'; - return; - } - - // Shows toolbar in edgeles - // TODO(@fundon): handles edgeless toolbar - }) - ); - disposables.add( effect(() => { const value = flags.value$.value; + + // Hides toolbar + if (value === Flag.None || flags.check(Flag.Hiding, value)) { + if (toolbar.dataset.open) delete toolbar.dataset.open; + return; + } + const flavour = flavour$.value; - if (!context.activated || flags.contains(Flag.Hiding, value)) return; + + // Shows toolbar + // 1. `Flag.Text`: formatting in note + // 2. `Flag.Native`: formating in database/table + // 3. `Flag.Block`: blocks in note + // 4. `Flag.Hovering`: inline links in note/database/table + // 5. `Flag.Surface`: elements in edgeless + renderToolbar(toolbar, context, flavour); + if (toolbar.dataset.open) return; + toolbar.dataset.open = 'true'; + }) + ); + + let abortController = new AbortController(); + + disposables.add( + effect(() => { + if (!abortController.signal.aborted) { + abortController.abort(); + } + + const value = flags.value$.value; + if ( - !flags.contains( - Flag.Hovering | Flag.Text | Flag.Native | Flag.Block, - value - ) + !context.activated || + Flag.None === value || + flags.contains(Flag.Hiding, value) ) return; - // TODO(@fundon): improves here - const isNote = flavour === 'affine:note'; - let placement = isNote ? ('top' as Placement) : undefined; - let virtualEl: ReferenceElement | null = null; + const build = referenceElement$.value; + const referenceElement = build?.(); + if (!referenceElement) return; - if (flags.check(Flag.Hovering, value)) { - const message = message$.value; - if (!message) return; + const flavour = flavour$.value; + const placement = placement$.value; + const sideOptions = sideOptions$.value; - const { element } = message; - - virtualEl = element; - placement = 'top'; - } else if (flags.check(Flag.Block, value)) { - const [ok, { selectedBlocks }] = context.chain - .pipe(getBlockSelectionsCommand) - .pipe(getSelectedBlocksCommand, { types: ['block'] }) - .run(); - - if (!ok || !selectedBlocks?.length) return; - - virtualEl = { - getBoundingClientRect: () => { - const rects = selectedBlocks.map(e => e.getBoundingClientRect()); - const bounds = getCommonBound(rects.map(Bound.fromDOMRect)); - if (!bounds) return rects[0]; - return new DOMRect(bounds.x, bounds.y, bounds.w, bounds.h); - }, - getClientRects: () => - selectedBlocks.map(e => e.getBoundingClientRect()), - }; - } else { - const range = range$.value; - if (!range) return; - - virtualEl = { - getBoundingClientRect: () => range.getBoundingClientRect(), - getClientRects: () => - Array.from(range.getClientRects()).filter(rect => - Math.round(rect.width) - ), - }; + if (abortController.signal.aborted) { + abortController = new AbortController(); } + const signal = abortController.signal; - if (!virtualEl) return; + const cleanup = autoUpdatePosition( + signal, + toolbar, + referenceElement, + flavour, + placement, + sideOptions + ); - return autoUpdatePosition(virtualEl, toolbar, placement); + signal.addEventListener('abort', cleanup, { once: true }); + + return () => { + if (signal.aborted) return; + abortController.abort(); + }; }) ); } diff --git a/blocksuite/affine/widgets/widget-toolbar/src/utils.ts b/blocksuite/affine/widgets/widget-toolbar/src/utils.ts index b99f59fa44..75029f50be 100644 --- a/blocksuite/affine/widgets/widget-toolbar/src/utils.ts +++ b/blocksuite/affine/widgets/widget-toolbar/src/utils.ts @@ -7,15 +7,14 @@ import { type ToolbarAction, type ToolbarActions, type ToolbarContext, - type ToolbarModuleConfig, } from '@blocksuite/affine-shared/services'; -import { BlockSelection } from '@blocksuite/block-std'; import { nextTick } from '@blocksuite/global/utils'; import { MoreVerticalIcon } from '@blocksuite/icons/lit'; import type { AutoUpdateOptions, Placement, ReferenceElement, + SideObject, } from '@floating-ui/dom'; import { autoUpdate, @@ -38,54 +37,83 @@ import orderBy from 'lodash-es/orderBy'; import partition from 'lodash-es/partition'; import toPairs from 'lodash-es/toPairs'; +export const sideMap = new Map([ + // includes frame element + ['affine:surface:frame', { top: 28 }], + // includes group element + ['affine:surface:group', { top: 20 }], + // only one shape element + ['affine:surface:shape', { top: 26, bottom: -26 }], +]); + export function autoUpdatePosition( - referenceElement: ReferenceElement, + signal: AbortSignal, toolbar: EditorToolbar, - placement: Placement = 'top-start', + referenceElement: ReferenceElement, + flavour: string, + placement: Placement, + sideOptions: Partial | null, options: AutoUpdateOptions = { elementResize: false, animationFrame: true } ) { - const abortController = new AbortController(); - const signal = abortController.signal; + const isInline = flavour === 'affine:note'; + const hasSurfaceScope = flavour.includes('surface'); + const offsetTop = sideOptions?.top ?? 0; + const offsetBottom = sideOptions?.bottom ?? 0; + const offsetY = offsetTop + (hasSurfaceScope ? 2 : 0); + const config = { + placement, + middleware: [ + offset(10 + offsetY), + isInline ? inline() : undefined, + shift(state => ({ + padding: { + top: 10, + right: 10, + bottom: 150, + left: 10, + }, + crossAxis: state.placement.includes('bottom'), + limiter: limitShift(), + })), + flip({ padding: 10 }), + hide(), + ], + }; const update = async () => { await Promise.race([ new Promise(resolve => { - const listener = () => resolve(signal.reason); - signal.addEventListener('abort', listener, { once: true }); + signal.addEventListener('abort', () => resolve(signal.reason), { + once: true, + }); if (signal.aborted) return; - signal.removeEventListener('abort', listener); resolve(null); }), - toolbar.updateComplete.then(nextTick), + isInline ? toolbar.updateComplete.then(nextTick) : toolbar.updateComplete, ]); if (signal.aborted) return; - const { x, y } = await computePosition(referenceElement, toolbar, { - placement, - middleware: [ - offset(10), - inline(), - shift(state => ({ - padding: { - top: 10, - right: 10, - bottom: 150, - left: 10, - }, - crossAxis: state.placement.includes('bottom'), - limiter: limitShift(), - })), - flip({ padding: 10 }), - hide(), - ], - }); + const result = await computePosition(referenceElement, toolbar, config); + + const { x, middlewareData, placement: currentPlacement } = result; + const y = + result.y - + (currentPlacement.includes('top') ? 0 : offsetTop + offsetBottom); toolbar.style.transform = `translate3d(${x}px, ${y}px, 0)`; + + if (toolbar.dataset.open) { + if (middlewareData.hide?.referenceHidden) { + delete toolbar.dataset.open; + } + } else { + toolbar.dataset.open = 'true'; + } }; - const cleanup = autoUpdate( + return autoUpdate( referenceElement, toolbar, () => { @@ -93,15 +121,37 @@ export function autoUpdatePosition( }, options ); - - return () => { - cleanup(); - if (signal.aborted) return; - abortController.abort(); - }; } -function group(actions: ToolbarAction[]) { +export function combine(actions: ToolbarActions, context: ToolbarContext) { + const grouped = group(actions); + + const generated = grouped.map(action => { + const newAction = { + ...action, + placement: action.placement ?? ActionPlacement.Normal, + }; + + if ('generate' in action && typeof action.generate === 'function') { + // TODO(@fundon): should delete `generate` fn + return { + ...newAction, + ...action.generate(context), + }; + } + + return newAction; + }); + + const filtered = generated.filter(action => { + if (typeof action.when === 'function') return action.when(context); + return action.when ?? true; + }); + + return filtered; +} + +function group(actions: ToolbarAction[]): ToolbarAction[] { const grouped = groupBy(actions, a => a.id); const paired = toPairs(grouped).map(([_, items]) => { @@ -114,28 +164,6 @@ function group(actions: ToolbarAction[]) { return paired; } -export function combine(actions: ToolbarActions, context: ToolbarContext) { - const grouped = group(actions); - - const generated = grouped.map(action => { - if ('generate' in action && action.generate) { - // TODO(@fundon): should delete `generate` fn - return { - ...action, - ...action.generate(context), - }; - } - return action; - }); - - const filtered = generated.filter(action => { - if (typeof action.when === 'function') return action.when(context); - return action.when ?? true; - }); - - return filtered; -} - const merge = (a: any, b: any) => mergeWith(a, b, (obj, src) => Array.isArray(obj) ? group(obj.concat(src)) : src @@ -155,27 +183,23 @@ export function renderToolbar( context: ToolbarContext, flavour: string ) { + const hasSurfaceScope = flavour.includes('surface'); const toolbarRegistry = context.toolbarRegistry; - const module = toolbarRegistry.modules.get(flavour); - if (!module) return; - const customModule = toolbarRegistry.modules.get(`custom:${flavour}`); - const customWildcardModule = toolbarRegistry.modules.get(`custom:affine:*`); - const config = module.config satisfies ToolbarModuleConfig; - const customConfig = (customModule?.config ?? { - actions: [], - }) satisfies ToolbarModuleConfig; - const customWildcardConfig = (customWildcardModule?.config ?? { - actions: [], - }) satisfies ToolbarModuleConfig; - const combined = combine( - [ - ...config.actions, - ...customConfig.actions, - ...customWildcardConfig.actions, - ], - context - ); + const actions = [ + flavour, + `custom:${flavour}`, + hasSurfaceScope ? ['affine:surface:*', 'custom:affine:surface:*'] : [], + 'affine:*', + 'custom:affine:*', + ] + .flat() + .map(key => toolbarRegistry.modules.get(key)) + .filter(module => !!module) + .map(module => module.config.actions) + .flat(); + + const combined = combine(actions, context); const ordered = orderBy( combined, @@ -194,40 +218,36 @@ export function renderToolbar( context, renderMenuActionItem ); - if (moreMenuItems.length) { - // TODO(@fundon): edgeless case needs to be considered - const key = `${flavour}:${context.getCurrentModelBy(BlockSelection)?.id}`; + // if (moreMenuItems.length) { + // TODO(@fundon): edgeless case needs to be considered + const key = `${context.getCurrentModel()?.id ?? context.getCurrentElement()?.id}`; - primaryActionGroup.push({ - id: 'more', - content: html`${keyed( - key, - html` - - ${MoreVerticalIcon()} - - `} - > -
- ${join(moreMenuItems, () => - renderToolbarSeparator('horizontal') - )} -
-
- ` - )}`, - }); - } + primaryActionGroup.push({ + id: 'more', + content: html`${keyed( + `${flavour}:${key}`, + html` + + ${MoreVerticalIcon()} + + `} + > +
+ ${join(moreMenuItems, renderToolbarSeparator('horizontal'))} +
+
+ ` + )}`, + }); + // } } render( - join(renderActions(primaryActionGroup, context), () => - renderToolbarSeparator() - ), + join(renderActions(primaryActionGroup, context), renderToolbarSeparator()), toolbar ); } diff --git a/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts index dd884604e9..a56d93218a 100644 --- a/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts +++ b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts @@ -217,14 +217,18 @@ function createToolbarMoreMenuConfigV2(baseUrl?: string) { id: 'copy-as-image', label: 'Copy as Image', icon: CopyAsImgaeIcon(), - when: ({ isEdgelessMode, gfx }) => - isEdgelessMode && gfx.selection.selectedElements.length > 0, + when: ({ isEdgelessMode, gfx, flags }) => + !flags.isHovering() && + isEdgelessMode && + gfx.selection.selectedElements.length > 0, }, { id: 'copy-link-to-block', label: 'Copy link to block', icon: LinkIcon(), - when: ({ isPageMode, selection, gfx }) => { + when: ({ isPageMode, selection, gfx, flags }) => { + if (flags.isHovering()) return false; + const items = selection .getGroup('note') .filter(item => @@ -394,7 +398,7 @@ function createToolbarMoreMenuConfigV2(baseUrl?: string) { } function createExternalLinkableToolbarConfig( - kclass: + klass: | typeof BookmarkBlockComponent | typeof EmbedFigmaBlockComponent | typeof EmbedGithubBlockComponent @@ -411,10 +415,7 @@ function createExternalLinkableToolbarConfig( tooltip: 'Copy link', icon: CopyIcon(), run(ctx) { - const model = ctx.getCurrentBlockComponentBy( - BlockSelection, - kclass - )?.model; + const model = ctx.getCurrentBlockComponentBy(klass)?.model; if (!model) return; const { url } = model.props; @@ -439,10 +440,7 @@ function createExternalLinkableToolbarConfig( tooltip: 'Edit', icon: EditIcon(), run(ctx) { - const component = ctx.getCurrentBlockComponentBy( - BlockSelection, - kclass - ); + const component = ctx.getCurrentBlockComponentBy(klass); if (!component) return; ctx.hide(); @@ -516,7 +514,7 @@ function createOpenDocActionGroup( id: 'A.open-doc', actions: openDocActions, content(ctx) { - const component = ctx.getCurrentBlockComponentBy(BlockSelection, klass); + const component = ctx.getCurrentBlockComponentBy(klass); if (!component) return null; const actions = this.actions @@ -624,7 +622,6 @@ const embedLinkedDocToolbarConfig = { icon: EditIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedLinkedDocBlockComponent ); if (!component) return; @@ -717,7 +714,6 @@ const embedSyncedDocToolbarConfig = { icon: EditIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedSyncedDocBlockComponent ); if (!component) return; @@ -922,7 +918,6 @@ const embedIframeToolbarConfig = { icon: CopyIcon(), run(ctx) { const model = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedIframeBlockComponent )?.model; if (!model) return; @@ -950,7 +945,6 @@ const embedIframeToolbarConfig = { icon: EditIcon(), run(ctx) { const component = ctx.getCurrentBlockComponentBy( - BlockSelection, EmbedIframeBlockComponent ); if (!component) return; diff --git a/tests/blocksuite/e2e/format-bar.spec.ts b/tests/blocksuite/e2e/format-bar.spec.ts index 28dfdfefec..76bcd9ceb4 100644 --- a/tests/blocksuite/e2e/format-bar.spec.ts +++ b/tests/blocksuite/e2e/format-bar.spec.ts @@ -8,6 +8,7 @@ import { dragBetweenIndices, enterPlaygroundRoom, focusRichText, + focusRichTextEnd, focusTitle, getBoundingBox, getEditorHostLocator, @@ -330,13 +331,22 @@ test('should format quick bar be able to link text', async ({ const linkPopoverInput = page.locator('.affine-link-popover-input'); await expect(linkPopoverInput).toBeVisible(); - await type(page, 'https://www.example.com'); + const url = 'https://www.example.com'; + + await type(page, url); await pressEnter(page); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_init.json` ); + const linkLocator = page.locator('affine-link a'); + await expect(linkLocator).toHaveAttribute('href', url); + + await focusRichTextEnd(page); + + await dragBetweenIndices(page, [1, 3], [1, 0]); + // The link button should be active after click await expect(linkBtn).toHaveAttribute('active', ''); await linkBtn.click();