From 8b995ea4206176acf77a01688cf03ef28d8aa14d Mon Sep 17 00:00:00 2001 From: fundon Date: Thu, 20 Mar 2025 02:08:21 +0000 Subject: [PATCH] chore(editor): remove edgeless element toolbar (#10900) --- .../blocks/block-note/src/configs/toolbar.ts | 19 +- .../src/edgeless/configs/toolbar/alignment.ts | 1 + .../src/edgeless/configs/toolbar/connector.ts | 3 +- .../src/edgeless/configs/toolbar/frame.ts | 1 + .../src/edgeless/configs/toolbar/group.ts | 1 + .../src/edgeless/configs/toolbar/misc.ts | 8 +- .../src/edgeless/configs/toolbar/more.ts | 8 +- .../configs/toolbar}/render-linked-doc.ts | 0 .../src/edgeless/configs/toolbar/shape.ts | 1 + .../src/edgeless/configs/toolbar/utils.ts | 1 + .../src/edgeless/edgeless-root-spec.ts | 7 - .../affine/blocks/block-root/src/effects.ts | 2 - .../affine/blocks/block-root/src/types.ts | 2 - .../element-toolbar/add-frame-button.ts | 65 -- .../element-toolbar/add-group-button.ts | 56 -- .../widgets/element-toolbar/align-button.ts | 350 ------- .../change-attachment-button.ts | 159 --- .../element-toolbar/change-brush-button.ts | 151 --- .../change-connector-button.ts | 604 ------------ .../change-edgeless-text-button.ts | 19 - .../change-embed-card-button.ts | 912 ------------------ .../element-toolbar/change-frame-button.ts | 215 ----- .../element-toolbar/change-group-button.ts | 131 --- .../element-toolbar/change-image-button.ts | 87 -- .../element-toolbar/change-mindmap-button.ts | 299 ------ .../element-toolbar/change-note-button.ts | 576 ----------- .../element-toolbar/change-shape-button.ts | 421 -------- .../element-toolbar/change-text-button.ts | 19 - .../element-toolbar/change-text-menu.ts | 474 --------- .../src/widgets/element-toolbar/effects.ts | 114 --- .../src/widgets/element-toolbar/icons.ts | 6 - .../src/widgets/element-toolbar/index.ts | 483 ---------- .../widgets/element-toolbar/lock-button.ts | 158 --- .../element-toolbar/more-menu/button.ts | 50 - .../element-toolbar/more-menu/config.ts | 459 --------- .../element-toolbar/more-menu/context.ts | 152 --- .../release-from-group-button.ts | 54 -- .../src/widgets/element-toolbar/styles.css.ts | 24 - .../blocks/block-root/src/widgets/index.ts | 4 - .../color-picker.ts | 1 + .../src/size-dropdown-menu/dropdown-menu.ts | 24 +- .../src/services/toolbar-service/action.ts | 1 + .../widgets/widget-toolbar/src/toolbar.ts | 12 +- .../widgets/widget-toolbar/src/utils.ts | 6 +- .../blocksuite/ai/actions/edgeless-handler.ts | 8 +- .../ai/actions/edgeless-response.ts | 20 +- .../blocksuite/ai/entries/edgeless/index.ts | 62 -- .../ai/extensions/ai-edgeless-root.ts | 10 +- .../extensions/editor-config/toolbar/index.ts | 4 +- .../blocksuite/clipboard/clipboard.spec.ts | 9 +- .../e2e/blocksuite/edgeless/embed.spec.ts | 4 +- .../e2e/blocksuite/edgeless/frame.spec.ts | 6 +- .../e2e/blocksuite/edgeless/note.spec.ts | 26 +- .../e2e/blocksuite/edgeless/shape.spec.ts | 8 +- .../blocksuite/outline/outline-panel.spec.ts | 16 +- tests/affine-local/e2e/links.spec.ts | 2 +- tests/affine-local/e2e/peek-view.spec.ts | 6 +- .../e2e/edgeless/auto-complete.spec.ts | 6 +- .../e2e/edgeless/color-picker.spec.ts | 83 +- .../e2e/edgeless/connector/connector.spec.ts | 4 +- .../e2e/edgeless/element-toolbar.spec.ts | 17 +- .../e2e/edgeless/frame/clipboard.spec.ts | 9 +- .../e2e/edgeless/frame/frame.spec.ts | 4 +- .../e2e/edgeless/group/group.spec.ts | 12 +- tests/blocksuite/e2e/edgeless/lock.spec.ts | 8 +- .../blocksuite/e2e/edgeless/note/note.spec.ts | 2 +- .../e2e/edgeless/note/scale.spec.ts | 6 +- tests/blocksuite/e2e/edgeless/shape.spec.ts | 31 +- .../blocksuite/e2e/utils/actions/edgeless.ts | 267 +++-- tests/blocksuite/e2e/utils/asserts.ts | 2 +- tests/kit/src/utils/editor.ts | 7 - 71 files changed, 330 insertions(+), 6449 deletions(-) rename blocksuite/affine/blocks/block-root/src/{widgets/element-toolbar/more-menu => edgeless/configs/toolbar}/render-linked-doc.ts (100%) delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/add-frame-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/add-group-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/align-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-attachment-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-brush-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-connector-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-edgeless-text-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-embed-card-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-frame-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-group-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-image-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-mindmap-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-note-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-shape-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-text-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-text-menu.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/effects.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/icons.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/index.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/lock-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/config.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/context.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/release-from-group-button.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/styles.css.ts diff --git a/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts b/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts index 53700cc490..b407b3f8a4 100644 --- a/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts @@ -351,6 +351,7 @@ const builtinSurfaceToolbarConfig = { }, { id: 'e.slicer', + label: 'Slicer', icon: ScissorsIcon(), tooltip: html` `${value}%`; - return html`${keyed( - firstModel, - html`` - )}`; + return html``; }, }, ], diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/alignment.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/alignment.ts index 0d22c87b6b..e3aed17861 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/alignment.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/alignment.ts @@ -266,6 +266,7 @@ export function renderAlignmentMenu( ) { return html` ctx.getSurfaceModelsByType(FrameBlockModel).length === 1, 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 index 2f40e3c475..ec63931b17 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/group.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/group.ts @@ -20,6 +20,7 @@ export const builtinGroupToolbarConfig = { { id: 'a.insert-into-page', label: 'Insert into Page', + showLabel: true, tooltip: 'Insert into Page', icon: PageIcon(), when: ctx => ctx.getSurfaceModelsByType(GroupElementModel).length === 1, 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 index 4066fc53a1..03d0a87e39 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/misc.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/misc.ts @@ -66,6 +66,7 @@ export const builtinMiscToolbarConfig = { placement: ActionPlacement.Start, id: 'b.add-frame', label: 'Frame', + showLabel: true, tooltip: 'Frame', icon: FrameIcon(), when(ctx) { @@ -106,6 +107,7 @@ export const builtinMiscToolbarConfig = { placement: ActionPlacement.Start, id: 'c.add-group', label: 'Group', + showLabel: true, tooltip: 'Group', icon: GroupingIcon(), when(ctx) { @@ -166,6 +168,7 @@ export const builtinMiscToolbarConfig = { { placement: ActionPlacement.End, id: 'a.draw-connector', + label: 'Draw connector', tooltip: 'Draw connector', icon: ConnectorCIcon(), when(ctx) { @@ -177,7 +180,7 @@ export const builtinMiscToolbarConfig = { const models = ctx.getSurfaceModels(); if (!models.length) return null; - const { id, label, icon, tooltip } = this; + const { label, icon, tooltip } = this; const quickConnect = (e: MouseEvent) => { e.stopPropagation(); @@ -194,7 +197,7 @@ export const builtinMiscToolbarConfig = { return html` ({ }: Menu) { return html` { - const frame = this.edgeless.service.frame.createFrameOnSelected(); - if (!frame) return; - this.edgeless.std - .getOptional(TelemetryProvider) - ?.track('CanvasElementAdded', { - control: 'context-menu', - page: 'whiteboard editor', - module: 'toolbar', - segment: 'toolbar', - type: 'frame', - }); - this.edgeless.surface.fitToViewport(Bound.deserialize(frame.xywh)); - }; - - protected override render() { - return html` - - ${FrameIcon()}Frame - - `; - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; -} - -export function renderAddFrameButton( - edgeless: EdgelessRootBlockComponent, - elements: GfxModel[] -) { - if (elements.length < 2) return nothing; - if (elements.some(e => e.group instanceof MindmapElementModel)) - return nothing; - - return html` - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/add-group-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/add-group-button.ts deleted file mode 100644 index ef6456428c..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/add-group-button.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - GroupElementModel, - MindmapElementModel, -} from '@blocksuite/affine-model'; -import type { GfxModel } from '@blocksuite/block-std/gfx'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { GroupingIcon } from '@blocksuite/icons/lit'; -import { css, html, LitElement, nothing } from 'lit'; -import { property } from 'lit/decorators.js'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; - -export class EdgelessAddGroupButton extends WithDisposable(LitElement) { - static override styles = css` - .label { - padding-left: 4px; - } - `; - - private readonly _createGroup = () => { - this.edgeless.service.createGroupFromSelected(); - }; - - protected override render() { - return html` - - ${GroupingIcon()}Group - - `; - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; -} - -export function renderAddGroupButton( - edgeless: EdgelessRootBlockComponent, - elements: GfxModel[] -) { - if (elements.length < 2) return nothing; - if (elements[0] instanceof GroupElementModel) return nothing; - if (elements.some(e => e.group instanceof MindmapElementModel)) - return nothing; - - return html` - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/align-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/align-button.ts deleted file mode 100644 index 2b241f8374..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/align-button.ts +++ /dev/null @@ -1,350 +0,0 @@ -import { - autoArrangeElementsCommand, - autoResizeElementsCommand, - EdgelessCRUDIdentifier, - updateXYWH, -} from '@blocksuite/affine-block-surface'; -import { MindmapElementModel } from '@blocksuite/affine-model'; -import type { GfxModel } from '@blocksuite/block-std/gfx'; -import { Bound } from '@blocksuite/global/gfx'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { - AlignBottomIcon, - AlignHorizontalCenterIcon, - AlignLeftIcon, - AlignRightIcon, - AlignTopIcon, - AlignVerticalCenterIcon, - AutoTidyUpIcon, - DistributeHorizontalIcon, - DistributeVerticalIcon, - ResizeTidyUpIcon, -} from '@blocksuite/icons/lit'; -import { css, html, LitElement, nothing, type TemplateResult } from 'lit'; -import { property } from 'lit/decorators.js'; -import { repeat } from 'lit/directives/repeat.js'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import { SmallArrowDownIcon } from './icons.js'; - -const enum Alignment { - AutoArrange = 'Auto arrange', - AutoResize = 'Resize & Align', - Bottom = 'Align bottom', - DistributeHorizontally = 'Distribute horizontally', - DistributeVertically = 'Distribute vertically', - Horizontally = 'Align horizontally', - Left = 'Align left', - Right = 'Align right', - Top = 'Align top', - Vertically = 'Align vertically', -} - -interface AlignmentIcon { - name: Alignment; - content: TemplateResult<1>; -} - -const iconSize = { width: '20px', height: '20px' }; -const HORIZONTAL_ALIGNMENT: AlignmentIcon[] = [ - { - name: Alignment.Left, - content: AlignLeftIcon(iconSize), - }, - { - name: Alignment.Horizontally, - content: AlignHorizontalCenterIcon(iconSize), - }, - { - name: Alignment.Right, - content: AlignRightIcon(iconSize), - }, - { - name: Alignment.DistributeHorizontally, - content: DistributeHorizontalIcon(iconSize), - }, -]; - -const VERTICAL_ALIGNMENT: AlignmentIcon[] = [ - { - name: Alignment.Top, - content: AlignTopIcon(iconSize), - }, - { - name: Alignment.Vertically, - content: AlignVerticalCenterIcon(iconSize), - }, - { - name: Alignment.Bottom, - content: AlignBottomIcon(iconSize), - }, - { - name: Alignment.DistributeVertically, - content: DistributeVerticalIcon(iconSize), - }, -]; - -const AUTO_ALIGNMENT: AlignmentIcon[] = [ - { - name: Alignment.AutoArrange, - content: AutoTidyUpIcon({ width: '20px', height: '20px' }), - }, - { - name: Alignment.AutoResize, - content: ResizeTidyUpIcon({ width: '20px', height: '20px' }), - }, -]; - -export class EdgelessAlignButton extends WithDisposable(LitElement) { - static override styles = css` - .align-menu-content { - max-width: 120px; - flex-wrap: wrap; - padding: 8px 2px; - } - .align-menu-separator { - width: 120px; - height: 1px; - background-color: var(--affine-background-tertiary-color); - } - `; - - private get elements() { - return this.edgeless.service.selection.selectedElements; - } - - private _align(type: Alignment) { - switch (type) { - case Alignment.Left: - this._alignLeft(); - break; - case Alignment.Horizontally: - this._alignHorizontally(); - break; - case Alignment.Right: - this._alignRight(); - break; - case Alignment.DistributeHorizontally: - this._alignDistributeHorizontally(); - break; - case Alignment.Top: - this._alignTop(); - break; - case Alignment.Vertically: - this._alignVertically(); - break; - case Alignment.Bottom: - this._alignBottom(); - break; - case Alignment.DistributeVertically: - this._alignDistributeVertically(); - break; - case Alignment.AutoArrange: - this.edgeless.std.command.exec(autoArrangeElementsCommand); - break; - case Alignment.AutoResize: - this.edgeless.std.command.exec(autoResizeElementsCommand); - break; - } - } - - private _alignBottom() { - const { elements } = this; - const bounds = elements.map(a => a.elementBound); - const bottom = Math.max(...bounds.map(b => b.maxY)); - - elements.forEach((ele, index) => { - const elementBound = bounds[index]; - const bound = Bound.deserialize(ele.xywh); - const offset = bound.maxY - elementBound.maxY; - bound.y = bottom - bound.h + offset; - this._updateXYWH(ele, bound); - }); - } - - private _alignDistributeHorizontally() { - const { elements } = this; - - elements.sort((a, b) => a.elementBound.minX - b.elementBound.minX); - const bounds = elements.map(a => a.elementBound); - const left = bounds[0].minX; - const right = bounds[bounds.length - 1].maxX; - - const totalWidth = right - left; - const totalGap = - totalWidth - elements.reduce((prev, ele) => prev + ele.elementBound.w, 0); - const gap = totalGap / (elements.length - 1); - let next = bounds[0].maxX + gap; - for (let i = 1; i < elements.length - 1; i++) { - const bound = Bound.deserialize(elements[i].xywh); - bound.x = next + bounds[i].w / 2 - bound.w / 2; - next += gap + bounds[i].w; - this._updateXYWH(elements[i], bound); - } - } - - private _alignDistributeVertically() { - const { elements } = this; - - elements.sort((a, b) => a.elementBound.minY - b.elementBound.minY); - const bounds = elements.map(a => a.elementBound); - const top = bounds[0].minY; - const bottom = bounds[bounds.length - 1].maxY; - - const totalHeight = bottom - top; - const totalGap = - totalHeight - - elements.reduce((prev, ele) => prev + ele.elementBound.h, 0); - const gap = totalGap / (elements.length - 1); - let next = bounds[0].maxY + gap; - for (let i = 1; i < elements.length - 1; i++) { - const bound = Bound.deserialize(elements[i].xywh); - bound.y = next + bounds[i].h / 2 - bound.h / 2; - next += gap + bounds[i].h; - this._updateXYWH(elements[i], bound); - } - } - - private _alignHorizontally() { - const { elements } = this; - const bounds = elements.map(a => a.elementBound); - const left = Math.min(...bounds.map(b => b.minX)); - const right = Math.max(...bounds.map(b => b.maxX)); - const centerX = (left + right) / 2; - - elements.forEach(ele => { - const bound = Bound.deserialize(ele.xywh); - bound.x = centerX - bound.w / 2; - this._updateXYWH(ele, bound); - }); - } - - private _alignLeft() { - const { elements } = this; - const bounds = elements.map(a => a.elementBound); - const left = Math.min(...bounds.map(b => b.minX)); - - elements.forEach((ele, index) => { - const elementBound = bounds[index]; - const bound = Bound.deserialize(ele.xywh); - const offset = bound.minX - elementBound.minX; - bound.x = left + offset; - this._updateXYWH(ele, bound); - }); - } - - private _alignRight() { - const { elements } = this; - const bounds = elements.map(a => a.elementBound); - const right = Math.max(...bounds.map(b => b.maxX)); - - elements.forEach((ele, index) => { - const elementBound = bounds[index]; - const bound = Bound.deserialize(ele.xywh); - const offset = bound.maxX - elementBound.maxX; - bound.x = right - bound.w + offset; - this._updateXYWH(ele, bound); - }); - } - - private _alignTop() { - const { elements } = this; - const bounds = elements.map(a => a.elementBound); - const top = Math.min(...bounds.map(b => b.minY)); - - elements.forEach((ele, index) => { - const elementBound = bounds[index]; - const bound = Bound.deserialize(ele.xywh); - const offset = bound.minY - elementBound.minY; - bound.y = top + offset; - this._updateXYWH(ele, bound); - }); - } - - private _alignVertically() { - const { elements } = this; - const bounds = elements.map(a => a.elementBound); - const top = Math.min(...bounds.map(b => b.minY)); - const bottom = Math.max(...bounds.map(b => b.maxY)); - const centerY = (top + bottom) / 2; - - elements.forEach(ele => { - const bound = Bound.deserialize(ele.xywh); - bound.y = centerY - bound.h / 2; - this._updateXYWH(ele, bound); - }); - } - - private _updateXYWH(ele: GfxModel, bound: Bound) { - const { updateElement } = this.edgeless.std.get(EdgelessCRUDIdentifier); - const { updateBlock } = this.edgeless.doc; - updateXYWH(ele, bound, updateElement, updateBlock); - } - - private renderIcons(icons: AlignmentIcon[]) { - return html` - ${repeat( - icons, - (item, index) => item.name + index, - ({ name, content }) => { - return html` - this._align(name)} - > - ${content} - - `; - } - )} - `; - } - - override firstUpdated() { - this._disposables.add( - this.edgeless.service.selection.slots.updated.subscribe(() => - this.requestUpdate() - ) - ); - } - - override render() { - return html` - - ${AlignLeftIcon(iconSize)}${SmallArrowDownIcon} - - `} - > -
- ${this.renderIcons(HORIZONTAL_ALIGNMENT)} - ${this.renderIcons(VERTICAL_ALIGNMENT)} -
- ${this.renderIcons(AUTO_ALIGNMENT)} -
-
- `; - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; -} - -export function renderAlignButton( - edgeless: EdgelessRootBlockComponent, - elements: GfxModel[] -) { - if (elements.length < 2) return nothing; - if (elements.some(e => e.group instanceof MindmapElementModel)) - return nothing; - - return html` - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-attachment-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-attachment-button.ts deleted file mode 100644 index 252336b490..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-attachment-button.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { - type AttachmentBlockComponent, - attachmentViewDropdownMenu, -} from '@blocksuite/affine-block-attachment'; -import { getEmbedCardIcons } from '@blocksuite/affine-block-embed'; -import { - CaptionIcon, - DownloadIcon, - PaletteIcon, -} from '@blocksuite/affine-components/icons'; -import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar'; -import type { - AttachmentBlockModel, - EmbedCardStyle, -} from '@blocksuite/affine-model'; -import { - EMBED_CARD_HEIGHT, - EMBED_CARD_WIDTH, -} from '@blocksuite/affine-shared/consts'; -import { - ThemeProvider, - ToolbarContext, -} from '@blocksuite/affine-shared/services'; -import { Bound } from '@blocksuite/global/gfx'; -import { WithDisposable } from '@blocksuite/global/lit'; -import type { TemplateResult } from 'lit'; -import { html, LitElement, nothing } from 'lit'; -import { property } from 'lit/decorators.js'; -import { join } from 'lit/directives/join.js'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; - -export class EdgelessChangeAttachmentButton extends WithDisposable(LitElement) { - private readonly _download = () => { - this._block?.download(); - }; - - private readonly _setCardStyle = (style: EmbedCardStyle) => { - const bounds = Bound.deserialize(this.model.xywh); - bounds.w = EMBED_CARD_WIDTH[style]; - bounds.h = EMBED_CARD_HEIGHT[style]; - const xywh = bounds.serialize(); - this.model.doc.updateBlock(this.model, { style, xywh }); - }; - - private readonly _showCaption = () => { - this._block?.captionEditor?.show(); - }; - - private get _block() { - const block = this.std.view.getBlock(this.model.id); - if (!block) return null; - return block as AttachmentBlockComponent; - } - - private get _doc() { - return this.model.doc; - } - - private get _getCardStyleOptions(): { - style: EmbedCardStyle; - Icon: TemplateResult<1>; - tooltip: string; - }[] { - const theme = this.std.get(ThemeProvider).theme; - const { EmbedCardListIcon, EmbedCardCubeIcon } = getEmbedCardIcons(theme); - return [ - { - style: 'horizontalThin', - Icon: EmbedCardListIcon, - tooltip: 'Horizontal style', - }, - { - style: 'cubeThick', - Icon: EmbedCardCubeIcon, - tooltip: 'Vertical style', - }, - ]; - } - - get std() { - return this.edgeless.std; - } - - override render() { - return join( - [ - this.model.props.style === 'pdf' - ? null - : html` - - ${PaletteIcon} - - `} - > - - - - `, - - // TODO(@fundon): should remove it when refactoring the element toolbar - attachmentViewDropdownMenu.content(new ToolbarContext(this.std)), - - html` - - ${DownloadIcon} - - `, - html` - - ${CaptionIcon} - - `, - ].filter(button => button !== null), - renderToolbarSeparator - ); - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor model!: AttachmentBlockModel; -} - -export function renderAttachmentButton( - edgeless: EdgelessRootBlockComponent, - attachments?: AttachmentBlockModel[] -) { - if (attachments?.length !== 1) return nothing; - - return html` - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-brush-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-brush-button.ts deleted file mode 100644 index 9b628a831f..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-brush-button.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface'; -import type { - EdgelessColorPickerButton, - PickColorEvent, -} from '@blocksuite/affine-components/color-picker'; -import { packColor } from '@blocksuite/affine-components/color-picker'; -import type { - BrushElementModel, - BrushProps, - ColorScheme, -} from '@blocksuite/affine-model'; -import { - DefaultTheme, - LineWidth, - resolveColor, -} from '@blocksuite/affine-model'; -import { FeatureFlagService } from '@blocksuite/affine-shared/services'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { html, LitElement, nothing } from 'lit'; -import { property, query } from 'lit/decorators.js'; -import countBy from 'lodash-es/countBy'; -import maxBy from 'lodash-es/maxBy'; - -import type { LineWidthEvent } from '../../edgeless/components/panel/line-width-panel.js'; -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; - -function getMostCommonColor( - elements: BrushElementModel[], - colorScheme: ColorScheme -): string { - const colors = countBy(elements, (ele: BrushElementModel) => - resolveColor(ele.color, colorScheme) - ); - const max = maxBy(Object.entries(colors), ([_k, count]) => count); - return max - ? (max[0] as string) - : resolveColor(DefaultTheme.black, colorScheme); -} - -function getMostCommonSize(elements: BrushElementModel[]): LineWidth { - const sizes = countBy(elements, ele => ele.lineWidth); - const max = maxBy(Object.entries(sizes), ([_k, count]) => count); - return max ? (Number(max[0]) as LineWidth) : LineWidth.Four; -} - -function notEqual(key: K, value: BrushProps[K]) { - return (element: BrushElementModel) => element[key] !== value; -} - -export class EdgelessChangeBrushButton extends WithDisposable(LitElement) { - private readonly _setLineWidth = ({ detail: lineWidth }: LineWidthEvent) => { - this._setBrushProp('lineWidth', lineWidth); - }; - - pickColor = (e: PickColorEvent) => { - const field = 'color'; - - if (e.type === 'pick') { - const color = e.detail.value; - this.elements.forEach(ele => { - const props = packColor(field, color); - this.crud.updateElement(ele.id, props); - }); - return; - } - - this.elements.forEach(ele => - ele[e.type === 'start' ? 'stash' : 'pop'](field) - ); - }; - - get doc() { - return this.edgeless.doc; - } - - get service() { - return this.edgeless.service; - } - - get surface() { - return this.edgeless.surface; - } - - get crud() { - return this.edgeless.std.get(EdgelessCRUDIdentifier); - } - - private _setBrushProp( - key: K, - value: BrushProps[K] - ) { - this.doc.captureSync(); - this.elements - .filter(notEqual(key, value)) - .forEach(element => - this.crud.updateElement(element.id, { [key]: value }) - ); - } - - override render() { - const colorScheme = this.edgeless.surface.renderer.getColorScheme(); - const elements = this.elements; - const selectedColor = getMostCommonColor(elements, colorScheme); - const selectedSize = getMostCommonSize(elements); - const enableCustomColor = this.edgeless.doc - .get(FeatureFlagService) - .getFlag('enable_color_picker'); - - return html` - - - - - - - - `; - } - - @query('edgeless-color-picker-button.color') - accessor colorButton!: EdgelessColorPickerButton; - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor elements: BrushElementModel[] = []; -} - -export function renderChangeBrushButton( - edgeless: EdgelessRootBlockComponent, - elements?: BrushElementModel[] -) { - if (!elements?.length) return nothing; - - return html` - - - `; -} 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 deleted file mode 100644 index 245dcb832f..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-connector-button.ts +++ /dev/null @@ -1,604 +0,0 @@ -import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface'; -import type { - EdgelessColorPickerButton, - PickColorEvent, -} from '@blocksuite/affine-components/color-picker'; -import { packColor } from '@blocksuite/affine-components/color-picker'; -import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar'; -import { - type ColorScheme, - type ConnectorElementModel, - type ConnectorElementProps, - ConnectorEndpoint, - type ConnectorLabelProps, - ConnectorMode, - DEFAULT_FRONT_ENDPOINT_STYLE, - DEFAULT_REAR_ENDPOINT_STYLE, - DefaultTheme, - LineWidth, - PointStyle, - resolveColor, - StrokeStyle, -} from '@blocksuite/affine-model'; -import { FeatureFlagService } from '@blocksuite/affine-shared/services'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { - AddTextIcon, - ConnectorCIcon, - ConnectorEIcon, - ConnectorLIcon, - EndPointArrowIcon, - EndPointCircleIcon, - EndPointDiamondIcon, - EndPointTriangleIcon, - FlipDirectionIcon, - StartPointArrowIcon, - StartPointCircleIcon, - StartPointDiamondIcon, - StartPointIcon, - StartPointTriangleIcon, - StyleGeneralIcon, - StyleScribbleIcon, -} from '@blocksuite/icons/lit'; -import { html, LitElement, nothing, type TemplateResult } from 'lit'; -import { property, query } from 'lit/decorators.js'; -import { choose } from 'lit/directives/choose.js'; -import { join } from 'lit/directives/join.js'; -import { repeat } from 'lit/directives/repeat.js'; -import { styleMap } from 'lit/directives/style-map.js'; -import countBy from 'lodash-es/countBy'; -import maxBy from 'lodash-es/maxBy'; - -import { - type LineStyleEvent, - LineStylesPanel, -} from '../../edgeless/components/panel/line-styles-panel.js'; -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import { mountConnectorLabelEditor } from '../../edgeless/utils/text.js'; -import { SmallArrowDownIcon } from './icons.js'; - -function getMostCommonColor( - elements: ConnectorElementModel[], - colorScheme: ColorScheme -): string { - const colors = countBy(elements, (ele: ConnectorElementModel) => - resolveColor(ele.stroke, colorScheme) - ); - const max = maxBy(Object.entries(colors), ([_k, count]) => count); - return max - ? (max[0] as string) - : resolveColor(DefaultTheme.connectorColor, colorScheme); -} - -function getMostCommonMode( - elements: ConnectorElementModel[] -): ConnectorMode | null { - const modes = countBy(elements, ele => ele.mode); - const max = maxBy(Object.entries(modes), ([_k, count]) => count); - return max ? (Number(max[0]) as ConnectorMode) : null; -} - -function getMostCommonLineWidth(elements: ConnectorElementModel[]): LineWidth { - const sizes = countBy(elements, ele => ele.strokeWidth); - const max = maxBy(Object.entries(sizes), ([_k, count]) => count); - return max ? (Number(max[0]) as LineWidth) : LineWidth.Four; -} - -export function getMostCommonLineStyle( - elements: ConnectorElementModel[] -): StrokeStyle { - const sizes = countBy(elements, ele => ele.strokeStyle); - const max = maxBy(Object.entries(sizes), ([_k, count]) => count); - return max ? (max[0] as StrokeStyle) : StrokeStyle.Solid; -} - -function getMostCommonRough(elements: ConnectorElementModel[]): boolean { - const { trueCount, falseCount } = elements.reduce( - (counts, ele) => { - if (ele.rough) { - counts.trueCount++; - } else { - counts.falseCount++; - } - return counts; - }, - { trueCount: 0, falseCount: 0 } - ); - - return trueCount > falseCount; -} - -function getMostCommonEndpointStyle( - elements: ConnectorElementModel[], - endpoint: ConnectorEndpoint, - fallback: PointStyle -): PointStyle { - const field = - endpoint === ConnectorEndpoint.Front - ? 'frontEndpointStyle' - : 'rearEndpointStyle'; - const modes = countBy(elements, ele => ele[field]); - const max = maxBy(Object.entries(modes), ([_k, count]) => count); - return max ? (max[0] as PointStyle) : fallback; -} - -function notEqual< - K extends keyof Omit, ->(key: K, value: ConnectorElementProps[K]) { - return (element: ConnectorElementModel) => element[key] !== value; -} - -interface EndpointStyle { - value: PointStyle; - icon: TemplateResult<1>; -} - -const iconSize = { width: '20px', height: '20px' }; -const STYLE_LIST = [ - { - name: 'General', - value: false, - icon: StyleGeneralIcon(iconSize), - }, - { - name: 'Scribbled', - value: true, - icon: StyleScribbleIcon(iconSize), - }, -] as const; - -const STYLE_CHOOSE: [boolean, () => TemplateResult<1>][] = [ - [false, () => StyleGeneralIcon(iconSize)], - [true, () => StyleScribbleIcon(iconSize)], -] as const; - -const FRONT_ENDPOINT_STYLE_LIST: EndpointStyle[] = [ - { - 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; - -const REAR_ENDPOINT_STYLE_LIST: EndpointStyle[] = [ - { - 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; - -const MODE_LIST = [ - { - name: 'Curve', - icon: ConnectorCIcon(), - value: ConnectorMode.Curve, - }, - { - name: 'Elbowed', - icon: ConnectorEIcon(), - value: ConnectorMode.Orthogonal, - }, - { - name: 'Straight', - icon: ConnectorLIcon(), - value: ConnectorMode.Straight, - }, -] as const; - -const MODE_CHOOSE: [ConnectorMode, () => TemplateResult<1>][] = [ - [ConnectorMode.Curve, () => ConnectorCIcon()], - [ConnectorMode.Orthogonal, () => ConnectorEIcon()], - [ConnectorMode.Straight, () => ConnectorLIcon()], -] as const; - -export class EdgelessChangeConnectorButton extends WithDisposable(LitElement) { - get crud() { - return this.edgeless.std.get(EdgelessCRUDIdentifier); - } - - private readonly _setConnectorStroke = ({ type, value }: LineStyleEvent) => { - if (type === 'size') { - this._setConnectorStrokeWidth(value); - return; - } - this._setConnectorStrokeStyle(value); - }; - - pickColor = (e: PickColorEvent) => { - const field = 'stroke'; - - if (e.type === 'pick') { - const color = e.detail.value; - this.elements.forEach(ele => { - const props = packColor(field, color); - this.crud.updateElement(ele.id, props); - }); - return; - } - - this.elements.forEach(ele => - ele[e.type === 'start' ? 'stash' : 'pop'](field) - ); - }; - - get doc() { - return this.edgeless.doc; - } - - get service() { - return this.edgeless.service; - } - - private _addLabel() { - mountConnectorLabelEditor(this.elements[0], this.edgeless); - } - - private _flipEndpointStyle( - frontEndpointStyle: PointStyle, - rearEndpointStyle: PointStyle - ) { - if (frontEndpointStyle === rearEndpointStyle) return; - - this.elements.forEach(element => - this.crud.updateElement(element.id, { - frontEndpointStyle: rearEndpointStyle, - rearEndpointStyle: frontEndpointStyle, - }) - ); - } - - private _getEndpointIcon(list: EndpointStyle[], style: PointStyle) { - return list.find(({ value }) => value === style)?.icon || StartPointIcon(); - } - - private _setConnectorMode(mode: ConnectorMode) { - this._setConnectorProp('mode', mode); - } - - private _setConnectorPointStyle(end: ConnectorEndpoint, style: PointStyle) { - const props = { - [end === ConnectorEndpoint.Front - ? 'frontEndpointStyle' - : 'rearEndpointStyle']: style, - }; - this.elements.forEach(element => - this.crud.updateElement(element.id, { ...props }) - ); - } - - private _setConnectorProp< - K extends keyof Omit, - >(key: K, value: ConnectorElementProps[K]) { - this.doc.captureSync(); - this.elements - .filter(notEqual(key, value)) - .forEach(element => - this.crud.updateElement(element.id, { [key]: value }) - ); - } - - private _setConnectorRough(rough: boolean) { - this._setConnectorProp('rough', rough); - } - - private _setConnectorStrokeStyle(strokeStyle: StrokeStyle) { - this._setConnectorProp('strokeStyle', strokeStyle); - } - - private _setConnectorStrokeWidth(strokeWidth: number) { - this._setConnectorProp('strokeWidth', strokeWidth); - } - - private _showAddButtonOrTextMenu() { - if (this.elements.length === 1 && !this.elements[0].text) { - return 'button'; - } - if (!this.elements.some(e => !e.text)) { - return 'menu'; - } - return 'nothing'; - } - - override render() { - const colorScheme = this.edgeless.surface.renderer.getColorScheme(); - const elements = this.elements; - const selectedColor = getMostCommonColor(elements, colorScheme); - const selectedMode = getMostCommonMode(elements); - const selectedLineSize = getMostCommonLineWidth(elements); - const selectedRough = getMostCommonRough(elements); - const selectedLineStyle = getMostCommonLineStyle(elements); - const selectedStartPointStyle = getMostCommonEndpointStyle( - elements, - ConnectorEndpoint.Front, - DEFAULT_FRONT_ENDPOINT_STYLE - ); - const selectedEndPointStyle = getMostCommonEndpointStyle( - elements, - ConnectorEndpoint.Rear, - DEFAULT_REAR_ENDPOINT_STYLE - ); - const enableCustomColor = this.edgeless.doc - .get(FeatureFlagService) - .getFlag('enable_color_picker'); - - return join( - [ - html` - -
- ${LineStylesPanel({ - selectedLineSize: selectedLineSize, - selectedLineStyle: selectedLineStyle, - onClick: this._setConnectorStroke, - })} -
- -
- `, - - html` - - ${choose(selectedRough, STYLE_CHOOSE)}${SmallArrowDownIcon} - - `} - > -
- ${repeat( - STYLE_LIST, - item => item.name, - ({ name, value, icon }) => html` - this._setConnectorRough(value)} - > - ${icon} - - ` - )} -
-
- `, - - html` - - ${this._getEndpointIcon( - FRONT_ENDPOINT_STYLE_LIST, - selectedStartPointStyle - )}${SmallArrowDownIcon} - - `} - > -
- ${repeat( - FRONT_ENDPOINT_STYLE_LIST, - item => item.value, - ({ value, icon }) => html` - - this._setConnectorPointStyle( - ConnectorEndpoint.Front, - value - )} - > - ${icon} - - ` - )} -
-
- - - this._flipEndpointStyle( - selectedStartPointStyle, - selectedEndPointStyle - )} - > - ${FlipDirectionIcon()} - - - - ${this._getEndpointIcon( - REAR_ENDPOINT_STYLE_LIST, - selectedEndPointStyle - )}${SmallArrowDownIcon} - - `} - > -
- ${repeat( - REAR_ENDPOINT_STYLE_LIST, - item => item.value, - ({ value, icon }) => html` - - this._setConnectorPointStyle( - ConnectorEndpoint.Rear, - value - )} - > - ${icon} - - ` - )} -
-
- - - ${choose(selectedMode, MODE_CHOOSE)}${SmallArrowDownIcon} - - `} - > -
- ${repeat( - MODE_LIST, - item => item.name, - ({ name, value, icon }) => html` - this._setConnectorMode(value)} - > - ${icon} - - ` - )} -
-
- `, - - choose | typeof nothing>( - this._showAddButtonOrTextMenu(), - [ - [ - 'button', - () => html` - - ${AddTextIcon()} - - `, - ], - [ - 'menu', - () => html` - - `, - ], - ['nothing', () => nothing], - ] - ), - ].filter(button => button !== nothing), - renderToolbarSeparator - ); - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor elements: ConnectorElementModel[] = []; - - @query('edgeless-color-picker-button.stroke-color') - accessor strokeColorButton!: EdgelessColorPickerButton; -} - -export function renderConnectorButton( - edgeless: EdgelessRootBlockComponent, - elements?: ConnectorElementModel[] -) { - if (!elements?.length) return nothing; - - return html` - - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-edgeless-text-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-edgeless-text-button.ts deleted file mode 100644 index 6ddb855aa7..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-edgeless-text-button.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { EdgelessTextBlockModel } from '@blocksuite/affine-model'; -import { html, nothing } from 'lit'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; - -export function renderChangeEdgelessTextButton( - edgeless: EdgelessRootBlockComponent, - elements?: EdgelessTextBlockModel[] -) { - if (!elements?.length) return nothing; - - return html` - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-embed-card-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-embed-card-button.ts deleted file mode 100644 index bbf335ec69..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-embed-card-button.ts +++ /dev/null @@ -1,912 +0,0 @@ -import {} from '@blocksuite/affine-block-bookmark'; -import { - EmbedLinkedDocBlockComponent, - EmbedSyncedDocBlockComponent, - getDocContentWithMaxLength, - getEmbedCardIcons, -} from '@blocksuite/affine-block-embed'; -import { - EdgelessCRUDIdentifier, - reassociateConnectorsCommand, -} from '@blocksuite/affine-block-surface'; -import { toggleEmbedCardEditModal } from '@blocksuite/affine-components/embed-card-modal'; -import { - CaptionIcon, - CopyIcon, - EditIcon, - ExpandFullSmallIcon, - OpenIcon, - PaletteIcon, -} from '@blocksuite/affine-components/icons'; -import { - notifyLinkedDocClearedAliases, - notifyLinkedDocSwitchedToCard, - notifyLinkedDocSwitchedToEmbed, -} from '@blocksuite/affine-components/notification'; -import { isPeekable, peek } from '@blocksuite/affine-components/peek'; -import { toast } from '@blocksuite/affine-components/toast'; -import { - type MenuItem, - renderToolbarSeparator, -} from '@blocksuite/affine-components/toolbar'; -import { - type AliasInfo, - BookmarkStyles, - type BuiltInEmbedModel, - type EmbedCardStyle, - isInternalEmbedModel, -} from '@blocksuite/affine-model'; -import { - EMBED_CARD_HEIGHT, - EMBED_CARD_WIDTH, -} from '@blocksuite/affine-shared/consts'; -import { - EmbedOptionProvider, - type EmbedOptions, - GenerateDocUrlProvider, - type GenerateDocUrlService, - type LinkEventType, - OpenDocExtensionIdentifier, - type OpenDocMode, - type TelemetryEvent, - TelemetryProvider, - ThemeProvider, -} from '@blocksuite/affine-shared/services'; -import { getHostName, referenceToNode } from '@blocksuite/affine-shared/utils'; -import type { BlockStdScope } from '@blocksuite/block-std'; -import { Bound } from '@blocksuite/global/gfx'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { css, html, LitElement, nothing, type TemplateResult } from 'lit'; -import { property, state } from 'lit/decorators.js'; -import { ifDefined } from 'lit/directives/if-defined.js'; -import { join } from 'lit/directives/join.js'; -import { repeat } from 'lit/directives/repeat.js'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import { - isBookmarkBlock, - isEmbedGithubBlock, - isEmbedHtmlBlock, - isEmbedLinkedDocBlock, - isEmbedSyncedDocBlock, -} from '../../edgeless/utils/query.js'; -import type { BuiltInEmbedBlockComponent } from '../../utils/types'; -import { SmallArrowDownIcon } from './icons.js'; - -export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) { - static override styles = css` - .affine-link-preview { - display: flex; - justify-content: flex-start; - width: 140px; - padding: var(--1, 0px); - border-radius: var(--1, 0px); - opacity: var(--add, 1); - user-select: none; - cursor: pointer; - - color: var(--affine-link-color); - font-feature-settings: - 'clig' off, - 'liga' off; - font-family: var(--affine-font-family); - font-size: var(--affine-font-sm); - font-style: normal; - font-weight: 400; - text-decoration: none; - text-wrap: nowrap; - } - - .affine-link-preview > span { - display: inline-block; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - - text-overflow: ellipsis; - overflow: hidden; - opacity: var(--add, 1); - } - - editor-icon-button.doc-title .label { - max-width: 110px; - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - user-select: none; - cursor: pointer; - color: var(--affine-link-color); - font-feature-settings: - 'clig' off, - 'liga' off; - font-family: var(--affine-font-family); - font-size: var(--affine-font-sm); - font-style: normal; - font-weight: 400; - text-decoration: none; - text-wrap: nowrap; - } - `; - - get crud() { - return this.edgeless.std.get(EdgelessCRUDIdentifier); - } - - private readonly _convertToCardView = () => { - if (this._isCardView) { - return; - } - - const block = this._blockComponent; - if (block && 'convertToCard' in block) { - block.convertToCard(); - return; - } - - if (!('url' in this.model)) { - return; - } - - const { xywh, style, caption } = this.model.props; - const { id, url } = this.model; - - let targetFlavour = 'affine:bookmark', - targetStyle = style; - - if (this._embedOptions && this._embedOptions.viewType === 'card') { - const { flavour, styles } = this._embedOptions; - targetFlavour = flavour; - targetStyle = styles.includes(style) ? style : styles[0]; - } else { - targetStyle = BookmarkStyles.includes(style) ? style : BookmarkStyles[0]; - } - - const bound = Bound.deserialize(xywh); - bound.w = EMBED_CARD_WIDTH[targetStyle]; - bound.h = EMBED_CARD_HEIGHT[targetStyle]; - - const newId = this.crud.addBlock( - targetFlavour, - { url, xywh: bound.serialize(), style: targetStyle, caption }, - this.edgeless.surface.model - ); - - this.std.command.exec(reassociateConnectorsCommand, { - oldId: id, - newId, - }); - - this.edgeless.service.selection.set({ - editing: false, - elements: [newId], - }); - this._doc.deleteBlock(this.model); - }; - - private readonly _convertToEmbedView = () => { - if (this._isEmbedView) { - return; - } - - const block = this._blockComponent; - if (block && 'convertToEmbed' in block) { - const referenceInfo = block.referenceInfo$.peek(); - - block.convertToEmbed(); - - if (referenceInfo.title || referenceInfo.description) - notifyLinkedDocSwitchedToEmbed(this.std); - - return; - } - - if (!('url' in this.model)) { - return; - } - - if (!this._embedOptions) return; - - const { flavour, styles } = this._embedOptions; - - const { id, url, xywh } = this.model; - const { style } = this.model.props; - - const targetStyle = styles.includes(style) ? style : styles[0]; - - const bound = Bound.deserialize(xywh); - bound.w = EMBED_CARD_WIDTH[targetStyle]; - bound.h = EMBED_CARD_HEIGHT[targetStyle]; - - const newId = this.crud.addBlock( - flavour, - { - url, - xywh: bound.serialize(), - style: targetStyle, - }, - this.edgeless.surface.model - ); - if (!newId) return; - - this.std.command.exec(reassociateConnectorsCommand, { - oldId: id, - newId, - }); - - this.edgeless.service.selection.set({ - editing: false, - elements: [newId], - }); - this._doc.deleteBlock(this.model); - }; - - private readonly _copyUrl = () => { - let url!: ReturnType; - - if ('url' in this.model.props) { - url = this.model.props.url; - } else if (isInternalEmbedModel(this.model)) { - url = this.std - .getOptional(GenerateDocUrlProvider) - ?.generateDocUrl(this.model.props.pageId, this.model.props.params); - } - - if (!url) return; - - navigator.clipboard.writeText(url).catch(console.error); - toast(this.std.host, 'Copied link to clipboard'); - this.edgeless.service.selection.clear(); - - track(this.std, this.model, this._viewType, 'CopiedLink', { - control: 'copy link', - }); - }; - - private _embedOptions: EmbedOptions | null = null; - - private readonly _getScale = () => { - if ('scale' in this.model.props) { - return this.model.props.scale ?? 1; - } else if (isEmbedHtmlBlock(this.model)) { - return 1; - } - - const bound = Bound.deserialize(this.model.xywh); - return bound.h / EMBED_CARD_HEIGHT[this.model.props.style]; - }; - - private readonly _open = ({ openMode }: { openMode?: OpenDocMode } = {}) => { - this._blockComponent?.open({ openMode }); - }; - - private readonly _openEditPopup = (e: MouseEvent) => { - e.stopPropagation(); - - if (isEmbedHtmlBlock(this.model)) return; - - this.std.selection.clear(); - - const originalDocInfo = this._originalDocInfo; - - toggleEmbedCardEditModal( - this.std.host, - this.model, - this._viewType, - originalDocInfo, - (std, component) => { - if ( - isEmbedLinkedDocBlock(this.model) && - component instanceof EmbedLinkedDocBlockComponent - ) { - component.refreshData(); - - notifyLinkedDocClearedAliases(std); - } - }, - (std, component, props) => { - if ( - isEmbedSyncedDocBlock(this.model) && - component instanceof EmbedSyncedDocBlockComponent - ) { - component.convertToCard(props); - - notifyLinkedDocSwitchedToCard(std); - } else { - this.model.doc.updateBlock(this.model, props); - component.requestUpdate(); - } - } - ); - - track(this.std, this.model, this._viewType, 'OpenedAliasPopup', { - control: 'edit', - }); - }; - - private readonly _peek = () => { - if (!this._blockComponent) return; - peek(this._blockComponent); - }; - - private readonly _setCardStyle = (style: EmbedCardStyle) => { - const bounds = Bound.deserialize(this.model.xywh); - bounds.w = EMBED_CARD_WIDTH[style]; - bounds.h = EMBED_CARD_HEIGHT[style]; - const xywh = bounds.serialize(); - this.model.doc.updateBlock(this.model, { style, xywh }); - - track(this.std, this.model, this._viewType, 'SelectedCardStyle', { - control: 'select card style', - type: style, - }); - }; - - private readonly _setEmbedScale = (scale: number) => { - if (isEmbedHtmlBlock(this.model)) return; - - const bound = Bound.deserialize(this.model.xywh); - if ('scale' in this.model.props) { - const oldScale = this.model.props.scale ?? 1; - const ratio = scale / oldScale; - bound.w *= ratio; - bound.h *= ratio; - const xywh = bound.serialize(); - this.model.doc.updateBlock(this.model, { scale, xywh }); - } else { - bound.h = EMBED_CARD_HEIGHT[this.model.props.style] * scale; - bound.w = EMBED_CARD_WIDTH[this.model.props.style] * scale; - const xywh = bound.serialize(); - this.model.doc.updateBlock(this.model, { xywh }); - } - this._embedScale = scale; - - track(this.std, this.model, this._viewType, 'SelectedCardScale', { - control: 'select card scale', - type: `${scale}`, - }); - }; - - private readonly _toggleCardScaleSelector = (e: Event) => { - const opened = (e as CustomEvent).detail; - if (!opened) return; - - track(this.std, this.model, this._viewType, 'OpenedCardScaleSelector', { - control: 'switch card scale', - }); - }; - - private readonly _toggleCardStyleSelector = (e: Event) => { - const opened = (e as CustomEvent).detail; - if (!opened) return; - - track(this.std, this.model, this._viewType, 'OpenedCardStyleSelector', { - control: 'switch card style', - }); - }; - - private readonly _toggleViewSelector = (e: Event) => { - const opened = (e as CustomEvent).detail; - if (!opened) return; - - track(this.std, this.model, this._viewType, 'OpenedViewSelector', { - control: 'switch view', - }); - }; - - private readonly _trackViewSelected = (type: string) => { - track(this.std, this.model, this._viewType, 'SelectedView', { - control: 'select view', - type: `${type} view`, - }); - }; - - private get _blockComponent() { - const blockSelection = - this.edgeless.service.selection.surfaceSelections.filter(sel => - sel.elements.includes(this.model.id) - ); - if (blockSelection.length !== 1) { - return; - } - - const blockComponent = this.std.view.getBlock( - blockSelection[0].blockId - ) as BuiltInEmbedBlockComponent | null; - - if (!blockComponent) return; - - return blockComponent; - } - - private get _canConvertToEmbedView() { - const block = this._blockComponent; - - return ( - (block && 'convertToEmbed' in block) || - this._embedOptions?.viewType === 'embed' - ); - } - - private get _canShowCardStylePanel() { - return ( - isBookmarkBlock(this.model) || - isEmbedGithubBlock(this.model) || - isEmbedLinkedDocBlock(this.model) - ); - } - - private get _canShowFullScreenButton() { - return isEmbedHtmlBlock(this.model); - } - - private get _canShowUrlOptions() { - return ( - 'url' in this.model && - (isBookmarkBlock(this.model) || - isEmbedGithubBlock(this.model) || - isEmbedLinkedDocBlock(this.model)) - ); - } - - private get _doc() { - return this.model.doc; - } - - private get _embedViewButtonDisabled() { - if (this._doc.readonly) { - return true; - } - return ( - isEmbedLinkedDocBlock(this.model) && - (referenceToNode(this.model.props) || - !!this._blockComponent?.closest('affine-embed-synced-doc-block') || - this.model.props.pageId === this._doc.id) - ); - } - - private get _getCardStyleOptions(): { - style: EmbedCardStyle; - Icon: TemplateResult<1>; - tooltip: string; - }[] { - const theme = this.std.get(ThemeProvider).theme; - const { - EmbedCardHorizontalIcon, - EmbedCardListIcon, - EmbedCardVerticalIcon, - EmbedCardCubeIcon, - } = getEmbedCardIcons(theme); - return [ - { - style: 'horizontal', - Icon: EmbedCardHorizontalIcon, - tooltip: 'Large horizontal style', - }, - { - style: 'list', - Icon: EmbedCardListIcon, - tooltip: 'Small horizontal style', - }, - { - style: 'vertical', - Icon: EmbedCardVerticalIcon, - tooltip: 'Large vertical style', - }, - { - style: 'cube', - Icon: EmbedCardCubeIcon, - tooltip: 'Small vertical style', - }, - ]; - } - - private get _isCardView() { - if (isBookmarkBlock(this.model) || isEmbedLinkedDocBlock(this.model)) { - return true; - } - return this._embedOptions?.viewType === 'card'; - } - - private get _isEmbedView() { - return ( - !isBookmarkBlock(this.model) && - (isEmbedSyncedDocBlock(this.model) || - this._embedOptions?.viewType === 'embed') - ); - } - - get _originalDocInfo(): AliasInfo | undefined { - const model = this.model; - const doc = isInternalEmbedModel(model) - ? this.std.workspace.getDoc(model.props.pageId) - : null; - - if (doc) { - const title = doc.meta?.title; - const description = isEmbedLinkedDocBlock(model) - ? getDocContentWithMaxLength(doc) - : undefined; - return { title, description }; - } - - return undefined; - } - - get _originalDocTitle() { - const model = this.model; - const doc = isInternalEmbedModel(model) - ? this.std.workspace.getDoc(model.props.pageId) - : null; - - return doc?.meta?.title || 'Untitled'; - } - - private get _viewType(): 'inline' | 'embed' | 'card' { - if (this._isCardView) { - return 'card'; - } - - if (this._isEmbedView) { - return 'embed'; - } - - // unreachable - return 'inline'; - } - - private get std() { - return this.edgeless.std; - } - - private _openMenuButton() { - const openDocConfig = this.std.get(OpenDocExtensionIdentifier); - const buttons: MenuItem[] = openDocConfig.items - .map(item => { - if ( - item.type === 'open-in-center-peek' && - this._blockComponent && - !isPeekable(this._blockComponent) - ) { - return null; - } - - if ( - !( - isEmbedLinkedDocBlock(this.model) || - isEmbedSyncedDocBlock(this.model) - ) - ) { - return null; - } - - return { - label: item.label, - type: item.type, - icon: item.icon, - disabled: - this.model.props.pageId === this._doc.id && - item.type === 'open-in-active-view', - action: () => { - if (item.type === 'open-in-center-peek') { - this._peek(); - } else { - this._open({ openMode: item.type }); - } - }, - }; - }) - .filter(item => item !== null); - - // todo: abstract this? - if (this._canShowFullScreenButton) { - buttons.push({ - type: 'open-this-doc', - label: 'Open this doc', - icon: ExpandFullSmallIcon, - action: this._open, - }); - } - - if (buttons.length === 0) { - return nothing; - } - - return html` - - ${OpenIcon}${SmallArrowDownIcon} - - `} - > -
- ${repeat( - buttons, - button => button.label, - ({ label, icon, action, disabled }) => html` - - ${icon}${label} - - ` - )} -
-
- `; - } - - private _showCaption() { - this._blockComponent?.captionEditor?.show(); - - track(this.std, this.model, this._viewType, 'OpenedCaptionEditor', { - control: 'add caption', - }); - } - - private _viewSelector() { - if (this._canConvertToEmbedView || this._isEmbedView) { - const buttons = [ - { - type: 'card', - label: 'Card view', - action: () => this._convertToCardView(), - disabled: this.model.doc.readonly, - }, - { - type: 'embed', - label: 'Embed view', - action: () => this._convertToEmbedView(), - disabled: this.model.doc.readonly || this._embedViewButtonDisabled, - }, - ]; - - return html` - -
- ${this._viewType} - view -
- ${SmallArrowDownIcon} - - `} - @toggle=${this._toggleViewSelector} - > -
- ${repeat( - buttons, - button => button.type, - ({ type, label, action, disabled }) => html` - { - action(); - this._trackViewSelected(type); - }} - > - ${label} - - ` - )} -
-
- `; - } - - return nothing; - } - - override connectedCallback() { - super.connectedCallback(); - this._embedScale = this._getScale(); - } - - override render() { - const model = this.model; - const isHtmlBlockModel = isEmbedHtmlBlock(model); - - if ('url' in this.model.props) { - this._embedOptions = this.std - .get(EmbedOptionProvider) - .getEmbedBlockOptions(this.model.props.url); - } - - const buttons = [ - this._openMenuButton(), - - this._canShowUrlOptions && 'url' in model.props - ? html` - - ${getHostName(model.props.url)} - - ` - : nothing, - - // internal embed model - isEmbedLinkedDocBlock(model) && model.props.title - ? html` - - ${this._originalDocTitle} - - ` - : nothing, - - isHtmlBlockModel - ? nothing - : html` - - ${CopyIcon} - - - - ${EditIcon} - - `, - - this._viewSelector(), - - 'style' in model && this._canShowCardStylePanel - ? html` - - ${PaletteIcon} - - `} - @toggle=${this._toggleCardStyleSelector} - > - - - - ` - : nothing, - - 'caption' in model - ? html` - - ${CaptionIcon} - - ` - : nothing, - - this.quickConnectButton, - - isHtmlBlockModel - ? nothing - : html` - - - ${Math.round(this._embedScale * 100) + '%'} - - ${SmallArrowDownIcon} - - `} - @toggle=${this._toggleCardScaleSelector} - > - - - `, - ]; - - return join( - buttons.filter(button => button !== nothing), - renderToolbarSeparator - ); - } - - @state() - private accessor _embedScale = 1; - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor model!: BuiltInEmbedModel; - - @property({ attribute: false }) - accessor quickConnectButton!: TemplateResult<1> | typeof nothing; -} - -export function renderEmbedButton( - edgeless: EdgelessRootBlockComponent, - models?: EdgelessChangeEmbedCardButton['model'][], - quickConnectButton?: TemplateResult<1>[] -) { - if (models?.length !== 1) return nothing; - - return html` - - `; -} - -function track( - std: BlockStdScope, - model: BuiltInEmbedModel, - viewType: string, - event: LinkEventType, - props: Partial -) { - std.getOptional(TelemetryProvider)?.track(event, { - segment: 'toolbar', - page: 'whiteboard editor', - module: 'element toolbar', - type: `${viewType} view`, - category: isInternalEmbedModel(model) ? 'linked doc' : 'link', - ...props, - }); -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-frame-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-frame-button.ts deleted file mode 100644 index ea23ffa52c..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-frame-button.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame'; -import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface'; -import type { - EdgelessColorPickerButton, - PickColorEvent, -} from '@blocksuite/affine-components/color-picker'; -import { packColor } from '@blocksuite/affine-components/color-picker'; -import { toast } from '@blocksuite/affine-components/toast'; -import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar'; -import { - type ColorScheme, - DEFAULT_NOTE_HEIGHT, - type FrameBlockModel, - NoteBlockModel, - NoteDisplayMode, - resolveColor, -} from '@blocksuite/affine-model'; -import { FeatureFlagService } from '@blocksuite/affine-shared/services'; -import { matchModels } from '@blocksuite/affine-shared/utils'; -import { deserializeXYWH, serializeXYWH } from '@blocksuite/global/gfx'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit'; -import { html, LitElement, nothing } from 'lit'; -import { property, query } from 'lit/decorators.js'; -import { join } from 'lit/directives/join.js'; -import countBy from 'lodash-es/countBy'; -import maxBy from 'lodash-es/maxBy'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import { mountFrameTitleEditor } from '../../edgeless/utils/text.js'; - -function getMostCommonColor( - elements: FrameBlockModel[], - colorScheme: ColorScheme -): string { - const colors = countBy(elements, (ele: FrameBlockModel) => - resolveColor(ele.props.background, colorScheme) - ); - const max = maxBy(Object.entries(colors), ([_k, count]) => count); - return max ? (max[0] as string) : 'transparent'; -} - -export class EdgelessChangeFrameButton extends WithDisposable(LitElement) { - get crud() { - return this.edgeless.std.get(EdgelessCRUDIdentifier); - } - - pickColor = (e: PickColorEvent) => { - const field = 'background'; - - if (e.type === 'pick') { - const color = e.detail.value; - this.frames.forEach(ele => { - const props = packColor(field, color); - this.crud.updateElement(ele.id, props); - }); - return; - } - - this.frames.forEach(ele => - ele[e.type === 'start' ? 'stash' : 'pop'](field) - ); - }; - - get service() { - return this.edgeless.service; - } - - private _insertIntoPage() { - if (!this.edgeless.doc.root) return; - - const rootModel = this.edgeless.doc.root; - const notes = rootModel.children.filter( - model => - matchModels(model, [NoteBlockModel]) && - model.props.displayMode !== NoteDisplayMode.EdgelessOnly - ); - const lastNote = notes[notes.length - 1]; - const referenceFrame = this.frames[0]; - - let targetParent = lastNote?.id; - - if (!lastNote) { - const targetXYWH = deserializeXYWH(referenceFrame.xywh); - - targetXYWH[1] = targetXYWH[1] + targetXYWH[3]; - targetXYWH[3] = DEFAULT_NOTE_HEIGHT; - - const newAddedNote = this.edgeless.doc.addBlock( - 'affine:note', - { - xywh: serializeXYWH(...targetXYWH), - }, - rootModel.id - ); - - targetParent = newAddedNote; - } - - this.edgeless.doc.addBlock( - 'affine:surface-ref', - { - reference: this.frames[0].id, - refFlavour: 'affine:frame', - }, - targetParent - ); - - toast(this.edgeless.host, 'Frame has been inserted into doc'); - } - - protected override render() { - const { frames } = this; - const len = frames.length; - const onlyOne = len === 1; - const colorScheme = this.edgeless.surface.renderer.getColorScheme(); - const background = getMostCommonColor(frames, colorScheme); - const enableCustomColor = this.edgeless.doc - .get(FeatureFlagService) - .getFlag('enable_color_picker'); - - return join( - [ - onlyOne - ? html` - - ${PageIcon()} - Insert into Page - - ` - : nothing, - - onlyOne - ? html` - - mountFrameTitleEditor(this.frames[0], this.edgeless)} - > - ${EditIcon()} - - ` - : nothing, - - html` - { - this.edgeless.doc.captureSync(); - const frameMgr = this.edgeless.std.get( - EdgelessFrameManagerIdentifier - ); - frames.forEach(frame => - frameMgr.removeAllChildrenFromFrame(frame) - ); - frames.forEach(frame => { - this.edgeless.service.removeElement(frame); - }); - this.edgeless.service.selection.clear(); - }} - > - ${UngroupIcon()} - - `, - - html` - - - `, - ].filter(button => button !== nothing), - renderToolbarSeparator - ); - } - - @query('edgeless-color-picker-button.background') - accessor backgroundButton!: EdgelessColorPickerButton; - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor frames: FrameBlockModel[] = []; -} - -export function renderFrameButton( - edgeless: EdgelessRootBlockComponent, - frames?: FrameBlockModel[] -) { - if (!frames?.length) return nothing; - - return html` - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-group-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-group-button.ts deleted file mode 100644 index b2eb6d8d95..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-group-button.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { toast } from '@blocksuite/affine-components/toast'; -import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar'; -import type { GroupElementModel } from '@blocksuite/affine-model'; -import { - DEFAULT_NOTE_HEIGHT, - NoteBlockModel, - NoteDisplayMode, -} from '@blocksuite/affine-model'; -import { matchModels } from '@blocksuite/affine-shared/utils'; -import { deserializeXYWH, serializeXYWH } from '@blocksuite/global/gfx'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit'; -import { html, LitElement, nothing } from 'lit'; -import { property } from 'lit/decorators.js'; -import { join } from 'lit/directives/join.js'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import { mountGroupTitleEditor } from '../../edgeless/utils/text.js'; - -export class EdgelessChangeGroupButton extends WithDisposable(LitElement) { - private _insertIntoPage() { - if (!this.edgeless.doc.root) return; - - const rootModel = this.edgeless.doc.root; - const notes = rootModel.children.filter( - model => - matchModels(model, [NoteBlockModel]) && - model.props.displayMode !== NoteDisplayMode.EdgelessOnly - ); - const lastNote = notes[notes.length - 1]; - const referenceGroup = this.groups[0]; - - let targetParent = lastNote?.id; - - if (!lastNote) { - const targetXYWH = deserializeXYWH(referenceGroup.xywh); - - targetXYWH[1] = targetXYWH[1] + targetXYWH[3]; - targetXYWH[3] = DEFAULT_NOTE_HEIGHT; - - const newAddedNote = this.edgeless.doc.addBlock( - 'affine:note', - { - xywh: serializeXYWH(...targetXYWH), - }, - rootModel.id - ); - - targetParent = newAddedNote; - } - - this.edgeless.doc.addBlock( - 'affine:surface-ref', - { - reference: this.groups[0].id, - refFlavour: 'group', - }, - targetParent - ); - - toast(this.edgeless.host, 'Group has been inserted into page'); - } - - protected override render() { - const { groups } = this; - const onlyOne = groups.length === 1; - - return join( - [ - onlyOne - ? html` - - ${PageIcon()} - Insert into Page - - ` - : nothing, - - onlyOne - ? html` - mountGroupTitleEditor(groups[0], this.edgeless)} - > - ${EditIcon()} - - ` - : nothing, - - html` - - groups.forEach(group => this.edgeless.service.ungroup(group))} - > - ${UngroupIcon()} - - `, - ].filter(button => button !== nothing), - renderToolbarSeparator - ); - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor groups!: GroupElementModel[]; -} - -export function renderGroupButton( - edgeless: EdgelessRootBlockComponent, - groups?: GroupElementModel[] -) { - if (!groups?.length) return nothing; - - return html` - - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-image-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-image-button.ts deleted file mode 100644 index 9a17ad3c8c..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-image-button.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - downloadImageBlob, - type ImageBlockComponent, -} from '@blocksuite/affine-block-image'; -import { CaptionIcon, DownloadIcon } from '@blocksuite/affine-components/icons'; -import type { ImageBlockModel } from '@blocksuite/affine-model'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { html, LitElement, nothing } from 'lit'; -import { property } from 'lit/decorators.js'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; - -export class EdgelessChangeImageButton extends WithDisposable(LitElement) { - private readonly _download = () => { - if (!this._blockComponent) return; - downloadImageBlob(this._blockComponent).catch(console.error); - }; - - private readonly _showCaption = () => { - this._blockComponent?.captionEditor?.show(); - }; - - private get _blockComponent() { - const blockSelection = - this.edgeless.service.selection.surfaceSelections.filter(sel => - sel.elements.includes(this.model.id) - ); - if (blockSelection.length !== 1) { - return; - } - - const block = this.edgeless.std.view.getBlock( - blockSelection[0].blockId - ) as ImageBlockComponent | null; - - return block; - } - - private get _doc() { - return this.model.doc; - } - - override render() { - return html` - - ${DownloadIcon} - - - - - - ${CaptionIcon} - - `; - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor model!: ImageBlockModel; -} - -export function renderChangeImageButton( - edgeless: EdgelessRootBlockComponent, - images?: ImageBlockModel[] -) { - if (images?.length !== 1) return nothing; - - return html` - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-mindmap-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-mindmap-button.ts deleted file mode 100644 index 3b303220de..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-mindmap-button.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { - MindmapStyleFour, - MindmapStyleOne, - MindmapStyleThree, - MindmapStyleTwo, -} from '@blocksuite/affine-block-surface'; -import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar'; -import type { - MindmapElementModel, - ShapeElementModel, -} from '@blocksuite/affine-model'; -import { LayoutType, MindmapStyle } from '@blocksuite/affine-model'; -import { EditPropsStore } from '@blocksuite/affine-shared/services'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { RadiantIcon, RightLayoutIcon, StyleIcon } from '@blocksuite/icons/lit'; -import { css, html, LitElement, nothing, type TemplateResult } from 'lit'; -import { property, state } from 'lit/decorators.js'; -import { join } from 'lit/directives/join.js'; -import { repeat } from 'lit/directives/repeat.js'; -import countBy from 'lodash-es/countBy'; -import maxBy from 'lodash-es/maxBy'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import { SmallArrowDownIcon } from './icons.js'; - -const iconSize = { width: '20', height: '20' }; - -const MINDMAP_STYLE_LIST = [ - { - value: MindmapStyle.ONE, - icon: MindmapStyleOne, - }, - { - value: MindmapStyle.FOUR, - icon: MindmapStyleFour, - }, - { - value: MindmapStyle.THREE, - icon: MindmapStyleThree, - }, - { - value: MindmapStyle.TWO, - icon: MindmapStyleTwo, - }, -]; - -interface LayoutItem { - name: string; - value: LayoutType; - icon: TemplateResult<1>; -} - -const MINDMAP_LAYOUT_LIST: LayoutItem[] = [ - { - name: 'Left', - value: LayoutType.LEFT, - icon: RightLayoutIcon({ - ...iconSize, - style: 'transform: rotate(0.5turn); transform-origin: center;', - }), - }, - { - name: 'Radial', - value: LayoutType.BALANCE, - icon: RadiantIcon(iconSize), - }, - { - name: 'Right', - value: LayoutType.RIGHT, - icon: RightLayoutIcon(iconSize), - }, -] as const; - -export class EdgelessChangeMindmapStylePanel extends LitElement { - static override styles = css` - :host { - display: flex; - align-items: center; - justify-content: center; - flex-direction: row; - gap: 8px; - background: var(--affine-background-overlay-panel-color); - } - - .style-item { - border-radius: 4px; - } - - .style-item > svg { - vertical-align: middle; - } - - .style-item.active, - .style-item:hover { - cursor: pointer; - background-color: var(--affine-hover-color); - } - `; - - override render() { - return repeat( - MINDMAP_STYLE_LIST, - item => item.value, - ({ value, icon }) => html` -
this.onSelect(value)} - > - ${icon} -
- ` - ); - } - - @property({ attribute: false }) - accessor mindmapStyle!: MindmapStyle | null; - - @property({ attribute: false }) - accessor onSelect!: (style: MindmapStyle) => void; -} - -export class EdgelessChangeMindmapLayoutPanel extends LitElement { - static override styles = css` - :host { - display: flex; - align-items: center; - justify-content: center; - flex-direction: row; - gap: 8px; - } - `; - - override render() { - return repeat( - MINDMAP_LAYOUT_LIST, - item => item.value, - ({ name, value, icon }) => html` - this.onSelect(value)} - > - ${icon} - - ` - ); - } - - @property({ attribute: false }) - accessor mindmapLayout!: LayoutType | null; - - @property({ attribute: false }) - accessor onSelect!: (style: LayoutType) => void; -} - -export class EdgelessChangeMindmapButton extends WithDisposable(LitElement) { - private readonly _updateLayoutType = (layoutType: LayoutType) => { - this.edgeless.std.get(EditPropsStore).recordLastProps('mindmap', { - layoutType, - }); - this.elements.forEach(element => { - element.layoutType = layoutType; - element.layout(); - }); - this.layoutType = layoutType; - }; - - private readonly _updateStyle = (style: MindmapStyle) => { - this.edgeless.std.get(EditPropsStore).recordLastProps('mindmap', { style }); - this._mindmaps.forEach(element => (element.style = style)); - }; - - private get _mindmaps() { - const mindmaps = new Set(); - - return this.elements.reduce((_, el) => { - mindmaps.add(el); - - return mindmaps; - }, mindmaps); - } - - get layout() { - const layoutType = this.layoutType ?? this._getCommonLayoutType(); - return MINDMAP_LAYOUT_LIST.find(item => item.value === layoutType)!; - } - - private _getCommonLayoutType() { - const values = countBy(this.elements, element => element.layoutType); - const max = maxBy(Object.entries(values), ([_k, count]) => count); - return max ? (Number(max[0]) as LayoutType) : LayoutType.BALANCE; - } - - private _getCommonStyle() { - const values = countBy(this.elements, element => element.style); - const max = maxBy(Object.entries(values), ([_k, count]) => count); - return max ? (Number(max[0]) as MindmapStyle) : MindmapStyle.ONE; - } - - private _isSubnode() { - return ( - this.nodes.length === 1 && - (this.nodes[0].group as MindmapElementModel).tree.element !== - this.nodes[0] - ); - } - - override render() { - return join( - [ - html` - - ${StyleIcon(iconSize)}${SmallArrowDownIcon} - - `} - > - - - - `, - - this._isSubnode() - ? nothing - : html` - - ${this.layout.icon}${SmallArrowDownIcon} - - `} - > - - - - `, - ].filter(button => button !== nothing), - renderToolbarSeparator - ); - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor elements!: MindmapElementModel[]; - - @state() - accessor layoutType!: LayoutType; - - @property({ attribute: false }) - accessor nodes!: ShapeElementModel[]; -} - -export function renderMindmapButton( - edgeless: EdgelessRootBlockComponent, - elements?: (ShapeElementModel | MindmapElementModel)[] -) { - if (!elements?.length) return nothing; - - const mindmaps: MindmapElementModel[] = []; - - elements.forEach(e => { - if (e.type === 'mindmap') { - mindmaps.push(e as MindmapElementModel); - } - - const group = edgeless.service.surface.getGroup(e.id); - - if (group && 'type' in group && group.type === 'mindmap') { - mindmaps.push(group as MindmapElementModel); - } - }); - - if (mindmaps.length === 0) { - return nothing; - } - - return html` - e.type === 'shape')} - .edgeless=${edgeless} - > - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-note-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-note-button.ts deleted file mode 100644 index ab20001792..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-note-button.ts +++ /dev/null @@ -1,576 +0,0 @@ -import { - changeNoteDisplayMode, - NoteConfigExtension, -} from '@blocksuite/affine-block-note'; -import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface'; -import type { - EdgelessColorPickerButton, - PickColorEvent, -} from '@blocksuite/affine-components/color-picker'; -import { packColor } from '@blocksuite/affine-components/color-picker'; -import { - type EditorMenuButton, - renderToolbarSeparator, -} from '@blocksuite/affine-components/toolbar'; -import { - type ColorScheme, - DefaultTheme, - type NoteBlockModel, - NoteDisplayMode, - resolveColor, - type StrokeStyle, -} from '@blocksuite/affine-model'; -import { - FeatureFlagService, - NotificationProvider, - SidebarExtensionIdentifier, - TelemetryProvider, - ThemeProvider, -} from '@blocksuite/affine-shared/services'; -import { EditorLifeCycleExtension } from '@blocksuite/block-std'; -import { Bound } from '@blocksuite/global/gfx'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { - AutoHeightIcon, - CornerIcon, - CustomizedHeightIcon, - LineStyleIcon, - LinkedPageIcon, - NoteShadowDuotoneIcon, - ScissorsIcon, -} from '@blocksuite/icons/lit'; -import { html, LitElement, nothing, type TemplateResult } from 'lit'; -import { property, query } from 'lit/decorators.js'; -import { join } from 'lit/directives/join.js'; -import { createRef, type Ref, ref } from 'lit/directives/ref.js'; -import countBy from 'lodash-es/countBy'; -import maxBy from 'lodash-es/maxBy'; - -import { - type LineStyleEvent, - LineStylesPanel, -} from '../../edgeless/components/panel/line-styles-panel.js'; -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import { SmallArrowDownIcon } from './icons.js'; -import * as styles from './styles.css'; - -const SIZE_LIST = [ - { name: 'None', value: 0 }, - { name: 'Small', value: 8 }, - { name: 'Medium', value: 16 }, - { name: 'Large', value: 24 }, - { name: 'Huge', value: 32 }, -] as const; - -const DisplayModeMap = { - [NoteDisplayMode.DocAndEdgeless]: 'Both', - [NoteDisplayMode.EdgelessOnly]: 'Edgeless', - [NoteDisplayMode.DocOnly]: 'Page', -} as const satisfies Record; - -function getMostCommonBackground( - elements: NoteBlockModel[], - colorScheme: ColorScheme -): string { - const colors = countBy(elements, (ele: NoteBlockModel) => - resolveColor(ele.props.background, colorScheme) - ); - const max = maxBy(Object.entries(colors), ([_k, count]) => count); - return max - ? (max[0] as string) - : resolveColor(DefaultTheme.noteBackgrounColor, colorScheme); -} - -export class EdgelessChangeNoteButton extends WithDisposable(LitElement) { - get crud() { - return this.edgeless.std.get(EdgelessCRUDIdentifier); - } - - private readonly _setBorderRadius = (borderRadius: number) => { - this.notes.forEach(note => { - const props = { - edgeless: { - ...note.props.edgeless, - style: { - ...note.props.edgeless.style, - borderRadius, - }, - }, - }; - this.crud.updateElement(note.id, props); - }); - }; - - private readonly _setNoteScale = (scale: number) => { - this.notes.forEach(note => { - this.doc.updateBlock(note, () => { - const bound = Bound.deserialize(note.xywh); - const oldScale = note.props.edgeless.scale ?? 1; - const ratio = scale / oldScale; - bound.w *= ratio; - bound.h *= ratio; - const xywh = bound.serialize(); - note.xywh = xywh; - note.props.edgeless.scale = scale; - }); - }); - }; - - pickColor = (e: PickColorEvent) => { - const field = 'background'; - - if (e.type === 'pick') { - const color = e.detail.value; - this.notes.forEach(element => { - const props = packColor(field, color); - this.crud.updateElement(element.id, props); - }); - return; - } - - this.notes.forEach(ele => ele[e.type === 'start' ? 'stash' : 'pop'](field)); - }; - - private get _advancedVisibilityEnabled() { - return this.doc - .get(FeatureFlagService) - .getFlag('enable_advanced_block_visibility'); - } - - private get doc() { - return this.edgeless.doc; - } - - private _getScaleLabel(scale: number) { - return Math.round(scale * 100) + '%'; - } - - private _handleNoteSlicerButtonClick() { - const surfaceService = this.edgeless.service; - if (!surfaceService) return; - - this.edgeless.slots.toggleNoteSlicer.next(); - } - - private _setCollapse() { - this.doc.captureSync(); - this.notes.forEach(note => { - const { collapse, collapsedHeight } = note.props.edgeless; - - if (collapse) { - this.doc.updateBlock(note, () => { - note.props.edgeless.collapse = false; - }); - } else if (collapsedHeight) { - const { xywh, edgeless } = note.props; - const bound = Bound.deserialize(xywh); - bound.h = collapsedHeight * (edgeless.scale ?? 1); - this.doc.updateBlock(note, () => { - note.props.edgeless.collapse = true; - note.props.xywh = bound.serialize(); - }); - } - }); - this.requestUpdate(); - } - - private _setDisplayMode(note: NoteBlockModel, newMode: NoteDisplayMode) { - const oldMode = note.props.displayMode; - this.edgeless.std.command.exec(changeNoteDisplayMode, { - noteId: note.id, - mode: newMode, - stopCapture: true, - }); - - // if change note to page only, should clear the selection - if (newMode === NoteDisplayMode.DocOnly) { - this.edgeless.service.selection.clear(); - } - - const abortController = new AbortController(); - const clear = () => { - this.doc.history.off('stack-item-added', addHandler); - this.doc.history.off('stack-item-popped', popHandler); - disposable.unsubscribe(); - }; - const closeNotify = () => { - abortController.abort(); - clear(); - }; - - const addHandler = this.doc.history.on('stack-item-added', closeNotify); - const popHandler = this.doc.history.on('stack-item-popped', closeNotify); - const disposable = this.edgeless.std - .get(EditorLifeCycleExtension) - .slots.unmounted.subscribe(closeNotify); - - const undo = () => { - this.doc.undo(); - closeNotify(); - }; - - const viewInToc = () => { - const sidebar = this.edgeless.std.getOptional(SidebarExtensionIdentifier); - sidebar?.open('outline'); - closeNotify(); - }; - - const title = - newMode !== NoteDisplayMode.EdgelessOnly - ? 'Note displayed in Page Mode' - : 'Note removed from Page Mode'; - const message = - newMode !== NoteDisplayMode.EdgelessOnly - ? 'Content added to your page.' - : 'Content removed from your page.'; - - const notification = this.edgeless.std.getOptional(NotificationProvider); - notification?.notify({ - title: title, - message: `${message}. Find it in the TOC for quick navigation.`, - accent: 'success', - duration: 5 * 1000, - footer: html`
- - -
`, - abort: abortController.signal, - onClose: () => { - clear(); - }, - }); - - this.edgeless.std - .getOptional(TelemetryProvider) - ?.track('NoteDisplayModeChanged', { - page: 'whiteboard editor', - segment: 'element toolbar', - module: 'element toolbar', - control: 'display mode', - type: 'note', - other: `from ${oldMode} to ${newMode}`, - }); - } - - private _setShadowType(shadowType: string) { - this.notes.forEach(note => { - const props = { - edgeless: { - ...note.props.edgeless, - style: { - ...note.props.edgeless.style, - shadowType, - }, - }, - }; - this.crud.updateElement(note.id, props); - }); - } - - private _setStrokeStyle(borderStyle: StrokeStyle) { - this.notes.forEach(note => { - const props = { - edgeless: { - ...note.props.edgeless, - style: { - ...note.props.edgeless.style, - borderStyle, - }, - }, - }; - this.crud.updateElement(note.id, props); - }); - } - - private _setStrokeWidth(borderSize: number) { - this.notes.forEach(note => { - const props = { - edgeless: { - ...note.props.edgeless, - style: { - ...note.props.edgeless.style, - borderSize, - }, - }, - }; - this.crud.updateElement(note.id, props); - }); - } - - private _setStyles({ type, value }: LineStyleEvent) { - if (type === 'size') { - this._setStrokeWidth(value); - return; - } - if (type === 'lineStyle') { - this._setStrokeStyle(value); - } - } - - override render() { - const len = this.notes.length; - const note = this.notes[0]; - const { edgeless, displayMode } = note.props; - const { shadowType, borderRadius, borderSize, borderStyle } = - edgeless.style; - const colorScheme = this.edgeless.surface.renderer.getColorScheme(); - const background = getMostCommonBackground(this.notes, colorScheme); - - const { collapse } = edgeless; - const scale = edgeless.scale ?? 1; - const currentMode = DisplayModeMap[displayMode]; - const onlyOne = len === 1; - const isDocOnly = displayMode === NoteDisplayMode.DocOnly; - - const hasPageBlockHeader = !!this.edgeless.std.getOptional( - NoteConfigExtension.identifier - )?.edgelessNoteHeader; - - const enableCustomColor = this.edgeless.doc - .get(FeatureFlagService) - .getFlag('enable_color_picker'); - - const theme = this.edgeless.std.get(ThemeProvider).theme; - const buttonIconSize = { width: '20px', height: '20px' }; - const buttons = [ - onlyOne && this._advancedVisibilityEnabled - ? html` - Show in - - ${currentMode} - ${SmallArrowDownIcon} - - `} - > - - this._setDisplayMode(note, newMode)} - > - - - ` - : nothing, - - onlyOne && !note.isPageBlock() && !this._advancedVisibilityEnabled - ? html` - this._setDisplayMode( - note, - displayMode === NoteDisplayMode.EdgelessOnly - ? NoteDisplayMode.DocAndEdgeless - : NoteDisplayMode.EdgelessOnly - )} - > - ${LinkedPageIcon(buttonIconSize)} - ${displayMode === NoteDisplayMode.EdgelessOnly - ? 'Display In Page' - : 'Displayed In Page'} - ` - : nothing, - - isDocOnly - ? nothing - : html` - - - `, - - isDocOnly - ? nothing - : html` - - ${NoteShadowDuotoneIcon(buttonIconSize)}${SmallArrowDownIcon} - - `} - > - this._setShadowType(value)} - > - - - - - ${LineStyleIcon(buttonIconSize)}${SmallArrowDownIcon} - - `} - > -
- ${LineStylesPanel({ - selectedLineSize: borderSize, - selectedLineStyle: borderStyle, - onClick: event => this._setStyles(event), - })} -
-
- - - ${CornerIcon(buttonIconSize)}${SmallArrowDownIcon} - - `} - > - this._setBorderRadius(size)} - .onPopperCose=${() => this._cornersPanelRef.value?.hide()} - > - - - `, - - onlyOne && this._advancedVisibilityEnabled - ? html` -
`} - .active=${this.enableNoteSlicer} - .iconSize=${'20px'} - @click=${() => this._handleNoteSlicerButtonClick()} - > - ${ScissorsIcon()} - - ` - : nothing, - - onlyOne ? this.quickConnectButton : nothing, - - !this.notes[0].isPageBlock() || !hasPageBlockHeader - ? html` this._setCollapse()} - > - ${collapse ? AutoHeightIcon() : CustomizedHeightIcon()} - ` - : nothing, - - html` - - ${this._getScaleLabel(scale)}${SmallArrowDownIcon} - - `} - > - this._setNoteScale(scale)} - .onPopperCose=${() => this._scalePanelRef.value?.hide()} - > - - `, - ]; - - return join( - buttons.filter(button => button !== nothing), - renderToolbarSeparator - ); - } - - private accessor _cornersPanelRef: Ref = createRef(); - - private accessor _scalePanelRef: Ref = createRef(); - - @query('edgeless-color-picker-button.background') - accessor backgroundButton!: EdgelessColorPickerButton; - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor enableNoteSlicer!: boolean; - - @property({ attribute: false }) - accessor notes: NoteBlockModel[] = []; - - @property({ attribute: false }) - accessor quickConnectButton!: TemplateResult<1> | typeof nothing; -} - -export function renderNoteButton( - edgeless: EdgelessRootBlockComponent, - notes?: NoteBlockModel[], - quickConnectButton?: TemplateResult<1>[] -) { - if (!notes?.length) return nothing; - - return html` - - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-shape-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-shape-button.ts deleted file mode 100644 index 40da84f0c6..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-shape-button.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface'; -import type { - EdgelessColorPickerButton, - PickColorEvent, -} from '@blocksuite/affine-components/color-picker'; -import { packColor } from '@blocksuite/affine-components/color-picker'; -import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar'; -import type { - Color, - ColorScheme, - ShapeElementModel, - ShapeProps, -} from '@blocksuite/affine-model'; -import { - DefaultTheme, - FontFamily, - getShapeName, - getShapeRadius, - getShapeType, - isTransparent, - LineWidth, - MindmapElementModel, - resolveColor, - ShapeStyle, - StrokeStyle, -} from '@blocksuite/affine-model'; -import { FeatureFlagService } from '@blocksuite/affine-shared/services'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { - AddTextIcon, - ShapeIcon, - StyleGeneralIcon, - StyleScribbleIcon, -} from '@blocksuite/icons/lit'; -import { css, html, LitElement, nothing, type TemplateResult } from 'lit'; -import { property, query } from 'lit/decorators.js'; -import { cache } from 'lit/directives/cache.js'; -import { choose } from 'lit/directives/choose.js'; -import { join } from 'lit/directives/join.js'; -import { styleMap } from 'lit/directives/style-map.js'; -import countBy from 'lodash-es/countBy'; -import isEqual from 'lodash-es/isEqual'; -import maxBy from 'lodash-es/maxBy'; - -import { - type LineStyleEvent, - LineStylesPanel, -} from '../../edgeless/components/panel/line-styles-panel.js'; -import type { EdgelessShapePanel } from '../../edgeless/components/panel/shape-panel.js'; -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import type { ShapeToolOption } from '../../edgeless/gfx-tool/shape-tool.js'; -import { mountShapeTextEditor } from '../../edgeless/utils/text.js'; -import { SmallArrowDownIcon } from './icons.js'; - -const changeShapeButtonStyles = [ - css` - .edgeless-component-line-size-button { - display: flex; - justify-content: center; - align-items: center; - width: 16px; - height: 16px; - } - - .edgeless-component-line-size-button div { - border-radius: 50%; - background-color: var(--affine-icon-color); - } - - .edgeless-component-line-size-button.size-s div { - width: 4px; - height: 4px; - } - .edgeless-component-line-size-button.size-l div { - width: 10px; - height: 10px; - } - `, -]; - -function getMostCommonFillColor( - elements: ShapeElementModel[], - colorScheme: ColorScheme -): string { - const colors = countBy(elements, (ele: ShapeElementModel) => - ele.filled ? resolveColor(ele.fillColor, colorScheme) : 'transparent' - ); - const max = maxBy(Object.entries(colors), ([_k, count]) => count); - return max - ? (max[0] as string) - : resolveColor(DefaultTheme.shapeFillColor, colorScheme); -} - -function getMostCommonStrokeColor( - elements: ShapeElementModel[], - colorScheme: ColorScheme -): string { - const colors = countBy(elements, (ele: ShapeElementModel) => - resolveColor(ele.strokeColor, colorScheme) - ); - const max = maxBy(Object.entries(colors), ([_k, count]) => count); - return max - ? (max[0] as string) - : resolveColor(DefaultTheme.shapeStrokeColor, colorScheme); -} - -function getMostCommonShape( - elements: ShapeElementModel[] -): ShapeToolOption['shapeName'] | null { - const shapeTypes = countBy(elements, (ele: ShapeElementModel) => - getShapeName(ele.shapeType, ele.radius) - ); - const max = maxBy(Object.entries(shapeTypes), ([_k, count]) => count); - return max ? (max[0] as ShapeToolOption['shapeName']) : null; -} - -function getMostCommonLineSize(elements: ShapeElementModel[]): LineWidth { - const sizes = countBy(elements, (ele: ShapeElementModel) => ele.strokeWidth); - const max = maxBy(Object.entries(sizes), ([_k, count]) => count); - return max ? (Number(max[0]) as LineWidth) : LineWidth.Four; -} - -function getMostCommonLineStyle(elements: ShapeElementModel[]): StrokeStyle { - const sizes = countBy(elements, (ele: ShapeElementModel) => ele.strokeStyle); - const max = maxBy(Object.entries(sizes), ([_k, count]) => count); - return max ? (max[0] as StrokeStyle) : StrokeStyle.Solid; -} - -function getMostCommonShapeStyle(elements: ShapeElementModel[]): ShapeStyle { - const roughnesses = countBy( - elements, - (ele: ShapeElementModel) => ele.shapeStyle - ); - const max = maxBy(Object.entries(roughnesses), ([_k, count]) => count); - return max ? (max[0] as ShapeStyle) : ShapeStyle.Scribbled; -} - -export class EdgelessChangeShapeButton extends WithDisposable(LitElement) { - static override styles = [changeShapeButtonStyles]; - - private readonly _setShapeStyles = ({ type, value }: LineStyleEvent) => { - if (type === 'size') { - this._setShapeStrokeWidth(value); - return; - } - if (type === 'lineStyle') { - this._setShapeStrokeStyle(value); - } - }; - - get service() { - return this.edgeless.service; - } - - get crud() { - return this.edgeless.std.get(EdgelessCRUDIdentifier); - } - - private _addText() { - mountShapeTextEditor(this.elements[0], this.edgeless); - } - - private _getTextColor(fillColor: Color, isNotTransparent = false) { - // When the shape is filled with black color, the text color should be white. - // When the shape is transparent, the text color should be set according to the theme. - // Otherwise, the text color should be black. - - if (isNotTransparent) { - if (isEqual(fillColor, DefaultTheme.black)) { - return DefaultTheme.white; - } else if (isEqual(fillColor, DefaultTheme.white)) { - return DefaultTheme.black; - } else if (isEqual(fillColor, DefaultTheme.pureBlack)) { - return DefaultTheme.pureWhite; - } else if (isEqual(fillColor, DefaultTheme.pureWhite)) { - return DefaultTheme.pureBlack; - } - } - - // aka `DefaultTheme.pureBlack` - return DefaultTheme.shapeTextColor; - } - - private _setShapeStrokeStyle(strokeStyle: StrokeStyle) { - this.elements.forEach(ele => - this.crud.updateElement(ele.id, { strokeStyle }) - ); - } - - private _setShapeStrokeWidth(strokeWidth: number) { - this.elements.forEach(ele => - this.crud.updateElement(ele.id, { strokeWidth }) - ); - } - - private _setShapeStyle(shapeStyle: ShapeStyle) { - const fontFamily = - shapeStyle === ShapeStyle.General ? FontFamily.Inter : FontFamily.Kalam; - - this.elements.forEach(ele => { - this.crud.updateElement(ele.id, { shapeStyle, fontFamily }); - }); - } - - private _showAddButtonOrTextMenu() { - if (this.elements.length === 1 && !this.elements[0].text) { - return 'button'; - } - if (!this.elements.some(e => !e.text)) { - return 'menu'; - } - return 'nothing'; - } - - override firstUpdated() { - const _disposables = this._disposables; - - _disposables.add( - this._shapePanel.slots.select.subscribe(shapeName => { - this.edgeless.doc.captureSync(); - this.elements.forEach(element => { - this.crud.updateElement(element.id, { - shapeType: getShapeType(shapeName), - radius: getShapeRadius(shapeName), - }); - }); - }) - ); - } - - pickColor>( - field: K - ) { - return (e: PickColorEvent) => { - if (e.type === 'pick') { - const value = e.detail.value; - const filled = field === 'fillColor' && !isTransparent(value); - this.elements.forEach(ele => { - const props = packColor(field, value); - // If `filled` can be set separately, this logic can be removed - if (field && !ele.filled) { - const color = this._getTextColor(value, filled); - Object.assign(props, { filled, color }); - } - this.crud.updateElement(ele.id, props); - }); - return; - } - - this.elements.forEach(ele => - ele[e.type === 'start' ? 'stash' : 'pop'](field) - ); - }; - } - - override render() { - const colorScheme = this.edgeless.surface.renderer.getColorScheme(); - const elements = this.elements; - const selectedShape = getMostCommonShape(elements); - const selectedFillColor = getMostCommonFillColor(elements, colorScheme); - const selectedStrokeColor = getMostCommonStrokeColor(elements, colorScheme); - const selectedLineSize = getMostCommonLineSize(elements); - const selectedLineStyle = getMostCommonLineStyle(elements); - const selectedShapeStyle = getMostCommonShapeStyle(elements); - const iconSize = { width: '20px', height: '20px' }; - const enableCustomColor = this.edgeless.doc - .get(FeatureFlagService) - .getFlag('enable_color_picker'); - - return join( - [ - html` - - ${ShapeIcon(iconSize)}${SmallArrowDownIcon} - - `} - > - - - - `, - - html` - - ${cache( - selectedShapeStyle === ShapeStyle.General - ? StyleGeneralIcon(iconSize) - : StyleScribbleIcon(iconSize) - )} - ${SmallArrowDownIcon} - - `} - > - this._setShapeStyle(value)} - > - - - `, - - html` - - - `, - - html` - -
- ${LineStylesPanel({ - selectedLineSize: selectedLineSize, - selectedLineStyle: selectedLineStyle, - onClick: this._setShapeStyles, - })} -
- -
- `, - - choose | typeof nothing>( - this._showAddButtonOrTextMenu(), - [ - [ - 'button', - () => html` - - ${AddTextIcon()} - - `, - ], - [ - 'menu', - () => html` - - `, - ], - ['nothing', () => nothing], - ] - ), - ].filter(button => button !== nothing), - renderToolbarSeparator - ); - } - - @query('edgeless-shape-panel') - private accessor _shapePanel!: EdgelessShapePanel; - - @query('edgeless-color-picker-button.border-style') - accessor borderStyleButton!: EdgelessColorPickerButton; - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor elements: ShapeElementModel[] = []; - - @query('edgeless-color-picker-button.fill-color') - accessor fillColorButton!: EdgelessColorPickerButton; -} - -export function renderChangeShapeButton( - edgeless: EdgelessRootBlockComponent, - elements?: ShapeElementModel[] -) { - if (!elements?.length) return nothing; - if (elements.some(e => e.group instanceof MindmapElementModel)) - return nothing; - - return html` - - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-text-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-text-button.ts deleted file mode 100644 index 77a0bc7359..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-text-button.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { TextElementModel } from '@blocksuite/affine-model'; -import { html, nothing } from 'lit'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; - -export function renderChangeTextButton( - edgeless: EdgelessRootBlockComponent, - elements?: TextElementModel[] -) { - if (!elements?.length) return nothing; - - return html` - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-text-menu.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-text-menu.ts deleted file mode 100644 index 95d0252652..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-text-menu.ts +++ /dev/null @@ -1,474 +0,0 @@ -import { - ConnectorUtils, - EdgelessCRUDIdentifier, - normalizeShapeBound, - TextUtils, -} from '@blocksuite/affine-block-surface'; -import type { - EdgelessColorPickerButton, - PickColorEvent, -} from '@blocksuite/affine-components/color-picker'; -import { packColor } from '@blocksuite/affine-components/color-picker'; -import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar'; -import { - type ColorScheme, - ConnectorElementModel, - DefaultTheme, - EdgelessTextBlockModel, - FontFamily, - FontStyle, - FontWeight, - resolveColor, - ShapeElementModel, - type SurfaceTextModel, - type SurfaceTextModelMap, - TextAlign, - TextElementModel, - type TextStyleProps, -} from '@blocksuite/affine-model'; -import { FeatureFlagService } from '@blocksuite/affine-shared/services'; -import { Bound } from '@blocksuite/global/gfx'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { - TextAlignCenterIcon, - TextAlignLeftIcon, - TextAlignRightIcon, -} from '@blocksuite/icons/lit'; -import { css, html, LitElement, nothing, type TemplateResult } from 'lit'; -import { property, query } from 'lit/decorators.js'; -import { choose } from 'lit/directives/choose.js'; -import { join } from 'lit/directives/join.js'; -import countBy from 'lodash-es/countBy'; -import maxBy from 'lodash-es/maxBy'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import { SmallArrowDownIcon } from './icons.js'; - -const FONT_SIZE_LIST = [ - { value: 16 }, - { value: 24 }, - { value: 32 }, - { value: 40 }, - { value: 64 }, - { value: 128 }, -] as const; - -const FONT_WEIGHT_CHOOSE: [FontWeight, () => string][] = [ - [FontWeight.Light, () => 'Light'], - [FontWeight.Regular, () => 'Regular'], - [FontWeight.SemiBold, () => 'Semibold'], -] as const; - -const FONT_STYLE_CHOOSE: [FontStyle, () => string | typeof nothing][] = [ - [FontStyle.Normal, () => nothing], - [FontStyle.Italic, () => 'Italic'], -] as const; - -const iconSize = { width: '20px', height: '20px' }; -const TEXT_ALIGN_CHOOSE: [TextAlign, () => TemplateResult<1>][] = [ - [TextAlign.Left, () => TextAlignLeftIcon(iconSize)], - [TextAlign.Center, () => TextAlignCenterIcon(iconSize)], - [TextAlign.Right, () => TextAlignRightIcon(iconSize)], -] as const; - -function countByField>( - elements: SurfaceTextModel[], - field: K -) { - return countBy(elements, element => extractField(element, field)); -} - -function extractField>( - element: SurfaceTextModel, - field: K -) { - //TODO: It's not a very good handling method. - // The edgeless-change-text-menu should be refactored into a widget to allow external registration of its own logic. - if (element instanceof EdgelessTextBlockModel) { - return field === 'fontSize' - ? null - : (element[field as keyof EdgelessTextBlockModel] as TextStyleProps[K]); - } - return ( - element instanceof ConnectorElementModel - ? element.labelStyle[field] - : element[field] - ) as TextStyleProps[K]; -} - -function getMostCommonValue>( - elements: SurfaceTextModel[], - field: K -) { - const values = countByField(elements, field); - return maxBy(Object.entries(values), ([_k, count]) => count); -} - -function getMostCommonAlign(elements: SurfaceTextModel[]) { - const max = getMostCommonValue(elements, 'textAlign'); - return max ? (max[0] as TextAlign) : TextAlign.Left; -} - -function getMostCommonColor( - elements: SurfaceTextModel[], - colorScheme: ColorScheme -): string { - const colors = countBy(elements, (ele: SurfaceTextModel) => { - const color = - ele instanceof ConnectorElementModel ? ele.labelStyle.color : ele.color; - return resolveColor(color, colorScheme); - }); - const max = maxBy(Object.entries(colors), ([_k, count]) => count); - return max - ? (max[0] as string) - : resolveColor(DefaultTheme.textColor, colorScheme); -} - -function getMostCommonFontFamily(elements: SurfaceTextModel[]) { - const max = getMostCommonValue(elements, 'fontFamily'); - return max ? (max[0] as FontFamily) : FontFamily.Inter; -} - -function getMostCommonFontSize(elements: SurfaceTextModel[]) { - const max = getMostCommonValue(elements, 'fontSize'); - return max ? Number(max[0]) : FONT_SIZE_LIST[0].value; -} - -function getMostCommonFontStyle(elements: SurfaceTextModel[]) { - const max = getMostCommonValue(elements, 'fontStyle'); - return max ? (max[0] as FontStyle) : FontStyle.Normal; -} - -function getMostCommonFontWeight(elements: SurfaceTextModel[]) { - const max = getMostCommonValue(elements, 'fontWeight'); - return max ? (max[0] as FontWeight) : FontWeight.Regular; -} - -function buildProps( - element: SurfaceTextModel, - props: { [K in keyof TextStyleProps]?: TextStyleProps[K] } -) { - if (element instanceof ConnectorElementModel) { - return { - labelStyle: { - ...element.labelStyle, - ...props, - }, - }; - } - - return { ...props }; -} - -export class EdgelessChangeTextMenu extends WithDisposable(LitElement) { - static override styles = css` - :host { - display: inherit; - align-items: inherit; - justify-content: inherit; - gap: inherit; - height: 100%; - } - `; - - get crud() { - return this.edgeless.std.get(EdgelessCRUDIdentifier); - } - - private readonly _setFontFamily = (fontFamily: FontFamily) => { - const currentFontWeight = getMostCommonFontWeight(this.elements); - const fontWeight = TextUtils.isFontWeightSupported( - fontFamily, - currentFontWeight - ) - ? currentFontWeight - : FontWeight.Regular; - const currentFontStyle = getMostCommonFontStyle(this.elements); - const fontStyle = TextUtils.isFontStyleSupported( - fontFamily, - currentFontStyle - ) - ? currentFontStyle - : FontStyle.Normal; - - const props = { fontFamily, fontWeight, fontStyle }; - this.elements.forEach(element => { - this.crud.updateElement(element.id, buildProps(element, props)); - this._updateElementBound(element); - }); - }; - - private readonly _setFontSize = (fontSize: number) => { - const props = { fontSize }; - this.elements.forEach(element => { - this.crud.updateElement(element.id, buildProps(element, props)); - this._updateElementBound(element); - }); - }; - - private readonly _setFontWeightAndStyle = ( - fontWeight: FontWeight, - fontStyle: FontStyle - ) => { - const props = { fontWeight, fontStyle }; - this.elements.forEach(element => { - this.crud.updateElement(element.id, buildProps(element, props)); - this._updateElementBound(element); - }); - }; - - private readonly _setTextAlign = (textAlign: TextAlign) => { - const props = { textAlign }; - this.elements.forEach(element => { - this.crud.updateElement(element.id, buildProps(element, props)); - }); - }; - - private readonly _updateElementBound = (element: SurfaceTextModel) => { - const elementType = this.elementType; - if (elementType === 'text' && element instanceof TextElementModel) { - // the change of font family will change the bound of the text - const { - text: yText, - fontFamily, - fontStyle, - fontSize, - fontWeight, - hasMaxWidth, - } = element; - const newBound = TextUtils.normalizeTextBound( - { - yText, - fontFamily, - fontStyle, - fontSize, - fontWeight, - hasMaxWidth, - }, - Bound.fromXYWH(element.deserializedXYWH) - ); - this.crud.updateElement(element.id, { - xywh: newBound.serialize(), - }); - } else if ( - elementType === 'connector' && - ConnectorUtils.isConnectorWithLabel(element) - ) { - const { - text, - labelXYWH, - labelStyle: { fontFamily, fontStyle, fontSize, fontWeight }, - labelConstraints: { hasMaxWidth, maxWidth }, - } = element as ConnectorElementModel; - const prevBounds = Bound.fromXYWH(labelXYWH || [0, 0, 16, 16]); - const center = prevBounds.center; - const bounds = TextUtils.normalizeTextBound( - { - yText: text!, - fontFamily, - fontStyle, - fontSize, - fontWeight, - hasMaxWidth, - maxWidth, - }, - prevBounds - ); - bounds.center = center; - this.crud.updateElement(element.id, { - labelXYWH: bounds.toXYWH(), - }); - } else if ( - elementType === 'shape' && - element instanceof ShapeElementModel - ) { - const newBound = normalizeShapeBound( - element, - Bound.fromXYWH(element.deserializedXYWH) - ); - this.crud.updateElement(element.id, { - xywh: newBound.serialize(), - }); - } - // no need to update the bound of edgeless text block, which updates itself using ResizeObserver - }; - - pickColor = (e: PickColorEvent) => { - if (e.type === 'pick') { - const color = e.detail.value; - this.elements.forEach(element => { - const props = packColor('color', color); - this.crud.updateElement(element.id, buildProps(element, props)); - this._updateElementBound(element); - }); - return; - } - - const key = this.elementType === 'connector' ? 'labelStyle' : 'color'; - this.elements.forEach(ele => { - ele[e.type === 'start' ? 'stash' : 'pop'](key as 'color'); - }); - }; - - get service() { - return this.edgeless.service; - } - - override render() { - const colorScheme = this.edgeless.surface.renderer.getColorScheme(); - const elements = this.elements; - const selectedAlign = getMostCommonAlign(elements); - const selectedColor = getMostCommonColor(elements, colorScheme); - const selectedFontFamily = getMostCommonFontFamily(elements); - const selectedFontSize = Math.trunc(getMostCommonFontSize(elements)); - const selectedFontStyle = getMostCommonFontStyle(elements); - const selectedFontWeight = getMostCommonFontWeight(elements); - const matchFontFaces = - TextUtils.getFontFacesByFontFamily(selectedFontFamily); - const fontStyleBtnDisabled = - matchFontFaces.length === 1 && - matchFontFaces[0].style === selectedFontStyle && - matchFontFaces[0].weight === selectedFontWeight; - const palettes = - this.elementType === 'shape' - ? DefaultTheme.ShapeTextColorPalettes - : DefaultTheme.Palettes; - const enableCustomColor = this.edgeless.doc - .get(FeatureFlagService) - .getFlag('enable_color_picker'); - - return join( - [ - html` - - Aa${SmallArrowDownIcon} - - `} - > - - - `, - - html` - - - `, - - html` - - - ${choose(selectedFontWeight, FONT_WEIGHT_CHOOSE)} - ${choose(selectedFontStyle, FONT_STYLE_CHOOSE)} - - ${SmallArrowDownIcon} - - `} - > - - - `, - - this.elementType === 'edgeless-text' - ? nothing - : html` - - ${selectedFontSize} - ${SmallArrowDownIcon} - - `} - > - - - `, - - html` - - ${choose(selectedAlign, TEXT_ALIGN_CHOOSE)}${SmallArrowDownIcon} - - `} - > - - - `, - ].filter(b => b !== nothing), - renderToolbarSeparator - ); - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor elements!: SurfaceTextModel[]; - - @property({ attribute: false }) - accessor elementType!: keyof SurfaceTextModelMap; - - @query('edgeless-color-picker-button.text-color') - accessor textColorButton!: EdgelessColorPickerButton; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/effects.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/effects.ts deleted file mode 100644 index cf611eaca3..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/effects.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { EdgelessAddFrameButton } from './add-frame-button.js'; -import { EdgelessAddGroupButton } from './add-group-button.js'; -import { EdgelessAlignButton } from './align-button.js'; -import { EdgelessChangeAttachmentButton } from './change-attachment-button.js'; -import { EdgelessChangeBrushButton } from './change-brush-button.js'; -import { EdgelessChangeConnectorButton } from './change-connector-button.js'; -import { EdgelessChangeEmbedCardButton } from './change-embed-card-button.js'; -import { EdgelessChangeFrameButton } from './change-frame-button.js'; -import { EdgelessChangeGroupButton } from './change-group-button.js'; -import { EdgelessChangeImageButton } from './change-image-button.js'; -import { - EdgelessChangeMindmapButton, - EdgelessChangeMindmapLayoutPanel, - EdgelessChangeMindmapStylePanel, -} from './change-mindmap-button.js'; -import { EdgelessChangeNoteButton } from './change-note-button.js'; -import { EdgelessChangeShapeButton } from './change-shape-button.js'; -import { EdgelessChangeTextMenu } from './change-text-menu.js'; -import { - EDGELESS_ELEMENT_TOOLBAR_WIDGET, - EdgelessElementToolbarWidget, -} from './index.js'; -import { EdgelessLockButton } from './lock-button.js'; -import { EdgelessMoreButton } from './more-menu/button.js'; -import { EdgelessReleaseFromGroupButton } from './release-from-group-button.js'; - -export function effects() { - customElements.define( - EDGELESS_ELEMENT_TOOLBAR_WIDGET, - EdgelessElementToolbarWidget - ); - customElements.define('edgeless-add-frame-button', EdgelessAddFrameButton); - customElements.define('edgeless-add-group-button', EdgelessAddGroupButton); - customElements.define('edgeless-align-button', EdgelessAlignButton); - customElements.define( - 'edgeless-change-attachment-button', - EdgelessChangeAttachmentButton - ); - customElements.define( - 'edgeless-change-brush-button', - EdgelessChangeBrushButton - ); - customElements.define( - 'edgeless-change-connector-button', - EdgelessChangeConnectorButton - ); - customElements.define( - 'edgeless-change-embed-card-button', - EdgelessChangeEmbedCardButton - ); - customElements.define( - 'edgeless-change-frame-button', - EdgelessChangeFrameButton - ); - customElements.define( - 'edgeless-change-group-button', - EdgelessChangeGroupButton - ); - customElements.define( - 'edgeless-change-image-button', - EdgelessChangeImageButton - ); - customElements.define( - 'edgeless-change-mindmap-style-panel', - EdgelessChangeMindmapStylePanel - ); - customElements.define( - 'edgeless-change-mindmap-layout-panel', - EdgelessChangeMindmapLayoutPanel - ); - customElements.define( - 'edgeless-change-mindmap-button', - EdgelessChangeMindmapButton - ); - customElements.define( - 'edgeless-change-note-button', - EdgelessChangeNoteButton - ); - customElements.define( - 'edgeless-change-shape-button', - EdgelessChangeShapeButton - ); - customElements.define('edgeless-change-text-menu', EdgelessChangeTextMenu); - customElements.define( - 'edgeless-release-from-group-button', - EdgelessReleaseFromGroupButton - ); - customElements.define('edgeless-more-button', EdgelessMoreButton); - customElements.define('edgeless-lock-button', EdgelessLockButton); -} - -declare global { - interface HTMLElementTagNameMap { - [EDGELESS_ELEMENT_TOOLBAR_WIDGET]: EdgelessElementToolbarWidget; - 'edgeless-add-frame-button': EdgelessAddFrameButton; - 'edgeless-add-group-button': EdgelessAddGroupButton; - 'edgeless-align-button': EdgelessAlignButton; - 'edgeless-change-attachment-button': EdgelessChangeAttachmentButton; - 'edgeless-change-brush-button': EdgelessChangeBrushButton; - 'edgeless-change-connector-button': EdgelessChangeConnectorButton; - 'edgeless-change-embed-card-button': EdgelessChangeEmbedCardButton; - 'edgeless-change-frame-button': EdgelessChangeFrameButton; - 'edgeless-change-group-button': EdgelessChangeGroupButton; - 'edgeless-change-mindmap-style-panel': EdgelessChangeMindmapStylePanel; - 'edgeless-change-mindmap-layout-panel': EdgelessChangeMindmapLayoutPanel; - 'edgeless-change-mindmap-button': EdgelessChangeMindmapButton; - 'edgeless-change-note-button': EdgelessChangeNoteButton; - 'edgeless-change-shape-button': EdgelessChangeShapeButton; - 'edgeless-change-text-menu': EdgelessChangeTextMenu; - 'edgeless-release-from-group-button': EdgelessReleaseFromGroupButton; - 'edgeless-more-button': EdgelessMoreButton; - 'edgeless-lock-button': EdgelessLockButton; - } -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/icons.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/icons.ts deleted file mode 100644 index a2644a3b76..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/icons.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ArrowDownSmallIcon } from '@blocksuite/icons/lit'; - -export const SmallArrowDownIcon = ArrowDownSmallIcon({ - width: '16', - height: '16', -}); diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/index.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/index.ts deleted file mode 100644 index d932fd2cb1..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/index.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { isFrameBlock } from '@blocksuite/affine-block-frame'; -import { isNoteBlock } from '@blocksuite/affine-block-surface'; -import { - cloneGroups, - darkToolbarStyles, - getMoreMenuConfig, - lightToolbarStyles, - type MenuItemGroup, - renderToolbarSeparator, -} from '@blocksuite/affine-components/toolbar'; -import type { - AttachmentBlockModel, - BrushElementModel, - BuiltInEmbedModel, - ConnectorElementModel, - EdgelessTextBlockModel, - FrameBlockModel, - ImageBlockModel, - MindmapElementModel, - NoteBlockModel, - RootBlockModel, - TextElementModel, -} from '@blocksuite/affine-model'; -import { - ConnectorMode, - GroupElementModel, - ShapeElementModel, -} from '@blocksuite/affine-model'; -import { ThemeProvider } from '@blocksuite/affine-shared/services'; -import { requestConnectedFrame } from '@blocksuite/affine-shared/utils'; -import { WidgetComponent } from '@blocksuite/block-std'; -import type { GfxModel } from '@blocksuite/block-std/gfx'; -import { clamp, getCommonBoundWithRotation } from '@blocksuite/global/gfx'; -import { ConnectorCIcon } from '@blocksuite/icons/lit'; -import { css, html, nothing, type TemplateResult, unsafeCSS } from 'lit'; -import { property, state } from 'lit/decorators.js'; -import { join } from 'lit/directives/join.js'; -import groupBy from 'lodash-es/groupBy'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import { - isAttachmentBlock, - isBookmarkBlock, - isEdgelessTextBlock, - isEmbeddedBlock, - isImageBlock, -} from '../../edgeless/utils/query.js'; -import { renderAddFrameButton } from './add-frame-button.js'; -import { renderAddGroupButton } from './add-group-button.js'; -import { renderAlignButton } from './align-button.js'; -import { renderAttachmentButton } from './change-attachment-button.js'; -import { renderChangeBrushButton } from './change-brush-button.js'; -import { renderConnectorButton } from './change-connector-button.js'; -import { renderChangeEdgelessTextButton } from './change-edgeless-text-button.js'; -import { renderEmbedButton } from './change-embed-card-button.js'; -import { renderFrameButton } from './change-frame-button.js'; -import { renderGroupButton } from './change-group-button.js'; -import { renderChangeImageButton } from './change-image-button.js'; -import { renderMindmapButton } from './change-mindmap-button.js'; -import { renderNoteButton } from './change-note-button.js'; -import { renderChangeShapeButton } from './change-shape-button.js'; -import { renderChangeTextButton } from './change-text-button.js'; -import { BUILT_IN_GROUPS } from './more-menu/config.js'; -import type { ElementToolbarMoreMenuContext } from './more-menu/context.js'; -import { renderReleaseFromGroupButton } from './release-from-group-button.js'; - -type CategorizedElements = { - shape?: ShapeElementModel[]; - brush?: BrushElementModel[]; - text?: TextElementModel[]; - group?: GroupElementModel[]; - connector?: ConnectorElementModel[]; - note?: NoteBlockModel[]; - frame?: FrameBlockModel[]; - image?: ImageBlockModel[]; - attachment?: AttachmentBlockModel[]; - mindmap?: MindmapElementModel[]; - embedCard?: BuiltInEmbedModel[]; - edgelessText?: EdgelessTextBlockModel[]; -}; - -type CustomEntry = { - render: (edgeless: EdgelessRootBlockComponent) => TemplateResult | null; - when: (model: GfxModel[]) => boolean; -}; - -export const EDGELESS_ELEMENT_TOOLBAR_WIDGET = - 'edgeless-element-toolbar-widget'; - -export class EdgelessElementToolbarWidget extends WidgetComponent< - RootBlockModel, - EdgelessRootBlockComponent -> { - static override styles = css` - :host { - position: absolute; - z-index: 3; - transform: translateZ(0); - will-change: transform; - -webkit-user-select: none; - user-select: none; - } - editor-toolbar[data-app-theme='light'] { - ${unsafeCSS(lightToolbarStyles.join('\n'))} - } - editor-toolbar[data-app-theme='dark'] { - ${unsafeCSS(darkToolbarStyles.join('\n'))} - } - `; - - private readonly _quickConnect = ({ x, y }: MouseEvent) => { - const element = this.selection.selectedElements[0]; - const point = this.edgeless.service.viewport.toViewCoordFromClientCoord([ - x, - y, - ]); - this.edgeless.doc.captureSync(); - this.edgeless.gfx.tool.setTool('connector', { - mode: ConnectorMode.Curve, - }); - - const ctc = this.edgeless.gfx.tool.get('connector'); - ctc.quickConnect(point, element); - }; - - private readonly _updateOnSelectedChange = ( - element: string | { id: string } - ) => { - const id = typeof element === 'string' ? element : element.id; - - if (this.isConnected && !this._dragging && this.selection.has(id)) { - this._recalculatePosition(); - this.requestUpdate(); - } - }; - - /* - * Caches the more menu items. - * Currently only supports configuring more menu. - */ - moreGroups: MenuItemGroup[] = - cloneGroups(BUILT_IN_GROUPS); - - get edgeless() { - return this.block as EdgelessRootBlockComponent; - } - - get selection() { - return this.edgeless.service.selection; - } - - get slots() { - return this.edgeless.slots; - } - - get surface() { - return this.edgeless.surface; - } - - private _groupSelected(): CategorizedElements { - const result = groupBy(this.selection.selectedElements, model => { - if (isNoteBlock(model)) { - return 'note'; - } else if (isFrameBlock(model)) { - return 'frame'; - } else if (isImageBlock(model)) { - return 'image'; - } else if (isAttachmentBlock(model)) { - return 'attachment'; - } else if (isBookmarkBlock(model) || isEmbeddedBlock(model)) { - return 'embedCard'; - } else if (isEdgelessTextBlock(model)) { - return 'edgelessText'; - } - - return model.type; - }); - return result as CategorizedElements; - } - - private _recalculatePosition() { - const { selection, viewport } = this.edgeless.service; - const elements = selection.selectedElements; - - if (elements.length === 0) { - this.style.transform = 'translate3d(0, 0, 0)'; - return; - } - - const bound = getCommonBoundWithRotation(elements); - - const { width, height } = viewport; - const { x, y, w } = viewport.toViewBound(bound); - - let left = x; - let top = y; - - const hasLocked = elements.some(e => e.isLocked()); - - let offset = 37 + 12; - // frame, group, shape - let hasFrame = false; - let hasGroup = false; - if ( - (hasFrame = elements.some(ele => isFrameBlock(ele))) || - (hasGroup = elements.some(ele => ele instanceof GroupElementModel)) - ) { - offset += 16 + 4; - if (hasFrame) { - offset += 8; - } - } else if ( - elements.length === 1 && - elements[0] instanceof ShapeElementModel - ) { - offset += 22 + 4; - } - - top = y - offset; - if (top < 0) { - top = y + bound.h * viewport.zoom + offset - 37; - if (hasFrame || hasGroup) { - top -= 16 + 4; - if (hasFrame) { - top -= 8; - } - } - } - - requestConnectedFrame(() => { - const rect = this.getBoundingClientRect(); - - if (hasLocked) { - left += 0.5 * (w - rect.width); - } - - left = clamp(left, 10, width - rect.width - 10); - top = clamp(top, 10, height - rect.height - 150); - - this.style.transform = `translate3d(${left}px, ${top}px, 0)`; - }, this); - } - - private _renderButtons() { - if (this.doc.readonly || this._dragging || !this.toolbarVisible) { - return []; - } - const { selectedElements } = this.selection; - if (selectedElements.some(e => e.isLocked())) { - return [ - html``, - ]; - } - - const groupedSelected = this._groupSelected(); - const { edgeless, selection } = this; - const { - shape, - brush, - connector, - note, - text, - frame, - group, - embedCard, - attachment, - image, - edgelessText, - mindmap: mindmaps, - } = groupedSelected; - const selectedAtLeastTwoTypes = - Object.values(groupedSelected).filter(e => !!e.length).length >= 2; - - const quickConnectButton = - selectedElements.length === 1 && !connector?.length - ? this._renderQuickConnectButton() - : undefined; - - const generalButtons = - selectedElements.length !== connector?.length - ? [ - renderAddFrameButton(edgeless, selectedElements), - renderAddGroupButton(edgeless, selectedElements), - renderAlignButton(edgeless, selectedElements), - ] - : []; - - const buttons: (symbol | TemplateResult)[] = selectedAtLeastTwoTypes - ? generalButtons - : [ - ...generalButtons, - renderMindmapButton(edgeless, mindmaps), - renderMindmapButton(edgeless, shape), - renderChangeShapeButton(edgeless, shape), - renderChangeBrushButton(edgeless, brush), - renderConnectorButton(edgeless, connector), - renderNoteButton(edgeless, note, quickConnectButton), - renderChangeTextButton(edgeless, text), - renderChangeEdgelessTextButton(edgeless, edgelessText), - renderFrameButton(edgeless, frame), - renderGroupButton(edgeless, group), - renderEmbedButton(edgeless, embedCard, quickConnectButton), - renderAttachmentButton(edgeless, attachment), - renderChangeImageButton(edgeless, image), - ]; - - if (selectedElements.length === 1) { - if (selection.firstElement.group instanceof GroupElementModel) { - buttons.unshift(renderReleaseFromGroupButton(this.edgeless)); - } - - if (!connector?.length) { - buttons.push(quickConnectButton?.pop() ?? nothing); - } - } - - buttons.push( - html`` - ); - - this._registeredEntries - .filter(entry => entry.when(selectedElements)) - .map(entry => entry.render(this.edgeless)) - .forEach(entry => entry && buttons.unshift(entry)); - - buttons.push(html` - - `); - - return buttons; - } - - private _renderQuickConnectButton() { - return [ - html` - - ${ConnectorCIcon()} - - `, - ]; - } - - protected override firstUpdated() { - const { _disposables, edgeless } = this; - - this.moreGroups = getMoreMenuConfig(this.std).configure(this.moreGroups); - - _disposables.add( - edgeless.service.viewport.viewportUpdated.subscribe(() => { - this._recalculatePosition(); - }) - ); - - _disposables.add( - this.selection.slots.updated.subscribe(() => { - if ( - this.selection.selectedIds.length === 0 || - this.selection.editing || - this.selection.inoperable - ) { - this.toolbarVisible = false; - } else { - this.selectedIds = this.selection.selectedIds; - this._recalculatePosition(); - this.toolbarVisible = true; - } - }) - ); - - _disposables.add( - this.edgeless.service.surface.elementAdded.subscribe( - this._updateOnSelectedChange - ) - ); - _disposables.add( - this.edgeless.service.surface.elementUpdated.subscribe( - this._updateOnSelectedChange - ) - ); - - _disposables.add( - this.doc.slots.blockUpdated.subscribe(this._updateOnSelectedChange) - ); - - _disposables.add( - edgeless.dispatcher.add('dragStart', () => { - this._dragging = true; - }) - ); - _disposables.add( - edgeless.dispatcher.add('dragEnd', () => { - this._dragging = false; - this._recalculatePosition(); - }) - ); - - _disposables.add( - edgeless.slots.elementResizeStart.subscribe(() => { - this._dragging = true; - }) - ); - _disposables.add( - edgeless.slots.elementResizeEnd.subscribe(() => { - this._dragging = false; - this._recalculatePosition(); - }) - ); - - _disposables.add( - edgeless.slots.readonlyUpdated.subscribe(() => this.requestUpdate()) - ); - - this.updateComplete - .then(() => { - _disposables.add( - this.std - .get(ThemeProvider) - .theme$.subscribe(() => this.requestUpdate()) - ); - }) - .catch(console.error); - } - - registerEntry(entry: CustomEntry) { - this._registeredEntries.push(entry); - } - - override render() { - const buttons = this._renderButtons(); - if (buttons.length === 0) return nothing; - - const appTheme = this.std.get(ThemeProvider).app$.value; - return html` - - ${join( - buttons.filter(b => b !== nothing), - renderToolbarSeparator - )} - - `; - } - - @state() - private accessor _dragging = false; - - @state() - private accessor _registeredEntries: { - render: (edgeless: EdgelessRootBlockComponent) => TemplateResult | null; - when: (model: GfxModel[]) => boolean; - }[] = []; - - @property({ attribute: false }) - accessor enableNoteSlicer!: boolean; - - @state({ - hasChanged: (value: string[], oldValue: string[]) => { - if (value.length !== oldValue?.length) { - return true; - } - - return value.some((id, index) => id !== oldValue[index]); - }, - }) - accessor selectedIds: string[] = []; - - @state() - accessor toolbarVisible = false; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/lock-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/lock-button.ts deleted file mode 100644 index 9357a849b8..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/lock-button.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { - GroupElementModel, - MindmapElementModel, -} from '@blocksuite/affine-model'; -import { - type ElementLockEvent, - TelemetryProvider, -} from '@blocksuite/affine-shared/services'; -import type { BlockStdScope } from '@blocksuite/block-std'; -import type { GfxModel } from '@blocksuite/block-std/gfx'; -import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; -import { LockIcon, UnlockIcon } from '@blocksuite/icons/lit'; -import { html, LitElement, nothing } from 'lit'; -import { property } from 'lit/decorators.js'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/index.js'; - -export class EdgelessLockButton extends SignalWatcher( - WithDisposable(LitElement) -) { - private get _selectedElements() { - const elements = new Set(); - this.edgeless.service.selection.selectedElements.forEach(element => { - if (element.group instanceof MindmapElementModel) { - elements.add(element.group); - } else { - elements.add(element); - } - }); - return [...elements]; - } - - private _lock() { - const { service, doc, std } = this.edgeless; - - // get most top selected elements(*) from tree, like in a tree below - // G0 - // / \ - // E1* G1 - // / \ - // E2* E3* - // - // (*) selected elements, [E1, E2, E3] - // return [E1] - - const selectedElements = this._selectedElements; - if (selectedElements.length === 0) return; - - const levels = selectedElements.map(element => element.groups.length); - const topElement = selectedElements[levels.indexOf(Math.min(...levels))]; - const otherElements = selectedElements.filter( - element => element !== topElement - ); - - doc.captureSync(); - - // release other elements from their groups and group with top element - otherElements.forEach(element => { - // oxlint-disable-next-line unicorn/prefer-dom-node-remove - element.group?.removeChild(element); - topElement.group?.addChild(element); - }); - - if (otherElements.length === 0) { - topElement.lock(); - this.edgeless.gfx.selection.set({ - editing: false, - elements: [topElement.id], - }); - track(std, topElement, 'lock'); - return; - } - - const groupId = service.createGroup([topElement, ...otherElements]); - - if (groupId) { - const group = service.crud.getElementById(groupId); - if (group) { - group.lock(); - this.edgeless.gfx.selection.set({ - editing: false, - elements: [groupId], - }); - track(std, group, 'group-lock'); - return; - } - } - - selectedElements.forEach(e => { - e.lock(); - track(std, e, 'lock'); - }); - - this.edgeless.gfx.selection.set({ - editing: false, - elements: selectedElements.map(e => e.id), - }); - } - - private _unlock() { - const { service, doc } = this.edgeless; - - const selectedElements = this._selectedElements; - if (selectedElements.length === 0) return; - - doc.captureSync(); - - selectedElements.forEach(element => { - if (element instanceof GroupElementModel) { - service.ungroup(element); - } else { - element.lockedBySelf = false; - } - track(this.edgeless.std, element, 'unlock'); - }); - } - - override render() { - const hasLocked = this._selectedElements.some(element => - element.isLocked() - ); - - this.dataset.locked = hasLocked ? 'true' : 'false'; - - const icon = hasLocked ? UnlockIcon : LockIcon; - - return html` - ${icon({ width: '20px', height: '20px' })} - ${hasLocked - ? html`Click to unlock` - : nothing} - `; - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; -} - -function track( - std: BlockStdScope, - element: GfxModel, - control: ElementLockEvent['control'] -) { - const type = - 'flavour' in element - ? (element.flavour.split(':')[1] ?? element.flavour) - : element.type; - - std.getOptional(TelemetryProvider)?.track('EdgelessElementLocked', { - page: 'whiteboard editor', - segment: 'element toolbar', - module: 'element toolbar', - control, - type, - }); -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/button.ts deleted file mode 100644 index 44d0a0a1c9..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/button.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar'; -import { renderGroups } from '@blocksuite/affine-components/toolbar'; -import type { GfxModel } from '@blocksuite/block-std/gfx'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { MoreHorizontalIcon, MoreVerticalIcon } from '@blocksuite/icons/lit'; -import { html, LitElement } from 'lit'; -import { property } from 'lit/decorators.js'; - -import type { EdgelessRootBlockComponent } from '../../../edgeless/edgeless-root-block.js'; -import { ElementToolbarMoreMenuContext } from './context.js'; - -export class EdgelessMoreButton extends WithDisposable(LitElement) { - override render() { - const context = new ElementToolbarMoreMenuContext(this.edgeless); - const actions = renderGroups(this.groups, context); - - return html` - - ${this.vertical - ? MoreVerticalIcon({ width: '20', height: '20' }) - : MoreHorizontalIcon({ width: '20', height: '20' })} - - `} - > -
- ${actions} -
-
- `; - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor elements: GfxModel[] = []; - - @property({ attribute: false }) - accessor groups!: MenuItemGroup[]; - - @property({ attribute: false }) - accessor vertical = false; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/config.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/config.ts deleted file mode 100644 index 617b11489e..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/config.ts +++ /dev/null @@ -1,459 +0,0 @@ -import type { AttachmentBlockComponent } from '@blocksuite/affine-block-attachment'; -import type { BookmarkBlockComponent } from '@blocksuite/affine-block-bookmark'; -import { - type EmbedFigmaBlockComponent, - type EmbedGithubBlockComponent, - type EmbedLoomBlockComponent, - type EmbedYoutubeBlockComponent, - notifyDocCreated, - promptDocTitle, -} from '@blocksuite/affine-block-embed'; -import type { ImageBlockComponent } from '@blocksuite/affine-block-image'; -import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface'; -import { isPeekable, peek } from '@blocksuite/affine-components/peek'; -import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar'; -import { - OpenDocExtensionIdentifier, - TelemetryProvider, -} from '@blocksuite/affine-shared/services'; -import { Bound, getCommonBoundWithRotation } from '@blocksuite/global/gfx'; -import { - ArrowDownBigBottomIcon, - ArrowDownBigIcon, - ArrowUpBigIcon, - ArrowUpBigTopIcon, - CenterPeekIcon, - CopyIcon, - DeleteIcon, - DuplicateIcon, - ExpandFullIcon, - FrameIcon, - GroupIcon, - LinkedPageIcon, - OpenInNewIcon, - ResetIcon, - SplitViewIcon, -} from '@blocksuite/icons/lit'; - -import { duplicate } from '../../../edgeless/utils/clipboard-utils.js'; -import { getSortedCloneElements } from '../../../edgeless/utils/clone-utils.js'; -import { moveConnectors } from '../../../edgeless/utils/connector.js'; -import { deleteElements } from '../../../edgeless/utils/crud.js'; -import type { ElementToolbarMoreMenuContext } from './context.js'; -import { - createLinkedDocFromEdgelessElements, - createLinkedDocFromNote, -} from './render-linked-doc.js'; - -type EmbedLinkBlockComponent = - | EmbedGithubBlockComponent - | EmbedFigmaBlockComponent - | EmbedLoomBlockComponent - | EmbedYoutubeBlockComponent; - -type RefreshableBlockComponent = - | EmbedLinkBlockComponent - | ImageBlockComponent - | AttachmentBlockComponent - | BookmarkBlockComponent; - -// Section Group: frame & group -export const sectionGroup: MenuItemGroup = { - type: 'section', - items: [ - { - icon: FrameIcon({ width: '20', height: '20' }), - label: 'Frame section', - type: 'create-frame', - action: ({ service, edgeless, std }) => { - const frame = service.frame.createFrameOnSelected(); - if (!frame) return; - - std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', { - control: 'context-menu', - page: 'whiteboard editor', - module: 'toolbar', - segment: 'toolbar', - type: 'frame', - }); - - edgeless.surface.fitToViewport(Bound.deserialize(frame.xywh)); - }, - }, - { - icon: GroupIcon({ width: '20', height: '20' }), - label: 'Group section', - type: 'create-group', - action: ({ service }) => { - service.createGroupFromSelected(); - }, - when: ctx => !ctx.hasFrame(), - }, - ], -}; - -// Reorder Group -export const reorderGroup: MenuItemGroup = { - type: 'reorder', - items: [ - { - icon: ArrowUpBigTopIcon({ width: '20', height: '20' }), - label: 'Bring to Front', - type: 'front', - action: ({ service, selectedElements }) => { - selectedElements.forEach(el => { - service.reorderElement(el, 'front'); - }); - }, - }, - { - icon: ArrowUpBigIcon({ width: '20', height: '20' }), - label: 'Bring Forward', - type: 'forward', - action: ({ service, selectedElements }) => { - selectedElements.forEach(el => { - service.reorderElement(el, 'forward'); - }); - }, - }, - { - icon: ArrowDownBigIcon({ width: '20', height: '20' }), - label: 'Send Backward', - type: 'backward', - action: ({ service, selectedElements }) => { - selectedElements.forEach(el => { - service.reorderElement(el, 'backward'); - }); - }, - }, - { - icon: ArrowDownBigBottomIcon({ width: '20', height: '20' }), - label: 'Send to Back', - type: 'back', - action: ({ service, selectedElements }) => { - selectedElements.forEach(el => { - service.reorderElement(el, 'back'); - }); - }, - }, - ], -}; - -// Open Group -// TODO: construct this group dynamically -export const openGroup: MenuItemGroup = { - type: 'open', - items: [ - { - icon: ExpandFullIcon({ width: '20', height: '20' }), - label: 'Open this doc', - type: 'open', - generate: ctx => { - const linkedDocBlock = ctx.getLinkedDocBlock(); - - if (!linkedDocBlock) return; - - const disabled = linkedDocBlock.props.pageId === ctx.doc.id; - - return { - action: () => { - const blockComponent = ctx.firstBlockComponent; - - if (!blockComponent) return; - if (!('open' in blockComponent)) return; - if (typeof blockComponent.open !== 'function') return; - - blockComponent.open(); - }, - - disabled, - }; - }, - when: ctx => { - const openDocService = ctx.std.get(OpenDocExtensionIdentifier); - return openDocService.isAllowed('open-in-active-view'); - }, - }, - { - icon: SplitViewIcon({ width: '20', height: '20' }), - label: 'Open in split view', - type: 'open-in-split-view', - generate: ctx => { - const linkedDocBlock = ctx.getLinkedDocBlock(); - - if (!linkedDocBlock) return; - - return { - action: () => { - const blockComponent = ctx.firstBlockComponent; - - if (!blockComponent) return; - if (!('open' in blockComponent)) return; - if (typeof blockComponent.open !== 'function') return; - - blockComponent.open({ openMode: 'open-in-new-view' }); - }, - }; - }, - when: ctx => { - const openDocService = ctx.std.get(OpenDocExtensionIdentifier); - return openDocService.isAllowed('open-in-new-view'); - }, - }, - { - icon: OpenInNewIcon({ width: '20', height: '20' }), - label: 'Open in new tab', - type: 'open-in-new-tab', - generate: ctx => { - const linkedDocBlock = ctx.getLinkedDocBlock(); - - if (!linkedDocBlock) return; - - return { - action: () => { - const blockComponent = ctx.firstBlockComponent; - - if (!blockComponent) return; - if (!('open' in blockComponent)) return; - if (typeof blockComponent.open !== 'function') return; - - blockComponent.open({ openMode: 'open-in-new-tab' }); - }, - }; - }, - when: ctx => { - const openDocService = ctx.std.get(OpenDocExtensionIdentifier); - return openDocService.isAllowed('open-in-new-tab'); - }, - }, - { - icon: CenterPeekIcon({ width: '20', height: '20' }), - label: 'Open in center peek', - type: 'center-peek', - generate: ctx => { - const valid = - ctx.isSingle() && - !!ctx.firstBlockComponent && - isPeekable(ctx.firstBlockComponent); - - if (!valid) return; - - return { - action: () => { - if (!ctx.firstBlockComponent) return; - - peek(ctx.firstBlockComponent); - }, - }; - }, - when: ctx => { - const openDocService = ctx.std.get(OpenDocExtensionIdentifier); - return openDocService.isAllowed('open-in-center-peek'); - }, - }, - ], -}; - -// Clipboard Group -export const clipboardGroup: MenuItemGroup = { - type: 'clipboard', - items: [ - { - icon: CopyIcon({ width: '20', height: '20' }), - label: 'Copy', - type: 'copy', - action: ({ edgeless }) => edgeless.clipboardController.copy(), - }, - { - icon: DuplicateIcon({ width: '20', height: '20' }), - label: 'Duplicate', - type: 'duplicate', - action: ({ edgeless, selectedElements }) => - duplicate(edgeless, selectedElements), - }, - { - icon: ResetIcon({ width: '20', height: '20' }), - label: 'Reload', - type: 'reload', - generate: ctx => { - if (ctx.hasFrame()) { - return; - } - - const blocks = ctx.selection.surfaceSelections - .map(s => ctx.getBlockComponent(s.blockId)) - .filter(block => !!block) - .filter(block => ctx.refreshable(block.model)); - - if ( - !blocks.length || - blocks.length !== ctx.selection.surfaceSelections.length - ) { - return; - } - - return { - action: () => - blocks.forEach(block => - (block as RefreshableBlockComponent).refreshData() - ), - }; - }, - }, - ], -}; - -// Conversions Group -export const conversionsGroup: MenuItemGroup = { - type: 'conversions', - items: [ - { - icon: LinkedPageIcon({ width: '20', height: '20' }), - label: 'Turn into linked doc', - type: 'turn-into-linked-doc', - action: async ctx => { - const { doc, service, surface, std } = ctx; - const element = ctx.getNoteBlock(); - if (!element) return; - - const title = await promptDocTitle(std); - if (title === null) return; - - const linkedDoc = createLinkedDocFromNote(doc, element, title); - const crud = std.get(EdgelessCRUDIdentifier); - // insert linked doc card - const cardId = crud.addBlock( - 'affine:embed-synced-doc', - { - xywh: element.xywh, - style: 'syncedDoc', - pageId: linkedDoc.id, - index: element.index, - }, - surface.model.id - ); - std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', { - control: 'context-menu', - page: 'whiteboard editor', - module: 'toolbar', - segment: 'toolbar', - type: 'embed-synced-doc', - }); - std.getOptional(TelemetryProvider)?.track('DocCreated', { - control: 'turn into linked doc', - page: 'whiteboard editor', - module: 'format toolbar', - type: 'embed-linked-doc', - }); - std.getOptional(TelemetryProvider)?.track('LinkedDocCreated', { - control: 'turn into linked doc', - page: 'whiteboard editor', - module: 'format toolbar', - type: 'embed-linked-doc', - other: 'new doc', - }); - moveConnectors(element.id, cardId, service); - // delete selected note - doc.transact(() => { - doc.deleteBlock(element); - }); - service.selection.set({ - elements: [cardId], - editing: false, - }); - }, - when: ctx => !!ctx.getNoteBlock(), - }, - { - icon: LinkedPageIcon({ width: '20', height: '20' }), - label: 'Create linked doc', - type: 'create-linked-doc', - action: async ({ doc, selection, surface, edgeless, host, std }) => { - const title = await promptDocTitle(std); - if (title === null) return; - - const elements = getSortedCloneElements(selection.selectedElements); - const linkedDoc = createLinkedDocFromEdgelessElements( - host, - elements, - title - ); - const crud = std.get(EdgelessCRUDIdentifier); - // delete selected elements - doc.transact(() => { - deleteElements(edgeless, elements); - }); - // insert linked doc card - const width = 364; - const height = 390; - const bound = getCommonBoundWithRotation(elements); - const cardId = crud.addBlock( - 'affine:embed-linked-doc', - { - xywh: `[${bound.center[0] - width / 2}, ${bound.center[1] - height / 2}, ${width}, ${height}]`, - style: 'vertical', - pageId: linkedDoc.id, - }, - surface.model.id - ); - selection.set({ - elements: [cardId], - editing: false, - }); - std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', { - control: 'context-menu', - page: 'whiteboard editor', - module: 'toolbar', - segment: 'toolbar', - type: 'embed-linked-doc', - }); - std.getOptional(TelemetryProvider)?.track('DocCreated', { - control: 'create linked doc', - page: 'whiteboard editor', - module: 'format toolbar', - type: 'embed-linked-doc', - }); - std.getOptional(TelemetryProvider)?.track('LinkedDocCreated', { - control: 'create linked doc', - page: 'whiteboard editor', - module: 'format toolbar', - type: 'embed-linked-doc', - other: 'new doc', - }); - - notifyDocCreated(std, doc); - }, - when: ctx => !(ctx.getLinkedDocBlock() || ctx.getNoteBlock()), - }, - ], -}; - -// Delete Group -export const deleteGroup: MenuItemGroup = { - type: 'delete', - items: [ - { - icon: DeleteIcon({ width: '20', height: '20' }), - label: 'Delete', - type: 'delete', - action: ({ doc, selection, selectedElements, edgeless }) => { - doc.captureSync(); - deleteElements(edgeless, selectedElements); - - selection.set({ - elements: [], - editing: false, - }); - }, - }, - ], -}; - -export const BUILT_IN_GROUPS = [ - sectionGroup, - reorderGroup, - openGroup, - clipboardGroup, - conversionsGroup, - deleteGroup, -]; diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/context.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/context.ts deleted file mode 100644 index 462618e8b8..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/more-menu/context.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { isFrameBlock } from '@blocksuite/affine-block-frame'; -import { - isNoteBlock, - type SurfaceBlockComponent, -} from '@blocksuite/affine-block-surface'; -import { MenuContext } from '@blocksuite/affine-components/toolbar'; -import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands'; -import { - GfxPrimitiveElementModel, - type GfxSelectionManager, -} from '@blocksuite/block-std/gfx'; -import type { BlockModel } from '@blocksuite/store'; - -import type { EdgelessRootBlockComponent } from '../../../edgeless/edgeless-root-block.js'; -import type { EdgelessRootService } from '../../../edgeless/edgeless-root-service.js'; -import { - isAttachmentBlock, - isBookmarkBlock, - isEmbeddedLinkBlock, - isEmbedLinkedDocBlock, - isEmbedSyncedDocBlock, - isImageBlock, -} from '../../../edgeless/utils/query.js'; - -export class ElementToolbarMoreMenuContext extends MenuContext { - readonly #empty: boolean; - - readonly #includedFrame: boolean; - - readonly #multiple: boolean; - - readonly #single: boolean; - - edgeless!: EdgelessRootBlockComponent; - - get doc() { - return this.edgeless.doc; - } - - get firstBlockComponent() { - return this.getBlockComponent(this.firstElement.id); - } - - override get firstElement() { - return this.selection.firstElement; - } - - get host() { - return this.edgeless.host; - } - - get selectedBlockModels() { - const [result, { selectedModels }] = this.std.command.exec( - getSelectedModelsCommand - ); - - if (!result) return []; - - return selectedModels ?? []; - } - - get selectedElements() { - return this.selection.selectedElements; - } - - get selection(): GfxSelectionManager { - return this.service.selection; - } - - get service(): EdgelessRootService { - return this.edgeless.service; - } - - get std() { - return this.edgeless.host.std; - } - - get surface(): SurfaceBlockComponent { - return this.edgeless.surface; - } - - get view() { - return this.host.view; - } - - constructor(edgeless: EdgelessRootBlockComponent) { - super(); - this.edgeless = edgeless; - - const selectedElements = this.selection.selectedElements; - const len = selectedElements.length; - - this.#empty = len === 0; - this.#single = len === 1; - this.#multiple = !this.#empty && !this.#single; - this.#includedFrame = !this.#empty && selectedElements.some(isFrameBlock); - } - - getBlockComponent(id: string) { - return this.view.getBlock(id); - } - - getLinkedDocBlock() { - const valid = - this.#single && - (isEmbedLinkedDocBlock(this.firstElement) || - isEmbedSyncedDocBlock(this.firstElement)); - - if (!valid) return null; - - return this.firstElement; - } - - getNoteBlock() { - const valid = this.#single && isNoteBlock(this.firstElement); - - if (!valid) return null; - - return this.firstElement; - } - - hasFrame() { - return this.#includedFrame; - } - - override isElement() { - return ( - this.#single && this.firstElement instanceof GfxPrimitiveElementModel - ); - } - - override isEmpty() { - return this.#empty; - } - - isMultiple() { - return this.#multiple; - } - - isSingle() { - return this.#single; - } - - refreshable(model: BlockModel) { - return ( - isImageBlock(model) || - isBookmarkBlock(model) || - isAttachmentBlock(model) || - isEmbeddedLinkBlock(model) - ); - } -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/release-from-group-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/release-from-group-button.ts deleted file mode 100644 index c90ac29277..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/release-from-group-button.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { GroupElementModel } from '@blocksuite/affine-model'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { ReleaseFromGroupIcon } from '@blocksuite/icons/lit'; -import { html, LitElement } from 'lit'; -import { property } from 'lit/decorators.js'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; - -export class EdgelessReleaseFromGroupButton extends WithDisposable(LitElement) { - private _releaseFromGroup() { - const service = this.edgeless.service; - const element = service.selection.firstElement; - - if (!(element.group instanceof GroupElementModel)) return; - - const group = element.group; - - // oxlint-disable-next-line unicorn/prefer-dom-node-remove - group.removeChild(element); - - element.index = service.layer.generateIndex(); - - const parent = group.group; - if (parent instanceof GroupElementModel) { - parent.addChild(element); - } - } - - protected override render() { - return html` - this._releaseFromGroup()} - > - ${ReleaseFromGroupIcon()} - - `; - } - - @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; -} - -export function renderReleaseFromGroupButton( - edgeless: EdgelessRootBlockComponent -) { - return html` - - `; -} diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/styles.css.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/styles.css.ts deleted file mode 100644 index 409eefeac2..0000000000 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/styles.css.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { cssVar } from '@toeverything/theme'; -import { cssVarV2 } from '@toeverything/theme/v2'; -import { style } from '@vanilla-extract/css'; - -export const viewInPageNotifyFooter = style({ - display: 'flex', - justifyContent: 'flex-end', - gap: '12px', -}); - -export const viewInPageNotifyFooterButton = style({ - padding: '0px 6px', - borderRadius: '4px', - color: cssVarV2('text/primary'), - - fontSize: cssVar('fontSm'), - lineHeight: '22px', - fontWeight: '500', - textAlign: 'center', - - ':hover': { - background: cssVarV2('layer/background/hoverOverlay'), - }, -}); diff --git a/blocksuite/affine/blocks/block-root/src/widgets/index.ts b/blocksuite/affine/blocks/block-root/src/widgets/index.ts index 606658f78c..c283cbbff7 100644 --- a/blocksuite/affine/blocks/block-root/src/widgets/index.ts +++ b/blocksuite/affine/blocks/block-root/src/widgets/index.ts @@ -1,9 +1,5 @@ export { EDGELESS_TOOLBAR_WIDGET } from '../edgeless/components/toolbar/edgeless-toolbar.js'; export { AffineEdgelessZoomToolbarWidget } from './edgeless-zoom-toolbar/index.js'; -export { - EDGELESS_ELEMENT_TOOLBAR_WIDGET, - EdgelessElementToolbarWidget, -} from './element-toolbar/index.js'; export { AffineImageToolbarWidget } from './image-toolbar/index.js'; export { AffineInnerModalWidget } from './inner-modal/inner-modal.js'; export * from './keyboard-toolbar/index.js'; diff --git a/blocksuite/affine/components/src/edgeless-shape-color-picker/color-picker.ts b/blocksuite/affine/components/src/edgeless-shape-color-picker/color-picker.ts index 9cfaacf901..e627d5e7c7 100644 --- a/blocksuite/affine/components/src/edgeless-shape-color-picker/color-picker.ts +++ b/blocksuite/affine/components/src/edgeless-shape-color-picker/color-picker.ts @@ -229,6 +229,7 @@ export class EdgelessShapeColorPicker extends WithDisposable( ({ label, type, value, onPick, hollowCircle }) => html`
${label}
) => { + const opened = e.detail; + if (opened) return; + this.input.value = ''; + } + ); + } + override render() { const { sizes, @@ -134,6 +151,7 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) { return html` key ?? value, ({ key, value }) => html` this.select(value)} > diff --git a/blocksuite/affine/shared/src/services/toolbar-service/action.ts b/blocksuite/affine/shared/src/services/toolbar-service/action.ts index ba7be5107b..34b0f9fd5d 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/action.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/action.ts @@ -19,6 +19,7 @@ type ActionBase = { export type ToolbarAction = ActionBase & { label?: string; + showLabel?: boolean; icon?: TemplateResult; tooltip?: string | TemplateResult; variant?: 'destructive'; diff --git a/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts b/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts index c6ccc2d161..6e780baf60 100644 --- a/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts +++ b/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts @@ -373,8 +373,16 @@ export class AffineToolbarWidget extends WidgetComponent { // Triggered only when not in editing state. disposables.add( context.gfx.selection.slots.updated.subscribe(selections => { - // TODO(@fundon): should remove it when edgeless element toolbar is removed - if (context.isEdgelessMode) return; + // Should remove selections when clicking on frame navigator + if (context.isPageMode) { + if ( + std.host.contains(std.range.value?.commonAncestorContainer ?? null) + ) { + std.range.clear(); + } + context.reset(); + return; + } const elementIds = selections .map(s => (s.editing || s.inoperable ? [] : s.elements)) diff --git a/blocksuite/affine/widgets/widget-toolbar/src/utils.ts b/blocksuite/affine/widgets/widget-toolbar/src/utils.ts index 4487b1c73a..07f85a0638 100644 --- a/blocksuite/affine/widgets/widget-toolbar/src/utils.ts +++ b/blocksuite/affine/widgets/widget-toolbar/src/utils.ts @@ -232,7 +232,7 @@ export function renderToolbar( `${flavour}:${key}`, html` @@ -319,7 +319,9 @@ function renderActionItem(action: ToolbarAction, context: ToolbarContext) { @click=${() => action.run?.(context)} > ${action.icon} - ${action.label ? html`${action.label}` : null} + ${action.showLabel && action.label + ? html`${action.label}` + : null} `; } diff --git a/packages/frontend/core/src/blocksuite/ai/actions/edgeless-handler.ts b/packages/frontend/core/src/blocksuite/ai/actions/edgeless-handler.ts index 8aad1ff02c..443d89aab5 100644 --- a/packages/frontend/core/src/blocksuite/ai/actions/edgeless-handler.ts +++ b/packages/frontend/core/src/blocksuite/ai/actions/edgeless-handler.ts @@ -45,7 +45,7 @@ import { actionToErrorResponse, actionToGenerating, actionToResponse, - getElementToolbar, + getToolbar, } from './edgeless-response'; async function getContentFromEmbedSyncedDocModel( @@ -400,7 +400,7 @@ export function actionToHandler( trackerOptions ); - const elementToolbar = getElementToolbar(host); + const toolbar = getToolbar(host); const isEmpty = selectedElements.length === 0; const isCreateImageAction = id === 'createImage'; const isMakeItRealAction = !isCreateImageAction && id === 'makeItReal'; @@ -411,8 +411,8 @@ export function actionToHandler( referenceElement = selectedBlocks.at(-1); } else if (edgelessCopilot.visible && edgelessCopilot.selectionElem) { referenceElement = edgelessCopilot.selectionElem; - } else if (elementToolbar.toolbarVisible) { - referenceElement = getElementToolbar(host); + } else if (toolbar?.dataset.open) { + referenceElement = toolbar; } else if (!isEmpty) { const lastSelected = selectedElements.at(-1)?.id; if (!lastSelected) return; diff --git a/packages/frontend/core/src/blocksuite/ai/actions/edgeless-response.ts b/packages/frontend/core/src/blocksuite/ai/actions/edgeless-response.ts index 01decfec0b..6b7634cba4 100644 --- a/packages/frontend/core/src/blocksuite/ai/actions/edgeless-response.ts +++ b/packages/frontend/core/src/blocksuite/ai/actions/edgeless-response.ts @@ -5,10 +5,6 @@ import { EDGELESS_TEXT_BLOCK_MIN_WIDTH, } from '@blocksuite/affine/blocks/edgeless-text'; import { addImages } from '@blocksuite/affine/blocks/image'; -import { - EDGELESS_ELEMENT_TOOLBAR_WIDGET, - type EdgelessElementToolbarWidget, -} from '@blocksuite/affine/blocks/root'; import { fitContent, getSurfaceBlock, @@ -26,6 +22,10 @@ import { NoteDisplayMode, } from '@blocksuite/affine/model'; import { TelemetryProvider } from '@blocksuite/affine/shared/services'; +import { + AFFINE_TOOLBAR_WIDGET, + type AffineToolbarWidget, +} from '@blocksuite/affine/widgets/toolbar'; import { ChatWithAiIcon, DeleteIcon, @@ -65,16 +65,14 @@ type ErrorConfig = Exclude< null >['errorStateConfig']; -export function getElementToolbar( - host: EditorHost -): EdgelessElementToolbarWidget { +export function getToolbar(host: EditorHost) { const rootBlockId = host.doc.root?.id as string; - const elementToolbar = host.view.getWidget( - EDGELESS_ELEMENT_TOOLBAR_WIDGET, + const toolbar = host.view.getWidget( + AFFINE_TOOLBAR_WIDGET, rootBlockId - ) as EdgelessElementToolbarWidget; + ) as AffineToolbarWidget; - return elementToolbar; + return toolbar.querySelector('editor-toolbar'); } export function getTriggerEntry(host: EditorHost) { diff --git a/packages/frontend/core/src/blocksuite/ai/entries/edgeless/index.ts b/packages/frontend/core/src/blocksuite/ai/entries/edgeless/index.ts index 79a3ca0e39..cf09d5e483 100644 --- a/packages/frontend/core/src/blocksuite/ai/entries/edgeless/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/entries/edgeless/index.ts @@ -1,7 +1,3 @@ -import type { - EdgelessElementToolbarWidget, - EdgelessRootBlockComponent, -} from '@blocksuite/affine/blocks/root'; import { noop } from '@blocksuite/affine/global/utils'; import type { DocMode } from '@blocksuite/affine/model'; import { @@ -25,64 +21,6 @@ export function setupEdgelessCopilot(widget: EdgelessCopilotWidget) { widget.groups = edgelessAIGroups; } -export function setupEdgelessElementToolbarAIEntry( - widget: EdgelessElementToolbarWidget -) { - widget.registerEntry({ - when: () => { - return true; - }, - render: (edgeless: EdgelessRootBlockComponent) => { - const chain = edgeless.service.std.command.chain(); - const filteredGroups = edgelessAIGroups.reduce((pre, group) => { - const filtered = group.items.filter(item => - item.showWhen?.(chain, 'edgeless' as DocMode, edgeless.host) - ); - - if (filtered.length > 0) pre.push({ ...group, items: filtered }); - - return pre; - }, [] as AIItemGroupConfig[]); - - if (filteredGroups.every(group => group.items.length === 0)) return null; - - const handler = () => { - const aiPanel = getAIPanelWidget(edgeless.host); - if (aiPanel.config) { - aiPanel.config.generateAnswer = ({ finish, input }) => { - finish('success'); - aiPanel.hide(); - extractSelectedContent(edgeless.host) - .then(context => { - AIProvider.slots.requestSendWithChat.next({ - input, - context, - host: edgeless.host, - }); - }) - .catch(console.error); - }; - aiPanel.config.inputCallback = text => { - const copilotWidget = getEdgelessCopilotWidget(edgeless.host); - const panel = copilotWidget.shadowRoot?.querySelector( - 'edgeless-copilot-panel' - ); - if (panel instanceof HTMLElement) { - panel.style.visibility = text ? 'hidden' : 'visible'; - } - }; - } - }; - - return html``; - }, - }); -} - export function edgelessToolbarAIEntryConfig(): ToolbarModuleConfig { return { actions: [ diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts index ca68625447..08960d1751 100644 --- a/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts +++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts @@ -2,10 +2,7 @@ import { BlockFlavourIdentifier, LifeCycleWatcher, } from '@blocksuite/affine/block-std'; -import { - EdgelessElementToolbarWidget, - EdgelessRootBlockSpec, -} from '@blocksuite/affine/blocks/root'; +import { EdgelessRootBlockSpec } from '@blocksuite/affine/blocks/root'; import { ToolbarModuleExtension } from '@blocksuite/affine/shared/services'; import type { ExtensionType } from '@blocksuite/affine/store'; import type { FrameworkProvider } from '@toeverything/infra'; @@ -15,7 +12,6 @@ import { toolbarAIEntryConfig } from '../entries'; import { edgelessToolbarAIEntryConfig, setupEdgelessCopilot, - setupEdgelessElementToolbarAIEntry, } from '../entries/edgeless/index'; import { setupSpaceAIEntry } from '../entries/space/setup-space'; import { CopilotTool } from '../tool/copilot-tool'; @@ -72,10 +68,6 @@ function getAIEdgelessRootWatcher(framework: FrameworkProvider) { if (component instanceof EdgelessCopilotWidget) { setupEdgelessCopilot(component); } - - if (component instanceof EdgelessElementToolbarWidget) { - setupEdgelessElementToolbarAIEntry(component); - } }); } } 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 98d4c10b61..f19585b2db 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 @@ -54,7 +54,6 @@ import { getSelectedModelsCommand } from '@blocksuite/affine/shared/commands'; import { ImageSelection } from '@blocksuite/affine/shared/selection'; import { ActionPlacement, - FeatureFlagService, GenerateDocUrlProvider, isRemovedUserInfo, OpenDocExtensionIdentifier, @@ -316,8 +315,7 @@ function createToolbarMoreMenuConfigV2(baseUrl?: string) { { id: 'block-meta-display', when: ctx => { - const featureFlag = ctx.std.get(FeatureFlagService); - const isEnabled = featureFlag.getFlag('enable_block_meta'); + const isEnabled = ctx.features.getFlag('enable_block_meta'); if (!isEnabled) return false; // only display when one block is selected by block selection diff --git a/tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts b/tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts index 43732fa140..1c262447e3 100644 --- a/tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts +++ b/tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts @@ -171,11 +171,10 @@ test('paste surface-ref block to another doc as embed-linked-doc block', async ( const frameTitle = page.locator('affine-frame-title'); await frameTitle.click(); await page.waitForTimeout(50); - const changeFrameButton = page.locator('edgeless-change-frame-button'); - // get insert into page button which with aria-label 'Insert into Page' - const insertIntoPageButton = changeFrameButton.locator( - `editor-icon-button[aria-label="Insert into Page"]` - ); + + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + + const insertIntoPageButton = toolbar.getByLabel('Insert into Page'); await insertIntoPageButton.click(); await clickPageModeButton(page); diff --git a/tests/affine-local/e2e/blocksuite/edgeless/embed.spec.ts b/tests/affine-local/e2e/blocksuite/edgeless/embed.spec.ts index 4f93f8a1ab..a144140ddb 100644 --- a/tests/affine-local/e2e/blocksuite/edgeless/embed.spec.ts +++ b/tests/affine-local/e2e/blocksuite/edgeless/embed.spec.ts @@ -2,7 +2,7 @@ import { test } from '@affine-test/kit/playwright'; import { clickEdgelessModeButton, locateEditorContainer, - locateElementToolbar, + locateToolbar, } from '@affine-test/kit/utils/editor'; import { pressEnter } from '@affine-test/kit/utils/keyboard'; import { openHomePage } from '@affine-test/kit/utils/load-page'; @@ -31,7 +31,7 @@ test('should close embed editing modal when editor switching to page mode by sho .getByTestId('cmdk-label') .getByText('Write, Draw, Plan all at Once.') .click(); - const toolbar = locateElementToolbar(page); + const toolbar = locateToolbar(page); await toolbar.getByLabel('Edit').click(); const editingModal = page.locator('embed-card-edit-modal'); diff --git a/tests/affine-local/e2e/blocksuite/edgeless/frame.spec.ts b/tests/affine-local/e2e/blocksuite/edgeless/frame.spec.ts index 52483b869c..464ec75bb1 100644 --- a/tests/affine-local/e2e/blocksuite/edgeless/frame.spec.ts +++ b/tests/affine-local/e2e/blocksuite/edgeless/frame.spec.ts @@ -5,7 +5,7 @@ import { createEdgelessNoteBlock, dragView, locateEditorContainer, - locateElementToolbar, + locateToolbar, toViewCoord, } from '@affine-test/kit/utils/editor'; import { @@ -34,7 +34,7 @@ test.beforeEach(async ({ page }) => { test('should update zindex of element when moving it into frame', async ({ page, }) => { - const toolbar = locateElementToolbar(page); + const toolbar = locateToolbar(page); // create a top frame await page.keyboard.press('f'); @@ -47,7 +47,7 @@ test('should update zindex of element when moving it into frame', async ({ await createEdgelessNoteBlock(page, [500, 500]); await clickView(page, [0, 100]); await clickView(page, [500, 500]); - await toolbar.getByLabel('More').click(); + await toolbar.getByLabel('more-menu').click(); await toolbar.getByLabel('Send to Back').click(); await pressEscape(page); diff --git a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts index bfaca7c939..b93fd9a4b5 100644 --- a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts +++ b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts @@ -7,8 +7,8 @@ import { getPageMode, getSelectedXYWH, locateEditorContainer, - locateElementToolbar, locateModeSwitchButton, + locateToolbar, moveToView, resizeElementByHandle, toViewCoord, @@ -201,8 +201,8 @@ test.describe('edgeless note element toolbar', () => { page, }) => { await selectAllByKeyboard(page); - const toolbar = locateElementToolbar(page); - const autoHeight = toolbar.getByTestId('edgeless-note-auto-height'); + const toolbar = locateToolbar(page); + const autoHeight = toolbar.getByTestId('auto-height'); const displayInPage = toolbar.getByTestId('display-in-page'); await expect(toolbar).toBeVisible(); @@ -218,8 +218,8 @@ test.describe('edgeless note element toolbar', () => { await clickView(page, [0, 0]); await clickView(page, [100, 100]); - const toolbar = locateElementToolbar(page); - const autoHeight = toolbar.getByTestId('edgeless-note-auto-height'); + const toolbar = locateToolbar(page); + const autoHeight = toolbar.getByTestId('auto-height'); const displayInPage = toolbar.getByTestId('display-in-page'); await expect(toolbar).toBeVisible(); @@ -237,7 +237,7 @@ test.describe('edgeless note element toolbar', () => { await clickView(page, [0, 0]); await clickView(page, [100, 100]); - const toolbar = locateElementToolbar(page); + const toolbar = locateToolbar(page); const displayInPage = toolbar.getByTestId('display-in-page'); await displayInPage.click(); @@ -300,7 +300,7 @@ test.describe('edgeless note element toolbar', () => { }, noteId); }; - const toolbar = locateElementToolbar(page); + const toolbar = locateToolbar(page); await selectAllByKeyboard(page); const noteId = (await getEdgelessSelectedIds(page))[0]; @@ -329,7 +329,11 @@ test.describe('edgeless note element toolbar', () => { await toolbar.getByRole('button', { name: 'Border style' }).click(); await toolbar.locator('.mode-solid').click(); await toolbar.getByRole('button', { name: 'Border style' }).click(); - await toolbar.locator('edgeless-line-width-panel').getByLabel('8').click(); + // TODO(@fundon): delete duplicate components + await toolbar + .locator('affine-edgeless-line-width-panel') + .getByLabel('8') + .click(); expect(await getNoteEdgelessProps(page, noteId)).toEqual({ style: { @@ -341,7 +345,11 @@ test.describe('edgeless note element toolbar', () => { }); await toolbar.getByRole('button', { name: 'Corners' }).click(); - await toolbar.locator('edgeless-size-panel').getByText('Large').click(); + // TODO(@fundon): delete duplicate components + await toolbar + .locator('affine-size-dropdown-menu') + .getByText('Large') + .click(); expect(await getNoteEdgelessProps(page, noteId)).toEqual({ style: { diff --git a/tests/affine-local/e2e/blocksuite/edgeless/shape.spec.ts b/tests/affine-local/e2e/blocksuite/edgeless/shape.spec.ts index 6a037903ad..b338225491 100644 --- a/tests/affine-local/e2e/blocksuite/edgeless/shape.spec.ts +++ b/tests/affine-local/e2e/blocksuite/edgeless/shape.spec.ts @@ -35,9 +35,7 @@ test('should add text to shape, default to pure black', async ({ page }) => { await page.keyboard.type('text'); await page.keyboard.press('Escape'); - const toolbar = page.locator( - 'edgeless-element-toolbar-widget editor-toolbar' - ); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); const textColorContainer = toolbar.locator( 'edgeless-color-picker-button.text-color' ); @@ -76,9 +74,7 @@ test('should add text to shape with pure white', async ({ page }) => { await page.keyboard.type('text'); await page.keyboard.press('Escape'); - const toolbar = page.locator( - 'edgeless-element-toolbar-widget editor-toolbar' - ); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); const textColorContainer = toolbar.locator( 'edgeless-color-picker-button.text-color' ); diff --git a/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts b/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts index 1bd29876e9..5b44133186 100644 --- a/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts +++ b/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts @@ -7,7 +7,7 @@ import { focusDocTitle, getEdgelessSelectedIds, getViewportCenter, - locateElementToolbar, + locateToolbar, setViewportCenter, } from '@affine-test/kit/utils/editor'; import { @@ -508,12 +508,10 @@ test.describe('advanced visibility control', () => { await expect(edgelessCard).toHaveCount(1); await clickView(page, [100, 100]); - const noteButtons = locateElementToolbar(page).locator( - 'edgeless-change-note-button' - ); + const toolbar = locateToolbar(page); - await noteButtons.getByRole('button', { name: 'Mode' }).click(); - await noteButtons.locator('note-display-mode-panel .item.both').click(); + await toolbar.getByRole('button', { name: 'Mode' }).click(); + await toolbar.locator('note-display-mode-panel .item.both').click(); await expect(bothCard).toHaveCount(2); await expect(edgelessCard).toHaveCount(0); @@ -541,10 +539,8 @@ test.describe('advanced visibility control', () => { await expect(bothCard).toHaveCount(2); await clickView(page, [200, 100]); - const changeNoteButtons = locateElementToolbar(page).locator( - 'edgeless-change-note-button' - ); - await changeNoteButtons.getByRole('button', { name: 'Slicer' }).click(); + const toolbar = locateToolbar(page); + await toolbar.getByRole('button', { name: 'Slicer' }).click(); await expect(page.locator('.note-slicer-button')).toBeVisible(); await page.locator('.note-slicer-button').click(); diff --git a/tests/affine-local/e2e/links.spec.ts b/tests/affine-local/e2e/links.spec.ts index c0fac4122e..0613b11bf8 100644 --- a/tests/affine-local/e2e/links.spec.ts +++ b/tests/affine-local/e2e/links.spec.ts @@ -156,7 +156,7 @@ test('not allowed to switch to embed view when linking to block', async ({ await cardLink.click(); - await toolbar.getByLabel('More').click(); + await toolbar.getByLabel('more-menu').click(); await toolbar.getByLabel('Copy link to block').click(); await page.keyboard.press('Enter'); diff --git a/tests/affine-local/e2e/peek-view.spec.ts b/tests/affine-local/e2e/peek-view.spec.ts index 0949a96609..f2774dc2ab 100644 --- a/tests/affine-local/e2e/peek-view.spec.ts +++ b/tests/affine-local/e2e/peek-view.spec.ts @@ -118,10 +118,10 @@ test('can open peek view for embedded frames', async ({ page }) => { // close affine-banner await page.locator('[data-testid=local-demo-tips-close-button]').click(); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + // insert the frame to page - await page - .locator('edgeless-change-frame-button:has-text("Insert into Page")') - .click(); + await toolbar.getByLabel('Insert into Page').click(); // switch back to page mode await clickPageModeButton(page); diff --git a/tests/blocksuite/e2e/edgeless/auto-complete.spec.ts b/tests/blocksuite/e2e/edgeless/auto-complete.spec.ts index e6af09215c..60bbc529b1 100644 --- a/tests/blocksuite/e2e/edgeless/auto-complete.spec.ts +++ b/tests/blocksuite/e2e/edgeless/auto-complete.spec.ts @@ -134,10 +134,12 @@ test.describe('auto-complete', () => { await edgelessCommonSetup(page); await createShapeElement(page, [0, 0], [100, 100], Shape.Square); await assertSelectedBound(page, [0, 0, 100, 100]); - await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); + await triggerComponentToolbarAction(page, 'changeShapeColor'); await changeShapeStrokeColor(page, 'MediumRed'); - await triggerComponentToolbarAction(page, 'changeShapeFillColor'); await changeShapeFillColor(page, 'HeavyGreen'); + // Closes color pickers + await triggerComponentToolbarAction(page, 'changeShapeColor'); + await dragBetweenViewCoords(page, [120, 50], [200, 0]); const noteButton = getAutoCompletePanelButton(page, 'note'); diff --git a/tests/blocksuite/e2e/edgeless/color-picker.spec.ts b/tests/blocksuite/e2e/edgeless/color-picker.spec.ts index bb0831e9be..ba295ad148 100644 --- a/tests/blocksuite/e2e/edgeless/color-picker.spec.ts +++ b/tests/blocksuite/e2e/edgeless/color-picker.spec.ts @@ -20,8 +20,8 @@ async function setupWithColorPickerFunction(page: Page) { await switchEditorMode(page); } -function getColorPickerButtonWithClass(page: Page, classes: string) { - return page.locator(`edgeless-color-picker-button.${classes}`); +function getColorPanelWithLabel(page: Page, label: string) { + return page.locator(`edgeless-color-panel[aria-label="${label}"]`); } function getCurrentColorUnitButton(locator: Locator) { @@ -71,12 +71,12 @@ test.describe('basic functions', () => { const end0 = { x: 150, y: 200 }; await addBasicShapeElement(page, start0, end0, Shape.Square); - const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); - await expect(fillColorButton).toBeVisible(); - await triggerComponentToolbarAction(page, 'changeShapeFillColor'); - const customButton = getCustomButton(fillColorButton); + const fillColorPanel = getColorPanelWithLabel(page, 'Fill color'); + await expect(fillColorPanel).toBeVisible(); + + const customButton = getCustomButton(fillColorPanel); await expect(customButton).toBeVisible(); }); @@ -91,12 +91,13 @@ test.describe('basic functions', () => { await triggerComponentToolbarAction(page, 'changeShapeFillColor'); - const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); - const customButton = getCustomButton(fillColorButton); + const fillColorPanel = getColorPanelWithLabel(page, 'Fill color'); + const customButton = getCustomButton(fillColorPanel); await customButton.click(); - const colorPickerPanel = getColorPickerPanel(fillColorButton); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + const colorPickerPanel = getColorPickerPanel(toolbar); await expect(colorPickerPanel).toBeVisible(); }); @@ -112,17 +113,18 @@ test.describe('basic functions', () => { await triggerComponentToolbarAction(page, 'changeShapeFillColor'); - const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); - const currentColorUnit = getCurrentColorUnitButton(fillColorButton); + const fillColorPanel = getColorPanelWithLabel(page, 'Fill color'); + const currentColorUnit = getCurrentColorUnitButton(fillColorPanel); const value = await getCurrentColor(currentColorUnit); await expect(currentColorUnit.locator('svg')).toHaveCSS('fill', value); - const customButton = getCustomButton(fillColorButton); + const customButton = getCustomButton(fillColorPanel); await customButton.click(); - const colorPickerPanel = getColorPickerPanel(fillColorButton); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + const colorPickerPanel = getColorPickerPanel(toolbar); await expect(colorPickerPanel).toBeVisible(); @@ -143,14 +145,16 @@ test.describe('basic functions', () => { const end0 = { x: 150, y: 200 }; await addBasicShapeElement(page, start0, end0, Shape.Square); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); - const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); - const customButton = getCustomButton(fillColorButton); - const colorPickerPanel = getColorPickerPanel(fillColorButton); + const fillColorPanel = getColorPanelWithLabel(page, 'Fill color'); + const customButton = getCustomButton(fillColorPanel); await customButton.click(); + const colorPickerPanel = getColorPickerPanel(toolbar); await expect(colorPickerPanel).toBeVisible(); await page.mouse.click(0, 0); @@ -159,7 +163,7 @@ test.describe('basic functions', () => { await dragBetweenCoords(page, { x: 125, y: 75 }, { x: 175, y: 225 }); - await fillColorButton.click(); + await toolbar.getByLabel(/^Color$/).click(); await expect(customButton).toBeVisible(); await expect(colorPickerPanel).toBeHidden(); @@ -174,14 +178,16 @@ test.describe('basic functions', () => { const end0 = { x: 150, y: 200 }; await addBasicShapeElement(page, start0, end0, Shape.Square); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); - const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); - const customButton = getCustomButton(fillColorButton); - const colorPickerPanel = getColorPickerPanel(fillColorButton); + const fillColorPanel = getColorPanelWithLabel(page, 'Fill color'); + const customButton = getCustomButton(fillColorPanel); await customButton.click(); + const colorPickerPanel = getColorPickerPanel(toolbar); const paletteControl = getPaletteControl(colorPickerPanel); const hexInput = getHexInput(colorPickerPanel); @@ -203,14 +209,16 @@ test.describe('basic functions', () => { const end0 = { x: 150, y: 200 }; await addBasicShapeElement(page, start0, end0, Shape.Square); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); - const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); - const customButton = getCustomButton(fillColorButton); - const colorPickerPanel = getColorPickerPanel(fillColorButton); + const fillColorPanel = getColorPanelWithLabel(page, 'Fill color'); + const customButton = getCustomButton(fillColorPanel); await customButton.click(); + const colorPickerPanel = getColorPickerPanel(toolbar); const hueControl = getHueControl(colorPickerPanel); const hexInput = getHexInput(colorPickerPanel); @@ -230,14 +238,16 @@ test.describe('basic functions', () => { const end0 = { x: 150, y: 200 }; await addBasicShapeElement(page, start0, end0, Shape.Square); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); - const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); - const customButton = getCustomButton(fillColorButton); - const colorPickerPanel = getColorPickerPanel(fillColorButton); + const fillColorPanel = getColorPanelWithLabel(page, 'Fill color'); + const customButton = getCustomButton(fillColorPanel); await customButton.click(); + const colorPickerPanel = getColorPickerPanel(toolbar); const hexInput = getHexInput(colorPickerPanel); await hexInput.fill('fff'); @@ -266,14 +276,16 @@ test.describe('basic functions', () => { const end0 = { x: 150, y: 200 }; await addBasicShapeElement(page, start0, end0, Shape.Square); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); - const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); - const customButton = getCustomButton(fillColorButton); - const colorPickerPanel = getColorPickerPanel(fillColorButton); + const fillColorPanel = getColorPanelWithLabel(page, 'Fill color'); + const customButton = getCustomButton(fillColorPanel); await customButton.click(); + const colorPickerPanel = getColorPickerPanel(toolbar); const alphaControl = getAlphaControl(colorPickerPanel); const alphaInput = getAlphaInput(colorPickerPanel); @@ -295,14 +307,16 @@ test.describe('basic functions', () => { const end0 = { x: 150, y: 200 }; await addBasicShapeElement(page, start0, end0, Shape.Square); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); - const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); - const customButton = getCustomButton(fillColorButton); - const colorPickerPanel = getColorPickerPanel(fillColorButton); + const fillColorPanel = getColorPanelWithLabel(page, 'Fill color'); + const customButton = getCustomButton(fillColorPanel); await customButton.click(); + const colorPickerPanel = getColorPickerPanel(toolbar); const alphaInput = getAlphaInput(colorPickerPanel); await alphaInput.fill('101'); @@ -336,8 +350,9 @@ test.describe('basic functions', () => { await triggerComponentToolbarAction(page, 'changeShapeFillColor'); - const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); - const currentColorUnit = getCurrentColorUnitButton(fillColorButton); + const fillColorPanel = getColorPanelWithLabel(page, 'Fill color'); + + const currentColorUnit = getCurrentColorUnitButton(fillColorPanel); const value = await getCurrentColor(currentColorUnit); let rgba = parseStringToRgba(value); diff --git a/tests/blocksuite/e2e/edgeless/connector/connector.spec.ts b/tests/blocksuite/e2e/edgeless/connector/connector.spec.ts index 6daffc482d..9205d0ee41 100644 --- a/tests/blocksuite/e2e/edgeless/connector/connector.spec.ts +++ b/tests/blocksuite/e2e/edgeless/connector/connector.spec.ts @@ -146,7 +146,7 @@ test('change connector line width', async ({ page }) => { await addBasicConnectorElement(page, start, end); await page.mouse.click(start.x + 5, start.y); - await triggerComponentToolbarAction(page, 'changeConnectorStrokeColor'); + await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles'); await changeConnectorStrokeColor(page, 'MediumGrey'); await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles'); @@ -171,7 +171,7 @@ test('change connector stroke style', async ({ page }) => { await addBasicConnectorElement(page, start, end); await page.mouse.click(start.x + 5, start.y); - await triggerComponentToolbarAction(page, 'changeConnectorStrokeColor'); + await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles'); await changeConnectorStrokeColor(page, 'MediumGrey'); await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles'); diff --git a/tests/blocksuite/e2e/edgeless/element-toolbar.spec.ts b/tests/blocksuite/e2e/edgeless/element-toolbar.spec.ts index 777e7cd6a6..590b912ede 100644 --- a/tests/blocksuite/e2e/edgeless/element-toolbar.spec.ts +++ b/tests/blocksuite/e2e/edgeless/element-toolbar.spec.ts @@ -75,14 +75,15 @@ test('should be hidden when resizing element', async ({ page }) => { const toolbar = locatorComponentToolbar(page); await expect(toolbar).toBeVisible(); - await resizeElementByHandle(page, { x: 400, y: 300 }, 'top-left', 30); + await resizeElementByHandle( + page, + { x: 400, y: 300 }, + 'top-left', + 30, + async () => { + await expect(toolbar).toBeHidden(); + } + ); - await page.mouse.move(450, 300); - await expect(toolbar).toBeEmpty(); - - await page.mouse.move(320, 220); - await expect(toolbar).toBeEmpty(); - - await page.mouse.up(); await expect(toolbar).toBeVisible(); }); diff --git a/tests/blocksuite/e2e/edgeless/frame/clipboard.spec.ts b/tests/blocksuite/e2e/edgeless/frame/clipboard.spec.ts index 3a3e29046e..67a088e420 100644 --- a/tests/blocksuite/e2e/edgeless/frame/clipboard.spec.ts +++ b/tests/blocksuite/e2e/edgeless/frame/clipboard.spec.ts @@ -107,8 +107,13 @@ test.describe('frame copy and paste', () => { const frameTitles = page.locator('affine-frame-title'); await frameTitles.nth(0).click(); - await page.locator('edgeless-more-button').click(); - await page.locator('editor-menu-action', { hasText: 'Duplicate' }).click(); + + const moreMenu = page.getByLabel('more-menu'); + + await moreMenu.click(); + await moreMenu + .locator('editor-menu-action', { hasText: 'Duplicate' }) + .click(); await pressEscape(page); await frameTitles.nth(0).click(); diff --git a/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts b/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts index 84e6951e80..eda0081819 100644 --- a/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts +++ b/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts @@ -388,8 +388,8 @@ test('delete frame by click ungroup should not delete its children', async ({ const frameTitle = page.locator('affine-frame-title'); await frameTitle.click(); - const elementToolbar = page.locator('edgeless-element-toolbar-widget'); - const ungroupButton = elementToolbar.getByLabel('Ungroup'); + const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); + const ungroupButton = toolbar.getByLabel('Ungroup'); await ungroupButton.click(); await assertCanvasElementsCount(page, 1); diff --git a/tests/blocksuite/e2e/edgeless/group/group.spec.ts b/tests/blocksuite/e2e/edgeless/group/group.spec.ts index 94e4477191..d3158edfbb 100644 --- a/tests/blocksuite/e2e/edgeless/group/group.spec.ts +++ b/tests/blocksuite/e2e/edgeless/group/group.spec.ts @@ -6,6 +6,7 @@ import { dragBetweenViewCoords, edgelessCommonSetup, getFirstContainerId, + locatorComponentToolbar, Shape, shiftClickView, triggerComponentToolbarAction, @@ -45,15 +46,16 @@ test.describe('group', () => { page, }) => { await clickView(page, [50, 50]); - await expect( - page.locator('edgeless-element-toolbar-widget') - ).toBeVisible(); - await expect(page.locator('edgeless-add-group-button')).not.toBeVisible(); + const toolbar = locatorComponentToolbar(page); + await expect(toolbar).toBeVisible(); + await expect(toolbar.getByLabel(/^Group$/)).not.toBeVisible(); }); test('create button show up when multi select', async ({ page }) => { await selectAllByKeyboard(page); - await expect(page.locator('edgeless-add-group-button')).toBeVisible(); + const toolbar = locatorComponentToolbar(page); + await expect(toolbar).toBeVisible(); + await expect(toolbar.getByLabel(/^Group$/)).toBeVisible(); }); test('create group by component toolbar', async ({ page }) => { diff --git a/tests/blocksuite/e2e/edgeless/lock.spec.ts b/tests/blocksuite/e2e/edgeless/lock.spec.ts index fccb6040d2..13e130a41f 100644 --- a/tests/blocksuite/e2e/edgeless/lock.spec.ts +++ b/tests/blocksuite/e2e/edgeless/lock.spec.ts @@ -42,12 +42,10 @@ import { test } from '../utils/playwright.js'; test.describe('lock', () => { const getButtons = (page: Page) => { - const elementToolbar = page.locator('edgeless-element-toolbar-widget'); + const toolbar = page.locator('affine-toolbar-widget'); return { - lock: elementToolbar.locator('edgeless-lock-button[data-locked="false"]'), - unlock: elementToolbar.locator( - 'edgeless-lock-button[data-locked="true"]' - ), + lock: toolbar.getByTestId('lock'), + unlock: toolbar.getByTestId('unlock'), }; }; diff --git a/tests/blocksuite/e2e/edgeless/note/note.spec.ts b/tests/blocksuite/e2e/edgeless/note/note.spec.ts index 5b308c9a7c..a53da1fa9f 100644 --- a/tests/blocksuite/e2e/edgeless/note/note.spec.ts +++ b/tests/blocksuite/e2e/edgeless/note/note.spec.ts @@ -262,7 +262,7 @@ test('duplicate note should work correctly', async ({ page }) => { await triggerComponentToolbarAction(page, 'duplicate'); await waitNextFrame(page, 200); // wait viewport fit animation - const moreActionsContainer = page.locator('.more-actions-container'); + const moreActionsContainer = page.getByLabel('more-menu').getByRole('menu'); await expect(moreActionsContainer).toBeHidden(); const noteLocator = page.locator('affine-edgeless-note'); diff --git a/tests/blocksuite/e2e/edgeless/note/scale.spec.ts b/tests/blocksuite/e2e/edgeless/note/scale.spec.ts index 6f192560a4..29439c7936 100644 --- a/tests/blocksuite/e2e/edgeless/note/scale.spec.ts +++ b/tests/blocksuite/e2e/edgeless/note/scale.spec.ts @@ -35,7 +35,7 @@ async function openScalePanel(page: Page, noteId: string) { await selectNoteInEdgeless(page, noteId); await triggerComponentToolbarAction(page, 'changeNoteScale'); await waitNextFrame(page); - const scalePanel = page.locator('edgeless-scale-panel'); + const scalePanel = page.locator('.scale-menu'); await expect(scalePanel).toBeVisible(); return scalePanel; } @@ -90,7 +90,7 @@ test.describe('note scale', () => { const noteId = await setupAndAddNote(page); const scalePanel = await openScalePanel(page, noteId); - const scaleInput = scalePanel.locator('.scale-input'); + const scaleInput = scalePanel.locator('input'); await scaleInput.click(); await page.keyboard.type('50'); await page.keyboard.press('Enter'); @@ -102,7 +102,7 @@ test.describe('note scale', () => { const noteId = await setupAndAddNote(page); const scalePanel = await openScalePanel(page, noteId); - const scaleInput = scalePanel.locator('.scale-input'); + const scaleInput = scalePanel.locator('input'); await scaleInput.click(); await page.keyboard.type('50'); await selectAllByKeyboard(page); diff --git a/tests/blocksuite/e2e/edgeless/shape.spec.ts b/tests/blocksuite/e2e/edgeless/shape.spec.ts index 54aa0a207b..aa32899b8f 100644 --- a/tests/blocksuite/e2e/edgeless/shape.spec.ts +++ b/tests/blocksuite/e2e/edgeless/shape.spec.ts @@ -8,7 +8,6 @@ import { changeShapeStrokeColor, changeShapeStrokeStyle, changeShapeStrokeWidth, - changeShapeStyle, clickComponentToolbarMoreMenuButton, getEdgelessSelectedRect, locatorComponentToolbar, @@ -347,17 +346,16 @@ test('change shape stroke width', async ({ page }) => { await addBasicRectShapeElement(page, start, end); await page.mouse.click(start.x + 5, start.y + 5); - await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); + await triggerComponentToolbarAction(page, 'changeShapeColor'); await changeShapeStrokeColor(page, 'MediumMagenta'); - await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles'); await changeShapeStrokeWidth(page); await page.mouse.click(start.x + 5, start.y + 5); await assertEdgelessSelectedRect(page, [100, 150, 100, 100]); await waitNextFrame(page); - await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles'); + await triggerComponentToolbarAction(page, 'changeShapeColor'); }); test('change shape stroke style', async ({ page }) => { @@ -370,14 +368,12 @@ test('change shape stroke style', async ({ page }) => { await addBasicRectShapeElement(page, start, end); await page.mouse.click(start.x + 5, start.y + 5); - await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); + await triggerComponentToolbarAction(page, 'changeShapeColor'); await changeShapeStrokeColor(page, 'MediumBlue'); - await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles'); await changeShapeStrokeStyle(page, 'dash'); await waitNextFrame(page); - await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles'); const activeButton = locatorShapeStrokeStyleButton(page, 'dash'); const className = await activeButton.evaluate(ele => ele.className); expect(className.includes(' active')).toBeTruthy(); @@ -552,12 +548,12 @@ test('change shape style', async ({ page }) => { await addBasicRectShapeElement(page, start, end); await page.mouse.click(start.x + 5, start.y + 5); - await triggerComponentToolbarAction(page, 'changeShapeStyle'); - await changeShapeStyle(page, 'general'); + await triggerComponentToolbarAction(page, 'changeShapeColor'); + // The style switching feature has been removed. + //await changeShapeStyle(page, 'general'); await waitNextFrame(page); await page.mouse.click(start.x + 5, start.y + 5); - await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); const color = 'LightPurple'; await changeShapeStrokeColor(page, color); await page.waitForTimeout(50); @@ -638,8 +634,11 @@ test.describe('shape hit test', () => { await addBasicRectShapeElement(page, rect.start, rect.end); await page.mouse.click(rect.start.x + 5, rect.start.y + 5); - await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + // opens color picker + await triggerComponentToolbarAction(page, 'changeShapeColor'); await changeShapeFillColorToTransparent(page); + // closes color picker + await triggerComponentToolbarAction(page, 'changeShapeColor'); await page.waitForTimeout(50); } @@ -715,15 +714,15 @@ test.describe('shape hit test', () => { await pressEscape(page); await waitNextFrame(page); - const textAlignBtn = locatorComponentToolbar(page).getByRole('button', { + const alignmentMenu = + locatorComponentToolbar(page).getByLabel('alignment-menu'); + + const textAlignBtn = alignmentMenu.getByRole('button', { name: 'Alignment', }); await textAlignBtn.click(); - await page - .locator('edgeless-align-panel') - .getByRole('button', { name: 'Left' }) - .click(); + await alignmentMenu.getByRole('button', { name: 'Left' }).click(); // creates an edgeless-text await page.mouse.dblclick(rect.start.x + 80, rect.start.y + 20); diff --git a/tests/blocksuite/e2e/utils/actions/edgeless.ts b/tests/blocksuite/e2e/utils/actions/edgeless.ts index 0f688a6b62..fef818ab14 100644 --- a/tests/blocksuite/e2e/utils/actions/edgeless.ts +++ b/tests/blocksuite/e2e/utils/actions/edgeless.ts @@ -284,7 +284,7 @@ export function locatorEdgelessComponentToolButton( more: 'More', }[type]; const button = page - .locator('edgeless-element-toolbar-widget editor-icon-button') + .locator('affine-toolbar-widget editor-toolbar editor-icon-button') .filter({ hasText: text, }); @@ -592,7 +592,8 @@ export async function resizeElementByHandle( | 'top-right' | 'bottom-right' | 'bottom-left' = 'top-left', - steps = 1 + steps = 1, + beforeMouseUp?: () => Promise ) { const handle = page.locator(`.handle[aria-label="${corner}"] .resize`); const box = await handle.boundingBox(); @@ -604,6 +605,7 @@ export async function resizeElementByHandle( { x: box.x + delta.x + offset, y: box.y + delta.y + offset }, { steps, + beforeMouseUp, } ); } @@ -757,14 +759,11 @@ export function locatorNoteDisplayModeButton( page: Page, mode: NoteDisplayMode ) { - return page - .locator('edgeless-change-note-button') - .locator('note-display-mode-panel') - .locator(`.item.${mode}`); + return page.locator('note-display-mode-panel').locator(`.item.${mode}`); } export function locatorScalePanelButton(page: Page, scale: number) { - return page.locator('edgeless-scale-panel').locator(`.scale-${scale}`); + return page.locator('affine-size-dropdown-menu').getByLabel(String(scale)); } export async function changeNoteDisplayMode(page: Page, mode: NoteDisplayMode) { @@ -796,9 +795,9 @@ export async function updateExistedBrushElementSize( } export async function openComponentToolbarMoreMenu(page: Page) { - const btn = page.locator( - 'edgeless-element-toolbar-widget edgeless-more-button editor-menu-button' - ); + const btn = page + .locator('affine-toolbar-widget editor-toolbar') + .getByLabel('more-menu'); await btn.click(); } @@ -1020,13 +1019,11 @@ export async function deleteAllConnectors(page: Page) { } export function locatorComponentToolbar(page: Page) { - return page.locator('edgeless-element-toolbar-widget'); + return page.locator('affine-toolbar-widget editor-toolbar'); } export function locatorComponentToolbarMoreButton(page: Page) { - const moreButton = locatorComponentToolbar(page).locator( - 'edgeless-more-button' - ); + const moreButton = locatorComponentToolbar(page).getByLabel('more-menu'); return moreButton; } type Action = @@ -1037,10 +1034,10 @@ type Action = | 'copyAsPng' | 'changeNoteColor' | 'changeShapeStyle' + | 'changeShapeColor' | 'changeShapeFillColor' | 'changeShapeStrokeColor' | 'changeShapeStrokeStyles' - | 'changeConnectorStrokeColor' | 'changeConnectorStrokeStyles' | 'changeConnectorShape' | 'addFrame' @@ -1075,11 +1072,9 @@ export async function triggerComponentToolbarAction( const moreButton = locatorComponentToolbarMoreButton(page); await moreButton.click(); - const actionButton = moreButton - .locator('.more-actions-container editor-menu-action') - .filter({ - hasText: 'Bring to Front', - }); + const actionButton = moreButton.locator('editor-menu-action').filter({ + hasText: 'Bring to Front', + }); await actionButton.click(); break; } @@ -1087,11 +1082,9 @@ export async function triggerComponentToolbarAction( const moreButton = locatorComponentToolbarMoreButton(page); await moreButton.click(); - const actionButton = moreButton - .locator('.more-actions-container editor-menu-action') - .filter({ - hasText: 'Bring Forward', - }); + const actionButton = moreButton.locator('editor-menu-action').filter({ + hasText: 'Bring Forward', + }); await actionButton.click(); break; } @@ -1099,11 +1092,9 @@ export async function triggerComponentToolbarAction( const moreButton = locatorComponentToolbarMoreButton(page); await moreButton.click(); - const actionButton = moreButton - .locator('.more-actions-container editor-menu-action') - .filter({ - hasText: 'Send Backward', - }); + const actionButton = moreButton.locator('editor-menu-action').filter({ + hasText: 'Send Backward', + }); await actionButton.click(); break; } @@ -1111,11 +1102,9 @@ export async function triggerComponentToolbarAction( const moreButton = locatorComponentToolbarMoreButton(page); await moreButton.click(); - const actionButton = moreButton - .locator('.more-actions-container editor-menu-action') - .filter({ - hasText: 'Send to Back', - }); + const actionButton = moreButton.locator('editor-menu-action').filter({ + hasText: 'Send to Back', + }); await actionButton.click(); break; } @@ -1123,11 +1112,9 @@ export async function triggerComponentToolbarAction( const moreButton = locatorComponentToolbarMoreButton(page); await moreButton.click(); - const actionButton = moreButton - .locator('.more-actions-container editor-menu-action') - .filter({ - hasText: 'Copy as PNG', - }); + const actionButton = moreButton.locator('editor-menu-action').filter({ + hasText: 'Copy as PNG', + }); await actionButton.click(); break; } @@ -1135,11 +1122,9 @@ export async function triggerComponentToolbarAction( const moreButton = locatorComponentToolbarMoreButton(page); await moreButton.click(); - const actionButton = moreButton - .locator('.more-actions-container editor-menu-action') - .filter({ - hasText: 'Frame Section', - }); + const actionButton = moreButton.locator('editor-menu-action').filter({ + hasText: 'Frame Section', + }); await actionButton.click(); break; } @@ -1147,68 +1132,48 @@ export async function triggerComponentToolbarAction( const moreButton = locatorComponentToolbarMoreButton(page); await moreButton.click(); - const actionButton = moreButton - .locator('.more-actions-container editor-menu-action') - .filter({ - hasText: 'Duplicate', - }); + const actionButton = moreButton.locator('editor-menu-action').filter({ + hasText: 'Duplicate', + }); await actionButton.click(); break; } - case 'changeShapeFillColor': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-shape-button') - .getByRole('button', { name: 'Fill color' }); - await button.click(); - break; - } + case 'changeShapeColor': + case 'changeShapeFillColor': case 'changeShapeStrokeColor': case 'changeShapeStrokeStyles': { const button = locatorComponentToolbar(page) - .locator('edgeless-change-shape-button') - .getByRole('button', { name: 'Border style' }); + .locator('edgeless-shape-color-picker') + .getByLabel(/^Color$/); await button.click(); break; } case 'changeShapeStyle': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-shape-button') - .getByRole('button', { name: /^Style$/ }); - await button.click(); - break; - } - case 'changeConnectorStrokeColor': { - const button = page - .locator('edgeless-change-connector-button') - .getByRole('button', { name: 'Stroke style' }); + const button = locatorComponentToolbar(page).getByLabel(/^Style$/); await button.click(); break; } case 'changeConnectorStrokeStyles': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-connector-button') - .getByRole('button', { name: 'Stroke style' }); + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Stroke style', + }); await button.click(); break; } case 'changeConnectorShape': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-connector-button') - .getByRole('button', { name: 'Shape' }); + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Shape', + }); await button.click(); break; } case 'addFrame': { - const button = locatorComponentToolbar(page).locator( - 'edgeless-add-frame-button' - ); + const button = locatorComponentToolbar(page).getByLabel(/^Frame$/); await button.click(); break; } case 'addGroup': { - const button = locatorComponentToolbar(page).locator( - 'edgeless-add-group-button' - ); + const button = locatorComponentToolbar(page).getByLabel(/^Group$/); await button.click(); break; } @@ -1223,67 +1188,64 @@ export async function triggerComponentToolbarAction( const moreButton = locatorComponentToolbarMoreButton(page); await moreButton.click(); - const actionButton = moreButton - .locator('.more-actions-container editor-menu-action') - .filter({ - hasText: 'Group Section', - }); + const actionButton = moreButton.locator('editor-menu-action').filter({ + hasText: 'Group Section', + }); await actionButton.click(); break; } case 'ungroup': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-group-button') - .getByRole('button', { name: 'Ungroup' }); + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Ungroup', + }); await button.click(); break; } case 'renameGroup': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-group-button') - .getByRole('button', { name: 'Rename' }); + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Rename', + }); await button.click(); break; } case 'releaseFromGroup': { - const button = locatorComponentToolbar(page).locator( - 'edgeless-release-from-group-button' - ); + const button = + locatorComponentToolbar(page).getByLabel('Release from group'); await button.click(); break; } case 'changeNoteColor': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-note-button') - .getByRole('button', { name: 'Background' }); + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Background', + }); await button.click(); break; } case 'changeNoteDisplayMode': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-note-button') - .getByRole('button', { name: 'Mode' }); + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Mode', + }); await button.click(); break; } case 'changeNoteSlicerSetting': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-note-button') - .getByRole('button', { name: 'Slicer' }); + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Slicer', + }); await button.click(); break; } case 'changeNoteScale': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-note-button') - .getByRole('button', { name: 'Scale' }); + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Scale', + }); await button.click(); break; } case 'autoSize': { - const button = locatorComponentToolbar(page) - .locator('edgeless-change-note-button') - .getByRole('button', { name: 'Size' }); + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Size', + }); await button.click(); break; } @@ -1305,11 +1267,9 @@ export async function triggerComponentToolbarAction( const moreButton = locatorComponentToolbarMoreButton(page); await moreButton.click(); - const actionButton = moreButton - .locator('.more-actions-container editor-menu-action') - .filter({ - hasText: 'Turn into linked doc', - }); + const actionButton = moreButton.locator('editor-menu-action').filter({ + hasText: 'Turn into linked doc', + }); await actionButton.click(); break; } @@ -1317,11 +1277,9 @@ export async function triggerComponentToolbarAction( const moreButton = locatorComponentToolbarMoreButton(page); await moreButton.click(); - const actionButton = moreButton - .locator('.more-actions-container editor-menu-action') - .filter({ - hasText: 'Create linked doc', - }); + const actionButton = moreButton.locator('editor-menu-action').filter({ + hasText: 'Create linked doc', + }); await actionButton.click(); break; } @@ -1356,24 +1314,18 @@ export async function triggerComponentToolbarAction( break; } case 'autoArrange': { - const button = locatorComponentToolbar(page).locator( - 'edgeless-align-button' - ); + const toolbar = locatorComponentToolbar(page); + const button = toolbar.getByLabel('Align objects'); await button.click(); - const arrange = button.locator('editor-icon-button').filter({ - hasText: 'Auto arrange', - }); + const arrange = toolbar.getByLabel('Auto arrange'); await arrange.click(); break; } case 'autoResize': { - const button = locatorComponentToolbar(page).locator( - 'edgeless-align-button' - ); + const toolbar = locatorComponentToolbar(page); + const button = toolbar.getByLabel('Align objects'); await button.click(); - const resize = button.locator('editor-icon-button').filter({ - hasText: 'Resize & Align', - }); + const resize = toolbar.getByLabel('Resize & Align'); await resize.click(); break; } @@ -1382,7 +1334,7 @@ export async function triggerComponentToolbarAction( export async function changeEdgelessNoteBackground(page: Page, label: string) { const colorButton = page - .locator('edgeless-change-note-button') + .locator('edgeless-color-picker-button') .locator('edgeless-color-panel') .locator(`.color-unit[aria-label="${label}"]`); await colorButton.click(); @@ -1390,18 +1342,16 @@ export async function changeEdgelessNoteBackground(page: Page, label: string) { export async function changeShapeFillColor(page: Page, label: string) { const colorButton = page - .locator('edgeless-change-shape-button') - .locator('edgeless-color-picker-button.fill-color') - .locator('edgeless-color-panel') + .locator('edgeless-shape-color-picker') + .locator('edgeless-color-panel[aria-label="Fill color"]') .locator(`.color-unit[aria-label="${label}"]`); await colorButton.click({ force: true }); } export async function changeShapeFillColorToTransparent(page: Page) { const colorButton = page - .locator('edgeless-change-shape-button') - .locator('edgeless-color-picker-button.fill-color') - .locator('edgeless-color-panel') + .locator('edgeless-shape-color-picker') + .locator('edgeless-color-panel[aria-label="Fill color"]') .locator('edgeless-color-custom-button'); await colorButton.click({ force: true }); @@ -1417,9 +1367,8 @@ export async function changeShapeFillColorToTransparent(page: Page) { export async function changeShapeStrokeColor(page: Page, color: string) { const colorButton = page - .locator('edgeless-change-shape-button') - .locator('edgeless-color-picker-button.border-style') - .locator('edgeless-color-panel') + .locator('edgeless-shape-color-picker') + .locator('edgeless-color-panel[aria-label="Border color"]') .locator(`.color-unit[aria-label="${color}"]`); await colorButton.click(); } @@ -1446,10 +1395,13 @@ export async function resizeConnectorByStartCapitalHandler( } export function getEdgelessLineWidthPanel(page: Page) { - return page - .locator('edgeless-change-shape-button') - .locator('edgeless-line-width-panel') - .locator('.line-width-panel'); + return ( + page + .locator('affine-toolbar-widget editor-toolbar') + // TODO(@fundon): remove ` edgeless-line-width-panel` + .locator('affine-edgeless-line-width-panel') + .locator('.line-width-panel') + ); } export async function changeShapeStrokeWidth(page: Page) { const lineWidthPanel = getEdgelessLineWidthPanel(page); @@ -1468,7 +1420,7 @@ export function locatorShapeStrokeStyleButton( mode: 'solid' | 'dash' | 'none' ) { return page - .locator('edgeless-change-shape-button') + .locator('affine-toolbar-widget editor-toolbar') .locator(`.line-style-button.mode-${mode}`); } @@ -1485,7 +1437,7 @@ export function locatorShapeStyleButton( style: 'general' | 'scribbled' ) { return page - .locator('edgeless-change-shape-button') + .locator('affine-toolbar-widget editor-toolbar') .locator('edgeless-shape-style-panel') .getByRole('button', { name: style }); } @@ -1499,8 +1451,7 @@ export async function changeShapeStyle( } export async function changeConnectorStrokeColor(page: Page, color: string) { - const colorButton = page - .locator('edgeless-change-connector-button') + const colorButton = locatorComponentToolbar(page) .locator('edgeless-color-panel') .getByLabel(color); await colorButton.click(); @@ -1510,10 +1461,12 @@ export function locatorConnectorStrokeWidthButton( page: Page, buttonPosition: number ) { - return page - .locator('edgeless-change-connector-button') - .locator(`edgeless-line-width-panel`) - .locator(`.line-width-button:nth-child(${buttonPosition})`); + return ( + locatorComponentToolbar(page) + // TODO(@fundon): remove redundant components + .locator('affine-edgeless-line-width-panel') + .locator(`.line-width-button:nth-child(${buttonPosition})`) + ); } export async function changeConnectorStrokeWidth( page: Page, @@ -1527,9 +1480,9 @@ export function locatorConnectorStrokeStyleButton( page: Page, mode: 'solid' | 'dash' | 'none' ) { - return page - .locator('edgeless-change-connector-button') - .locator(`.line-style-button.mode-${mode}`); + return locatorComponentToolbar(page).locator( + `.line-style-button.mode-${mode}` + ); } export async function changeConnectorStrokeStyle( page: Page, diff --git a/tests/blocksuite/e2e/utils/asserts.ts b/tests/blocksuite/e2e/utils/asserts.ts index 63495ed316..ce008f7124 100644 --- a/tests/blocksuite/e2e/utils/asserts.ts +++ b/tests/blocksuite/e2e/utils/asserts.ts @@ -1197,7 +1197,7 @@ export async function assertConnectorStrokeColor( color: string ) { const colorButton = page - .locator('edgeless-change-connector-button') + .locator('affine-toolbar-widget editor-toolbar') .locator('edgeless-color-panel') .locator(`.color-unit[aria-label="${label}"]`) .locator('svg'); diff --git a/tests/kit/src/utils/editor.ts b/tests/kit/src/utils/editor.ts index 79417dbc08..63904f7c10 100644 --- a/tests/kit/src/utils/editor.ts +++ b/tests/kit/src/utils/editor.ts @@ -4,7 +4,6 @@ import { expect, type Locator, type Page } from '@playwright/test'; declare type _GLOBAL_ = typeof BlocksuiteEffects; -const EDGELESS_ELEMENT_TOOLBAR_WIDGET = 'edgeless-element-toolbar-widget'; const EDGELESS_TOOLBAR_WIDGET = 'edgeless-toolbar-widget'; export function locateModeSwitchButton( @@ -408,12 +407,6 @@ export async function resizeElementByHandle( await dragView(page, from, to, editorIndex); } -export function locateElementToolbar(page: Page, editorIndex = 0) { - return locateEditorContainer(page, editorIndex).locator( - EDGELESS_ELEMENT_TOOLBAR_WIDGET - ); -} - /** * Create a not block in canvas * @param position the position or xwyh of the note block in canvas