chore(editor): remove edgeless element toolbar (#10900)

This commit is contained in:
fundon
2025-03-20 02:08:21 +00:00
parent 831f290f84
commit 8b995ea420
71 changed files with 330 additions and 6449 deletions

View File

@@ -351,6 +351,7 @@ const builtinSurfaceToolbarConfig = {
}, },
{ {
id: 'e.slicer', id: 'e.slicer',
label: 'Slicer',
icon: ScissorsIcon(), icon: ScissorsIcon(),
tooltip: html`<affine-tooltip-content-with-shortcut tooltip: html`<affine-tooltip-content-with-shortcut
data-tip="${'Cutting mode'}" data-tip="${'Cutting mode'}"
@@ -369,6 +370,7 @@ const builtinSurfaceToolbarConfig = {
}, },
{ {
id: 'f.auto-height', id: 'f.auto-height',
label: 'Size',
when(ctx) { when(ctx) {
const elements = ctx.getSurfaceModelsByType(NoteBlockModel); const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
return ( return (
@@ -397,6 +399,8 @@ const builtinSurfaceToolbarConfig = {
return { return {
...options, ...options,
run(ctx) { run(ctx) {
ctx.store.captureSync();
for (const model of models) { for (const model of models) {
const edgeless = model.props.edgeless; const edgeless = model.props.edgeless;
@@ -470,15 +474,12 @@ const builtinSurfaceToolbarConfig = {
}; };
const format = (value: number) => `${value}%`; const format = (value: number) => `${value}%`;
return html`${keyed( return html`<affine-size-dropdown-menu
firstModel, @select=${onSelect}
html`<affine-size-dropdown-menu @toggle=${onToggle}
@select=${onSelect} .format=${format}
@toggle=${onToggle} .size$=${scale$}
.format=${format} ></affine-size-dropdown-menu>`;
.size$=${scale$}
></affine-size-dropdown-menu>`
)}`;
}, },
}, },
], ],

View File

@@ -266,6 +266,7 @@ export function renderAlignmentMenu(
) { ) {
return html` return html`
<editor-menu-button <editor-menu-button
aria-label="alignment-menu"
.contentPadding="${'8px'}" .contentPadding="${'8px'}"
.button=${html` .button=${html`
<editor-icon-button <editor-icon-button

View File

@@ -319,7 +319,8 @@ export const builtinConnectorToolbarConfig = {
], ],
}, },
{ {
id: 'g.add-text', id: 'g.text',
tooltip: 'Add text',
icon: AddTextIcon(), icon: AddTextIcon(),
when(ctx) { when(ctx) {
const models = ctx.getSurfaceModelsByType(ConnectorElementModel); const models = ctx.getSurfaceModelsByType(ConnectorElementModel);

View File

@@ -37,6 +37,7 @@ const builtinSurfaceToolbarConfig = {
{ {
id: 'a.insert-into-page', id: 'a.insert-into-page',
label: 'Insert into Page', label: 'Insert into Page',
showLabel: true,
tooltip: 'Insert into Page', tooltip: 'Insert into Page',
icon: PageIcon(), icon: PageIcon(),
when: ctx => ctx.getSurfaceModelsByType(FrameBlockModel).length === 1, when: ctx => ctx.getSurfaceModelsByType(FrameBlockModel).length === 1,

View File

@@ -20,6 +20,7 @@ export const builtinGroupToolbarConfig = {
{ {
id: 'a.insert-into-page', id: 'a.insert-into-page',
label: 'Insert into Page', label: 'Insert into Page',
showLabel: true,
tooltip: 'Insert into Page', tooltip: 'Insert into Page',
icon: PageIcon(), icon: PageIcon(),
when: ctx => ctx.getSurfaceModelsByType(GroupElementModel).length === 1, when: ctx => ctx.getSurfaceModelsByType(GroupElementModel).length === 1,

View File

@@ -66,6 +66,7 @@ export const builtinMiscToolbarConfig = {
placement: ActionPlacement.Start, placement: ActionPlacement.Start,
id: 'b.add-frame', id: 'b.add-frame',
label: 'Frame', label: 'Frame',
showLabel: true,
tooltip: 'Frame', tooltip: 'Frame',
icon: FrameIcon(), icon: FrameIcon(),
when(ctx) { when(ctx) {
@@ -106,6 +107,7 @@ export const builtinMiscToolbarConfig = {
placement: ActionPlacement.Start, placement: ActionPlacement.Start,
id: 'c.add-group', id: 'c.add-group',
label: 'Group', label: 'Group',
showLabel: true,
tooltip: 'Group', tooltip: 'Group',
icon: GroupingIcon(), icon: GroupingIcon(),
when(ctx) { when(ctx) {
@@ -166,6 +168,7 @@ export const builtinMiscToolbarConfig = {
{ {
placement: ActionPlacement.End, placement: ActionPlacement.End,
id: 'a.draw-connector', id: 'a.draw-connector',
label: 'Draw connector',
tooltip: 'Draw connector', tooltip: 'Draw connector',
icon: ConnectorCIcon(), icon: ConnectorCIcon(),
when(ctx) { when(ctx) {
@@ -177,7 +180,7 @@ export const builtinMiscToolbarConfig = {
const models = ctx.getSurfaceModels(); const models = ctx.getSurfaceModels();
if (!models.length) return null; if (!models.length) return null;
const { id, label, icon, tooltip } = this; const { label, icon, tooltip } = this;
const quickConnect = (e: MouseEvent) => { const quickConnect = (e: MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
@@ -194,7 +197,7 @@ export const builtinMiscToolbarConfig = {
return html` return html`
<editor-icon-button <editor-icon-button
data-testid=${id} data-testid="${'draw-connector'}"
aria-label=${label} aria-label=${label}
.tooltip=${tooltip} .tooltip=${tooltip}
@click=${quickConnect} @click=${quickConnect}
@@ -316,6 +319,7 @@ export const builtinLockedToolbarConfig = {
placement: ActionPlacement.End, placement: ActionPlacement.End,
id: 'b.unlock', id: 'b.unlock',
label: 'Click to unlock', label: 'Click to unlock',
showLabel: true,
icon: UnlockIcon(), icon: UnlockIcon(),
run(ctx) { run(ctx) {
const models = ctx.getSurfaceModels(); const models = ctx.getSurfaceModels();

View File

@@ -42,14 +42,14 @@ import {
ResetIcon, ResetIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import {
createLinkedDocFromEdgelessElements,
createLinkedDocFromNote,
} from '../../../widgets/element-toolbar/more-menu/render-linked-doc';
import { duplicate } from '../../utils/clipboard-utils'; import { duplicate } from '../../utils/clipboard-utils';
import { getSortedCloneElements } from '../../utils/clone-utils'; import { getSortedCloneElements } from '../../utils/clone-utils';
import { moveConnectors } from '../../utils/connector'; import { moveConnectors } from '../../utils/connector';
import { deleteElements } from '../../utils/crud'; import { deleteElements } from '../../utils/crud';
import {
createLinkedDocFromEdgelessElements,
createLinkedDocFromNote,
} from './render-linked-doc';
import { getEdgelessWith } from './utils'; import { getEdgelessWith } from './utils';
export const moreActions = [ export const moreActions = [

View File

@@ -306,6 +306,7 @@ export const builtinShapeToolbarConfig = {
}, },
{ {
id: 'f.text', id: 'f.text',
tooltip: 'Add text',
icon: AddTextIcon(), icon: AddTextIcon(),
when(ctx) { when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel); const models = ctx.getSurfaceModelsByType(ShapeElementModel);

View File

@@ -25,6 +25,7 @@ export function renderMenu<T>({
}: Menu<T>) { }: Menu<T>) {
return html` return html`
<editor-menu-button <editor-menu-button
aria-label="${`${label.toLowerCase()}-menu`}"
.button=${html` .button=${html`
<editor-icon-button <editor-icon-button
aria-label="${label}" aria-label="${label}"

View File

@@ -17,7 +17,6 @@ import { literal, unsafeStatic } from 'lit/static-html.js';
import { CommonSpecs } from '../common-specs/index.js'; import { CommonSpecs } from '../common-specs/index.js';
import { edgelessNavigatorBgWidget } from '../widgets/edgeless-navigator-bg/index.js'; import { edgelessNavigatorBgWidget } from '../widgets/edgeless-navigator-bg/index.js';
import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from '../widgets/edgeless-zoom-toolbar/index.js'; import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from '../widgets/edgeless-zoom-toolbar/index.js';
import { EDGELESS_ELEMENT_TOOLBAR_WIDGET } from '../widgets/element-toolbar/index.js';
import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js'; import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js';
import { EDGELESS_DRAGGING_AREA_WIDGET } from './components/rects/edgeless-dragging-area-rect.js'; import { EDGELESS_DRAGGING_AREA_WIDGET } from './components/rects/edgeless-dragging-area-rect.js';
import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js'; import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js';
@@ -29,11 +28,6 @@ export const edgelessZoomToolbarWidget = WidgetViewExtension(
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET, AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET)}` literal`${unsafeStatic(AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET)}`
); );
export const elementToolbarWidget = WidgetViewExtension(
'affine:page',
EDGELESS_ELEMENT_TOOLBAR_WIDGET,
literal`${unsafeStatic(EDGELESS_ELEMENT_TOOLBAR_WIDGET)}`
);
export const edgelessDraggingAreaWidget = WidgetViewExtension( export const edgelessDraggingAreaWidget = WidgetViewExtension(
'affine:page', 'affine:page',
EDGELESS_DRAGGING_AREA_WIDGET, EDGELESS_DRAGGING_AREA_WIDGET,
@@ -77,7 +71,6 @@ export const EdgelessRootBlockSpec: ExtensionType[] = [
edgelessRemoteSelectionWidget, edgelessRemoteSelectionWidget,
edgelessZoomToolbarWidget, edgelessZoomToolbarWidget,
frameTitleWidget, frameTitleWidget,
elementToolbarWidget,
autoConnectWidget, autoConnectWidget,
edgelessDraggingAreaWidget, edgelessDraggingAreaWidget,
noteSlicerWidget, noteSlicerWidget,

View File

@@ -85,7 +85,6 @@ import {
} from './widgets/edgeless-zoom-toolbar/index.js'; } from './widgets/edgeless-zoom-toolbar/index.js';
import { ZoomBarToggleButton } from './widgets/edgeless-zoom-toolbar/zoom-bar-toggle-button.js'; import { ZoomBarToggleButton } from './widgets/edgeless-zoom-toolbar/zoom-bar-toggle-button.js';
import { EdgelessZoomToolbar } from './widgets/edgeless-zoom-toolbar/zoom-toolbar.js'; import { EdgelessZoomToolbar } from './widgets/edgeless-zoom-toolbar/zoom-toolbar.js';
import { effects as widgetEdgelessElementToolbarEffects } from './widgets/element-toolbar/effects.js';
import { AffineImageToolbar } from './widgets/image-toolbar/components/image-toolbar.js'; import { AffineImageToolbar } from './widgets/image-toolbar/components/image-toolbar.js';
import { AFFINE_IMAGE_TOOLBAR_WIDGET } from './widgets/image-toolbar/index.js'; import { AFFINE_IMAGE_TOOLBAR_WIDGET } from './widgets/image-toolbar/index.js';
import { import {
@@ -108,7 +107,6 @@ import {
export function effects() { export function effects() {
// Run other effects // Run other effects
widgetEdgelessElementToolbarEffects();
widgetMobileToolbarEffects(); widgetMobileToolbarEffects();
widgetLinkedDocEffects(); widgetLinkedDocEffects();

View File

@@ -9,7 +9,6 @@ import type { AFFINE_SLASH_MENU_WIDGET } from '@blocksuite/affine-widget-slash-m
import type { EdgelessRootBlockComponent } from './edgeless/edgeless-root-block.js'; import type { EdgelessRootBlockComponent } from './edgeless/edgeless-root-block.js';
import type { PageRootBlockComponent } from './page/page-root-block.js'; import type { PageRootBlockComponent } from './page/page-root-block.js';
import type { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from './widgets/edgeless-zoom-toolbar/index.js'; import type { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from './widgets/edgeless-zoom-toolbar/index.js';
import type { EDGELESS_ELEMENT_TOOLBAR_WIDGET } from './widgets/element-toolbar/index.js';
import type { AFFINE_KEYBOARD_TOOLBAR_WIDGET } from './widgets/index.js'; import type { AFFINE_KEYBOARD_TOOLBAR_WIDGET } from './widgets/index.js';
import type { AFFINE_INNER_MODAL_WIDGET } from './widgets/inner-modal/inner-modal.js'; import type { AFFINE_INNER_MODAL_WIDGET } from './widgets/inner-modal/inner-modal.js';
import type { AFFINE_LINKED_DOC_WIDGET } from './widgets/linked-doc/config.js'; import type { AFFINE_LINKED_DOC_WIDGET } from './widgets/linked-doc/config.js';
@@ -37,7 +36,6 @@ export type EdgelessRootBlockWidgetName =
| typeof AFFINE_DOC_REMOTE_SELECTION_WIDGET | typeof AFFINE_DOC_REMOTE_SELECTION_WIDGET
| typeof AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET | typeof AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET
| typeof AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET | typeof AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET
| typeof EDGELESS_ELEMENT_TOOLBAR_WIDGET
| typeof AFFINE_VIEWPORT_OVERLAY_WIDGET | typeof AFFINE_VIEWPORT_OVERLAY_WIDGET
| typeof AFFINE_FRAME_TITLE_WIDGET; | typeof AFFINE_FRAME_TITLE_WIDGET;

View File

@@ -1,65 +0,0 @@
import { MindmapElementModel } from '@blocksuite/affine-model';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import type { GfxModel } from '@blocksuite/block-std/gfx';
import { Bound } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
import { FrameIcon } 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 EdgelessAddFrameButton extends WithDisposable(LitElement) {
static override styles = css`
.label {
padding-left: 4px;
}
`;
private readonly _createFrame = () => {
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`
<editor-icon-button
aria-label="Frame"
.tooltip=${'Frame'}
.labelHeight=${'20px'}
.iconSize=${'20px'}
@click=${this._createFrame}
>
${FrameIcon()}<span class="label medium">Frame</span>
</editor-icon-button>
`;
}
@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`
<edgeless-add-frame-button
.edgeless=${edgeless}
></edgeless-add-frame-button>
`;
}

View File

@@ -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`
<editor-icon-button
aria-label="Group"
.tooltip=${'Group'}
.labelHeight=${'20px'}
.iconSize=${'20px'}
@click=${this._createGroup}
>
${GroupingIcon()}<span class="label medium">Group</span>
</editor-icon-button>
`;
}
@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`
<edgeless-add-group-button
.edgeless=${edgeless}
></edgeless-add-group-button>
`;
}

View File

@@ -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`
<editor-icon-button
aria-label=${name}
.tooltip=${name}
.iconSize=${'20px'}
@click=${() => this._align(name)}
>
${content}
</editor-icon-button>
`;
}
)}
`;
}
override firstUpdated() {
this._disposables.add(
this.edgeless.service.selection.slots.updated.subscribe(() =>
this.requestUpdate()
)
);
}
override render() {
return html`
<editor-menu-button
.button=${html`
<editor-icon-button
aria-label="Align objects"
.tooltip=${'Align objects'}
>
${AlignLeftIcon(iconSize)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<div class="align-menu-content">
${this.renderIcons(HORIZONTAL_ALIGNMENT)}
${this.renderIcons(VERTICAL_ALIGNMENT)}
<div class="align-menu-separator"></div>
${this.renderIcons(AUTO_ALIGNMENT)}
</div>
</editor-menu-button>
`;
}
@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`
<edgeless-align-button .edgeless=${edgeless}></edgeless-align-button>
`;
}

View File

@@ -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`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Card style"
.tooltip=${'Card style'}
>
${PaletteIcon}
</editor-icon-button>
`}
>
<card-style-panel
.value=${this.model.props.style}
.options=${this._getCardStyleOptions}
.onSelect=${this._setCardStyle}
>
</card-style-panel>
</editor-menu-button>
`,
// TODO(@fundon): should remove it when refactoring the element toolbar
attachmentViewDropdownMenu.content(new ToolbarContext(this.std)),
html`
<editor-icon-button
aria-label="Download"
.tooltip=${'Download'}
?disabled=${this._doc.readonly}
@click=${this._download}
>
${DownloadIcon}
</editor-icon-button>
`,
html`
<editor-icon-button
aria-label="Add caption"
.tooltip=${'Add caption'}
class="change-attachment-button caption"
?disabled=${this._doc.readonly}
@click=${this._showCaption}
>
${CaptionIcon}
</editor-icon-button>
`,
].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`
<edgeless-change-attachment-button
.model=${attachments[0]}
.edgeless=${edgeless}
></edgeless-change-attachment-button>
`;
}

View File

@@ -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<K extends keyof BrushProps>(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<K extends keyof BrushProps>(
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`
<edgeless-line-width-panel
.selectedSize=${selectedSize}
@select=${this._setLineWidth}
>
</edgeless-line-width-panel>
<editor-toolbar-separator></editor-toolbar-separator>
<edgeless-color-picker-button
class="color"
.label="${'Color'}"
.pick=${this.pickColor}
.color=${selectedColor}
.theme=${colorScheme}
.originalColor=${elements[0].color}
.enableCustomColor=${enableCustomColor}
>
</edgeless-color-picker-button>
`;
}
@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`
<edgeless-change-brush-button .elements=${elements} .edgeless=${edgeless}>
</edgeless-change-brush-button>
`;
}

View File

@@ -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<ConnectorElementProps, keyof ConnectorLabelProps>,
>(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<ConnectorElementProps, keyof ConnectorLabelProps>,
>(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`
<edgeless-color-picker-button
class="stroke-color"
.label="${'Stroke style'}"
.pick=${this.pickColor}
.color=${selectedColor}
.theme=${colorScheme}
.hollowCircle=${true}
.originalColor=${elements[0].stroke}
.enableCustomColor=${enableCustomColor}
>
<div
slot="other"
class="line-styles"
style=${styleMap({
display: 'flex',
flexDirection: 'row',
gap: '8px',
alignItems: 'center',
})}
>
${LineStylesPanel({
selectedLineSize: selectedLineSize,
selectedLineStyle: selectedLineStyle,
onClick: this._setConnectorStroke,
})}
</div>
<editor-toolbar-separator
slot="separator"
data-orientation="horizontal"
></editor-toolbar-separator>
</edgeless-color-picker-button>
`,
html`
<editor-menu-button
.button=${html`
<editor-icon-button
aria-label="Style"
.tooltip=${'Style'}
.iconSize=${'20px'}
>
${choose(selectedRough, STYLE_CHOOSE)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<div>
${repeat(
STYLE_LIST,
item => item.name,
({ name, value, icon }) => html`
<editor-icon-button
aria-label=${name}
.tooltip=${name}
.active=${selectedRough === value}
.activeMode=${'background'}
.iconSize=${'20px'}
@click=${() => this._setConnectorRough(value)}
>
${icon}
</editor-icon-button>
`
)}
</div>
</editor-menu-button>
`,
html`
<editor-menu-button
.button=${html`
<editor-icon-button
aria-label="Start point style"
.tooltip=${'Start point style'}
.iconSize=${'20px'}
>
${this._getEndpointIcon(
FRONT_ENDPOINT_STYLE_LIST,
selectedStartPointStyle
)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<div>
${repeat(
FRONT_ENDPOINT_STYLE_LIST,
item => item.value,
({ value, icon }) => html`
<editor-icon-button
aria-label=${value}
.tooltip=${value}
.active=${selectedStartPointStyle === value}
.activeMode=${'background'}
.iconSize=${'20px'}
@click=${() =>
this._setConnectorPointStyle(
ConnectorEndpoint.Front,
value
)}
>
${icon}
</editor-icon-button>
`
)}
</div>
</editor-menu-button>
<editor-icon-button
aria-label="Flip direction"
.tooltip=${'Flip direction'}
.disabled=${false}
.iconSize=${'20px'}
@click=${() =>
this._flipEndpointStyle(
selectedStartPointStyle,
selectedEndPointStyle
)}
>
${FlipDirectionIcon()}
</editor-icon-button>
<editor-menu-button
.button=${html`
<editor-icon-button
aria-label="End point style"
.tooltip=${'End point style'}
.iconSize=${'20px'}
>
${this._getEndpointIcon(
REAR_ENDPOINT_STYLE_LIST,
selectedEndPointStyle
)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<div>
${repeat(
REAR_ENDPOINT_STYLE_LIST,
item => item.value,
({ value, icon }) => html`
<editor-icon-button
aria-label=${value}
.tooltip=${value}
.active=${selectedEndPointStyle === value}
.activeMode=${'background'}
.iconSize=${'20px'}
@click=${() =>
this._setConnectorPointStyle(
ConnectorEndpoint.Rear,
value
)}
>
${icon}
</editor-icon-button>
`
)}
</div>
</editor-menu-button>
<editor-menu-button
.button=${html`
<editor-icon-button
aria-label="Shape"
.tooltip=${'Connector shape'}
.iconSize=${'20px'}
>
${choose(selectedMode, MODE_CHOOSE)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<div>
${repeat(
MODE_LIST,
item => item.name,
({ name, value, icon }) => html`
<editor-icon-button
aria-label=${name}
.tooltip=${name}
.active=${selectedMode === value}
.activeMode=${'background'}
.iconSize=${'20px'}
@click=${() => this._setConnectorMode(value)}
>
${icon}
</editor-icon-button>
`
)}
</div>
</editor-menu-button>
`,
choose<string, TemplateResult<1> | typeof nothing>(
this._showAddButtonOrTextMenu(),
[
[
'button',
() => html`
<editor-icon-button
aria-label="Add text"
.tooltip=${'Add text'}
.iconSize=${'20px'}
@click=${this._addLabel}
>
${AddTextIcon()}
</editor-icon-button>
`,
],
[
'menu',
() => html`
<edgeless-change-text-menu
.elementType=${'connector'}
.elements=${this.elements}
.edgeless=${this.edgeless}
></edgeless-change-text-menu>
`,
],
['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`
<edgeless-change-connector-button
.elements=${elements}
.edgeless=${edgeless}
>
</edgeless-change-connector-button>
`;
}

View File

@@ -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`
<edgeless-change-text-menu
.elementType=${'edgeless-text'}
.elements=${elements}
.edgeless=${edgeless}
></edgeless-change-text-menu>
`;
}

View File

@@ -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<GenerateDocUrlService['generateDocUrl']>;
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<boolean>).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<boolean>).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<boolean>).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`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Open doc"
.justify=${'space-between'}
.labelHeight=${'20px'}
>
${OpenIcon}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<div data-size="small" data-orientation="vertical">
${repeat(
buttons,
button => button.label,
({ label, icon, action, disabled }) => html`
<editor-menu-action
aria-label=${ifDefined(label)}
?disabled=${disabled}
@click=${action}
>
${icon}<span class="label">${label}</span>
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`;
}
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`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Switch view"
.justify=${'space-between'}
.labelHeight=${'20px'}
.iconContainerWidth=${'110px'}
>
<div class="label">
<span style="text-transform: capitalize"
>${this._viewType}</span
>
view
</div>
${SmallArrowDownIcon}
</editor-icon-button>
`}
@toggle=${this._toggleViewSelector}
>
<div data-size="small" data-orientation="vertical">
${repeat(
buttons,
button => button.type,
({ type, label, action, disabled }) => html`
<editor-menu-action
data-testid=${`link-to-${type}`}
aria-label=${ifDefined(label)}
?data-selected=${this._viewType === type}
?disabled=${disabled || this._viewType === type}
@click=${() => {
action();
this._trackViewSelected(type);
}}
>
${label}
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`;
}
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`
<a
class="affine-link-preview"
href=${model.props.url}
rel="noopener noreferrer"
target="_blank"
>
<span>${getHostName(model.props.url)}</span>
</a>
`
: nothing,
// internal embed model
isEmbedLinkedDocBlock(model) && model.props.title
? html`
<editor-icon-button
class="doc-title"
aria-label="Doc title"
.hover=${false}
.labelHeight=${'20px'}
.tooltip=${this._originalDocTitle}
@click=${this._open}
>
<span class="label">${this._originalDocTitle}</span>
</editor-icon-button>
`
: nothing,
isHtmlBlockModel
? nothing
: html`
<editor-icon-button
aria-label="Click link"
.tooltip=${'Click link'}
class="change-embed-card-button copy"
?disabled=${this._doc.readonly}
@click=${this._copyUrl}
>
${CopyIcon}
</editor-icon-button>
<editor-icon-button
aria-label="Edit"
.tooltip=${'Edit'}
class="change-embed-card-button edit"
?disabled=${this._doc.readonly}
@click=${this._openEditPopup}
>
${EditIcon}
</editor-icon-button>
`,
this._viewSelector(),
'style' in model && this._canShowCardStylePanel
? html`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Card style"
.tooltip=${'Card style'}
>
${PaletteIcon}
</editor-icon-button>
`}
@toggle=${this._toggleCardStyleSelector}
>
<card-style-panel
.value=${model.style}
.options=${this._getCardStyleOptions}
.onSelect=${this._setCardStyle}
>
</card-style-panel>
</editor-menu-button>
`
: nothing,
'caption' in model
? html`
<editor-icon-button
aria-label="Add caption"
.tooltip=${'Add caption'}
class="change-embed-card-button caption"
?disabled=${this._doc.readonly}
@click=${this._showCaption}
>
${CaptionIcon}
</editor-icon-button>
`
: nothing,
this.quickConnectButton,
isHtmlBlockModel
? nothing
: html`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Scale"
.tooltip=${'Scale'}
.justify=${'space-between'}
.iconContainerWidth=${'65px'}
.labelHeight=${'20px'}
>
<span class="label">
${Math.round(this._embedScale * 100) + '%'}
</span>
${SmallArrowDownIcon}
</editor-icon-button>
`}
@toggle=${this._toggleCardScaleSelector}
>
<edgeless-scale-panel
class="embed-scale-popper"
.scale=${Math.round(this._embedScale * 100)}
.onSelect=${this._setEmbedScale}
></edgeless-scale-panel>
</editor-menu-button>
`,
];
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`
<edgeless-change-embed-card-button
.model=${models[0]}
.edgeless=${edgeless}
.quickConnectButton=${quickConnectButton?.pop() ?? nothing}
></edgeless-change-embed-card-button>
`;
}
function track(
std: BlockStdScope,
model: BuiltInEmbedModel,
viewType: string,
event: LinkEventType,
props: Partial<TelemetryEvent>
) {
std.getOptional(TelemetryProvider)?.track(event, {
segment: 'toolbar',
page: 'whiteboard editor',
module: 'element toolbar',
type: `${viewType} view`,
category: isInternalEmbedModel(model) ? 'linked doc' : 'link',
...props,
});
}

View File

@@ -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`
<editor-icon-button
aria-label=${'Insert into Page'}
.tooltip=${'Insert into Page'}
.iconSize=${'20px'}
.labelHeight=${'20px'}
@click=${this._insertIntoPage}
>
${PageIcon()}
<span class="label">Insert into Page</span>
</editor-icon-button>
`
: nothing,
onlyOne
? html`
<editor-icon-button
aria-label="Rename"
.tooltip=${'Rename'}
.iconSize=${'20px'}
@click=${() =>
mountFrameTitleEditor(this.frames[0], this.edgeless)}
>
${EditIcon()}
</editor-icon-button>
`
: nothing,
html`
<editor-icon-button
aria-label="Ungroup"
.tooltip=${'Ungroup'}
.iconSize=${'20px'}
@click=${() => {
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()}
</editor-icon-button>
`,
html`
<edgeless-color-picker-button
class="background"
.label="${'Background'}"
.pick=${this.pickColor}
.color=${background}
.theme=${colorScheme}
.originalColor=${this.frames[0].props.background}
.enableCustomColor=${enableCustomColor}
>
</edgeless-color-picker-button>
`,
].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`
<edgeless-change-frame-button
.edgeless=${edgeless}
.frames=${frames}
></edgeless-change-frame-button>
`;
}

View File

@@ -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`
<editor-icon-button
aria-label="Insert into Page"
.tooltip=${'Insert into Page'}
.iconSize=${'20px'}
.labelHeight=${'20px'}
@click=${this._insertIntoPage}
>
${PageIcon()}
<span class="label">Insert into Page</span>
</editor-icon-button>
`
: nothing,
onlyOne
? html`
<editor-icon-button
aria-label="Rename"
.tooltip=${'Rename'}
.iconSize=${'20px'}
@click=${() => mountGroupTitleEditor(groups[0], this.edgeless)}
>
${EditIcon()}
</editor-icon-button>
`
: nothing,
html`
<editor-icon-button
aria-label="Ungroup"
.tooltip=${'Ungroup'}
.iconSize=${'20px'}
@click=${() =>
groups.forEach(group => this.edgeless.service.ungroup(group))}
>
${UngroupIcon()}
</editor-icon-button>
`,
].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`
<edgeless-change-group-button .edgeless=${edgeless} .groups=${groups}>
</edgeless-change-group-button>
`;
}

View File

@@ -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`
<editor-icon-button
aria-label="Download"
.tooltip=${'Download'}
?disabled=${this._doc.readonly}
@click=${this._download}
>
${DownloadIcon}
</editor-icon-button>
<editor-toolbar-separator></editor-toolbar-separator>
<editor-icon-button
aria-label="Add caption"
.tooltip=${'Add caption'}
class="change-image-button caption"
?disabled=${this._doc.readonly}
@click=${this._showCaption}
>
${CaptionIcon}
</editor-icon-button>
`;
}
@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`
<edgeless-change-image-button
.model=${images[0]}
.edgeless=${edgeless}
></edgeless-change-image-button>
`;
}

View File

@@ -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`
<div
role="button"
class="style-item ${value === this.mindmapStyle ? 'active' : ''}"
@click=${() => this.onSelect(value)}
>
${icon}
</div>
`
);
}
@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`
<editor-icon-button
aria-label=${name}
.tooltip=${name}
.tipPosition=${'top'}
.active=${this.mindmapLayout === value}
.activeMode=${'background'}
@click=${() => this.onSelect(value)}
>
${icon}
</editor-icon-button>
`
);
}
@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<MindmapElementModel>();
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`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button aria-label="Style" .tooltip=${'Style'}>
${StyleIcon(iconSize)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-change-mindmap-style-panel
.mindmapStyle=${this._getCommonStyle()}
.onSelect=${this._updateStyle}
>
</edgeless-change-mindmap-style-panel>
</editor-menu-button>
`,
this._isSubnode()
? nothing
: html`
<editor-menu-button
.button=${html`
<editor-icon-button aria-label="Layout" .tooltip=${'Layout'}>
${this.layout.icon}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-change-mindmap-layout-panel
.mindmapLayout=${this.layout.value}
.onSelect=${this._updateLayoutType}
>
</edgeless-change-mindmap-layout-panel>
</editor-menu-button>
`,
].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`
<edgeless-change-mindmap-button
.elements=${mindmaps}
.nodes=${elements.filter(e => e.type === 'shape')}
.edgeless=${edgeless}
>
</edgeless-change-mindmap-button>
`;
}

View File

@@ -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<NoteDisplayMode, string>;
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`<div class=${styles.viewInPageNotifyFooter}>
<button
class=${styles.viewInPageNotifyFooterButton}
@click=${undo}
data-testid="undo-display-in-page"
>
Undo
</button>
<button
class=${styles.viewInPageNotifyFooterButton}
@click=${viewInToc}
data-testid="view-in-toc"
>
View in Toc
</button>
</div>`,
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`
<span class="display-mode-button-label">Show in</span>
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Mode"
.tooltip=${'Display mode'}
.justify=${'space-between'}
.labelHeight=${'20px'}
>
<span class="label">${currentMode}</span>
${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<note-display-mode-panel
.displayMode=${displayMode}
.onSelect=${(newMode: NoteDisplayMode) =>
this._setDisplayMode(note, newMode)}
>
</note-display-mode-panel>
</editor-menu-button>
`
: nothing,
onlyOne && !note.isPageBlock() && !this._advancedVisibilityEnabled
? html`<editor-icon-button
aria-label="Display In Page"
.showTooltip=${displayMode === NoteDisplayMode.DocAndEdgeless}
.tooltip=${'This note is part of Page Mode. Click to remove it from the page.'}
data-testid="display-in-page"
@click=${() =>
this._setDisplayMode(
note,
displayMode === NoteDisplayMode.EdgelessOnly
? NoteDisplayMode.DocAndEdgeless
: NoteDisplayMode.EdgelessOnly
)}
>
${LinkedPageIcon(buttonIconSize)}
<span class="label"
>${displayMode === NoteDisplayMode.EdgelessOnly
? 'Display In Page'
: 'Displayed In Page'}</span
>
</editor-icon-button>`
: nothing,
isDocOnly
? nothing
: html`
<edgeless-color-picker-button
class="background"
.label=${'Background'}
.pick=${this.pickColor}
.color=${background}
.colorPanelClass=${'small'}
.theme=${colorScheme}
.palettes=${DefaultTheme.NoteBackgroundColorPalettes}
.originalColor=${note.props.background}
.enableCustomColor=${enableCustomColor}
>
</edgeless-color-picker-button>
`,
isDocOnly
? nothing
: html`
<editor-menu-button
.contentPadding=${'6px'}
.button=${html`
<editor-icon-button
aria-label="Shadow style"
.tooltip=${'Shadow style'}
>
${NoteShadowDuotoneIcon(buttonIconSize)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-note-shadow-panel
.theme=${theme}
.value=${shadowType}
.background=${background}
.onSelect=${(value: string) => this._setShadowType(value)}
>
</edgeless-note-shadow-panel>
</editor-menu-button>
<editor-menu-button
.button=${html`
<editor-icon-button
aria-label="Border style"
.tooltip=${'Border style'}
>
${LineStyleIcon(buttonIconSize)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<div data-orientation="horizontal">
${LineStylesPanel({
selectedLineSize: borderSize,
selectedLineStyle: borderStyle,
onClick: event => this._setStyles(event),
})}
</div>
</editor-menu-button>
<editor-menu-button
${ref(this._cornersPanelRef)}
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button aria-label="Corners" .tooltip=${'Corners'}>
${CornerIcon(buttonIconSize)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-size-panel
.size=${borderRadius}
.sizeList=${SIZE_LIST}
.minSize=${0}
.onSelect=${(size: number) => this._setBorderRadius(size)}
.onPopperCose=${() => this._cornersPanelRef.value?.hide()}
>
</edgeless-size-panel>
</editor-menu-button>
`,
onlyOne && this._advancedVisibilityEnabled
? html`
<editor-icon-button
aria-label="Slicer"
.tooltip=${html`<affine-tooltip-content-with-shortcut
data-tip="${'Cutting mode'}"
data-shortcut="${'-'}"
></affine-tooltip-content-with-shortcut>`}
.active=${this.enableNoteSlicer}
.iconSize=${'20px'}
@click=${() => this._handleNoteSlicerButtonClick()}
>
${ScissorsIcon()}
</editor-icon-button>
`
: nothing,
onlyOne ? this.quickConnectButton : nothing,
!this.notes[0].isPageBlock() || !hasPageBlockHeader
? html`<editor-icon-button
aria-label="Size"
data-testid="edgeless-note-auto-height"
.tooltip=${collapse ? 'Auto height' : 'Customized height'}
.iconSize=${'20px'}
@click=${() => this._setCollapse()}
>
${collapse ? AutoHeightIcon() : CustomizedHeightIcon()}
</editor-icon-button>`
: nothing,
html`
<editor-menu-button
${ref(this._scalePanelRef)}
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Scale"
.tooltip=${'Scale'}
.justify=${'space-between'}
.labelHeight=${'20px'}
.iconContainerWidth=${'65px'}
>
<span class="label">${this._getScaleLabel(scale)}</span
>${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-scale-panel
.scale=${Math.round(scale * 100)}
.onSelect=${(scale: number) => this._setNoteScale(scale)}
.onPopperCose=${() => this._scalePanelRef.value?.hide()}
></edgeless-scale-panel>
</editor-menu-button>
`,
];
return join(
buttons.filter(button => button !== nothing),
renderToolbarSeparator
);
}
private accessor _cornersPanelRef: Ref<EditorMenuButton> = createRef();
private accessor _scalePanelRef: Ref<EditorMenuButton> = 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`
<edgeless-change-note-button
.notes=${notes}
.edgeless=${edgeless}
.enableNoteSlicer=${false}
.quickConnectButton=${quickConnectButton?.pop() ?? nothing}
>
</edgeless-change-note-button>
`;
}

View File

@@ -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<K extends keyof Pick<ShapeProps, 'fillColor' | 'strokeColor'>>(
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`
<editor-menu-button
.button=${html`
<editor-icon-button
aria-label="Switch type"
.tooltip=${'Switch type'}
>
${ShapeIcon(iconSize)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-shape-panel
.selectedShape=${selectedShape}
.shapeStyle=${selectedShapeStyle}
>
</edgeless-shape-panel>
</editor-menu-button>
`,
html`
<editor-menu-button
.button=${html`
<editor-icon-button aria-label="Style" .tooltip=${'Style'}>
${cache(
selectedShapeStyle === ShapeStyle.General
? StyleGeneralIcon(iconSize)
: StyleScribbleIcon(iconSize)
)}
${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-shape-style-panel
.value=${selectedShapeStyle}
.onSelect=${(value: ShapeStyle) => this._setShapeStyle(value)}
>
</edgeless-shape-style-panel>
</editor-menu-button>
`,
html`
<edgeless-color-picker-button
class="fill-color"
.label="${'Fill color'}"
.pick=${this.pickColor('fillColor')}
.color=${selectedFillColor}
.theme=${colorScheme}
.originalColor=${elements[0].fillColor}
.enableCustomColor=${enableCustomColor}
>
</edgeless-color-picker-button>
`,
html`
<edgeless-color-picker-button
class="border-style"
.label="${'Border style'}"
.pick=${this.pickColor('strokeColor')}
.color=${selectedStrokeColor}
.theme=${colorScheme}
.hollowCircle=${true}
.originalColor=${elements[0].strokeColor}
.enableCustomColor=${enableCustomColor}
>
<div
slot="other"
class="line-styles"
style=${styleMap({
display: 'flex',
flexDirection: 'row',
gap: '8px',
alignItems: 'center',
})}
>
${LineStylesPanel({
selectedLineSize: selectedLineSize,
selectedLineStyle: selectedLineStyle,
onClick: this._setShapeStyles,
})}
</div>
<editor-toolbar-separator
slot="separator"
data-orientation="horizontal"
></editor-toolbar-separator>
</edgeless-color-picker-button>
`,
choose<string, TemplateResult<1> | typeof nothing>(
this._showAddButtonOrTextMenu(),
[
[
'button',
() => html`
<editor-icon-button
aria-label="Add text"
.tooltip="${'Add text'}"
.iconSize="${'20px'}"
@click=${this._addText}
>
${AddTextIcon()}
</editor-icon-button>
`,
],
[
'menu',
() => html`
<edgeless-change-text-menu
.elementType="${'shape'}"
.elements=${elements}
.edgeless=${this.edgeless}
></edgeless-change-text-menu>
`,
],
['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`
<edgeless-change-shape-button .elements=${elements} .edgeless=${edgeless}>
</edgeless-change-shape-button>
`;
}

View File

@@ -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`
<edgeless-change-text-menu
.elementType=${'text'}
.elements=${elements}
.edgeless=${edgeless}
></edgeless-change-text-menu>
`;
}

View File

@@ -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<K extends keyof Omit<TextStyleProps, 'color'>>(
elements: SurfaceTextModel[],
field: K
) {
return countBy(elements, element => extractField(element, field));
}
function extractField<K extends keyof Omit<TextStyleProps, 'color'>>(
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<K extends keyof Omit<TextStyleProps, 'color'>>(
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`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Font"
.tooltip=${'Font'}
.justify=${'space-between'}
.labelHeight=${'20px'}
.iconContainerWidth=${'40px'}
>
<span
class="label padding0"
style=${`font-family: ${TextUtils.wrapFontFamily(selectedFontFamily)}`}
>Aa</span
>${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-font-family-panel
.value=${selectedFontFamily}
.onSelect=${this._setFontFamily}
></edgeless-font-family-panel>
</editor-menu-button>
`,
html`
<edgeless-color-picker-button
class="text-color"
.label="${'Text color'}"
.pick=${this.pickColor}
.isText=${true}
.color=${selectedColor}
.originalColor=${elements[0] instanceof ConnectorElementModel
? elements[0].labelStyle.color
: elements[0].color}
.theme=${colorScheme}
.palettes=${palettes}
.enableCustomColor=${enableCustomColor}
>
</edgeless-color-picker-button>
`,
html`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Font style"
.tooltip=${'Font style'}
.justify=${'space-between'}
.labelHeight=${'20px'}
.iconContainerWidth=${'90px'}
.disabled=${fontStyleBtnDisabled}
>
<span class="label ellipsis">
${choose(selectedFontWeight, FONT_WEIGHT_CHOOSE)}
${choose(selectedFontStyle, FONT_STYLE_CHOOSE)}
</span>
${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-font-weight-and-style-panel
.fontFamily=${selectedFontFamily}
.fontWeight=${selectedFontWeight}
.fontStyle=${selectedFontStyle}
.onSelect=${this._setFontWeightAndStyle}
></edgeless-font-weight-and-style-panel>
</editor-menu-button>
`,
this.elementType === 'edgeless-text'
? nothing
: html`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Font size"
.tooltip=${'Font size'}
.justify=${'space-between'}
.labelHeight=${'20px'}
.iconContainerWidth=${'60px'}
>
<span class="label">${selectedFontSize}</span>
${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-size-panel
data-type="check"
.size=${selectedFontSize}
.sizeList=${FONT_SIZE_LIST}
.onSelect=${this._setFontSize}
></edgeless-size-panel>
</editor-menu-button>
`,
html`
<editor-menu-button
.button=${html`
<editor-icon-button
aria-label="Alignment"
.tooltip=${'Alignment'}
>
${choose(selectedAlign, TEXT_ALIGN_CHOOSE)}${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<edgeless-align-panel
.value=${selectedAlign}
.onSelect=${this._setTextAlign}
></edgeless-align-panel>
</editor-menu-button>
`,
].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;
}

View File

@@ -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;
}
}

View File

@@ -1,6 +0,0 @@
import { ArrowDownSmallIcon } from '@blocksuite/icons/lit';
export const SmallArrowDownIcon = ArrowDownSmallIcon({
width: '16',
height: '16',
});

View File

@@ -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<ElementToolbarMoreMenuContext>[] =
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`<edgeless-lock-button
.edgeless=${this.edgeless}
></edgeless-lock-button>`,
];
}
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`<edgeless-lock-button
.edgeless=${this.edgeless}
></edgeless-lock-button>`
);
this._registeredEntries
.filter(entry => entry.when(selectedElements))
.map(entry => entry.render(this.edgeless))
.forEach(entry => entry && buttons.unshift(entry));
buttons.push(html`
<edgeless-more-button
.elements=${selectedElements}
.edgeless=${edgeless}
.groups=${this.moreGroups}
.vertical=${true}
></edgeless-more-button>
`);
return buttons;
}
private _renderQuickConnectButton() {
return [
html`
<editor-icon-button
aria-label="Draw connector"
.tooltip=${'Draw connector'}
.activeMode=${'background'}
.iconSize=${'20px'}
@click=${this._quickConnect}
>
${ConnectorCIcon()}
</editor-icon-button>
`,
];
}
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`
<editor-toolbar data-app-theme=${appTheme}>
${join(
buttons.filter(b => b !== nothing),
renderToolbarSeparator
)}
</editor-toolbar>
`;
}
@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;
}

View File

@@ -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<GfxModel>();
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`<editor-icon-button
@click=${hasLocked ? this._unlock : this._lock}
>
${icon({ width: '20px', height: '20px' })}
${hasLocked
? html`<span class="label medium">Click to unlock</span>`
: nothing}
</editor-icon-button>`;
}
@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,
});
}

View File

@@ -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`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button aria-label="More" .tooltip=${'More'}>
${this.vertical
? MoreVerticalIcon({ width: '20', height: '20' })
: MoreHorizontalIcon({ width: '20', height: '20' })}
</editor-icon-button>
`}
>
<div
class="more-actions-container"
data-size="large"
data-orientation="vertical"
>
${actions}
</div>
</editor-menu-button>
`;
}
@property({ attribute: false })
accessor edgeless!: EdgelessRootBlockComponent;
@property({ attribute: false })
accessor elements: GfxModel[] = [];
@property({ attribute: false })
accessor groups!: MenuItemGroup<ElementToolbarMoreMenuContext>[];
@property({ attribute: false })
accessor vertical = false;
}

View File

@@ -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<ElementToolbarMoreMenuContext> = {
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<ElementToolbarMoreMenuContext> = {
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<ElementToolbarMoreMenuContext> = {
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<ElementToolbarMoreMenuContext> = {
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<ElementToolbarMoreMenuContext> = {
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<ElementToolbarMoreMenuContext> = {
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,
];

View File

@@ -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)
);
}
}

View File

@@ -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`
<editor-icon-button
aria-label="Release from group"
.tooltip=${'Release from group'}
.iconSize=${'20px'}
@click=${() => this._releaseFromGroup()}
>
${ReleaseFromGroupIcon()}
</editor-icon-button>
`;
}
@property({ attribute: false })
accessor edgeless!: EdgelessRootBlockComponent;
}
export function renderReleaseFromGroupButton(
edgeless: EdgelessRootBlockComponent
) {
return html`
<edgeless-release-from-group-button
.edgeless=${edgeless}
></edgeless-release-from-group-button>
`;
}

View File

@@ -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'),
},
});

View File

@@ -1,9 +1,5 @@
export { EDGELESS_TOOLBAR_WIDGET } from '../edgeless/components/toolbar/edgeless-toolbar.js'; export { EDGELESS_TOOLBAR_WIDGET } from '../edgeless/components/toolbar/edgeless-toolbar.js';
export { AffineEdgelessZoomToolbarWidget } from './edgeless-zoom-toolbar/index.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 { AffineImageToolbarWidget } from './image-toolbar/index.js';
export { AffineInnerModalWidget } from './inner-modal/inner-modal.js'; export { AffineInnerModalWidget } from './inner-modal/inner-modal.js';
export * from './keyboard-toolbar/index.js'; export * from './keyboard-toolbar/index.js';

View File

@@ -229,6 +229,7 @@ export class EdgelessShapeColorPicker extends WithDisposable(
({ label, type, value, onPick, hollowCircle }) => html` ({ label, type, value, onPick, hollowCircle }) => html`
<div class="picker-label">${label}</div> <div class="picker-label">${label}</div>
<edgeless-color-panel <edgeless-color-panel
aria-label="${label}"
role="listbox" role="listbox"
.hasTransparent=${false} .hasTransparent=${false}
.hollowCircle=${hollowCircle} .hollowCircle=${hollowCircle}

View File

@@ -1,6 +1,6 @@
import { stopPropagation } from '@blocksuite/affine-shared/utils'; import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { PropTypes, requiredProperties } from '@blocksuite/block-std'; import { PropTypes, requiredProperties } from '@blocksuite/block-std';
import { SignalWatcher } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { DoneIcon } from '@blocksuite/icons/lit'; import { DoneIcon } from '@blocksuite/icons/lit';
import type { ReadonlySignal, Signal } from '@preact/signals-core'; import type { ReadonlySignal, Signal } from '@preact/signals-core';
import { css, html, LitElement, type TemplateResult } from 'lit'; import { css, html, LitElement, type TemplateResult } from 'lit';
@@ -24,7 +24,9 @@ const SIZE_LIST: SizeItem[] = [
@requiredProperties({ @requiredProperties({
size$: PropTypes.object, size$: PropTypes.object,
}) })
export class SizeDropdownMenu extends SignalWatcher(LitElement) { export class SizeDropdownMenu extends SignalWatcher(
WithDisposable(LitElement)
) {
static override styles = css` static override styles = css`
div[data-orientation] { div[data-orientation] {
width: 68px; width: 68px;
@@ -117,9 +119,24 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) {
this.menuButton.hide(); this.menuButton.hide();
}; };
@query('input')
accessor input!: HTMLInputElement;
@query('editor-menu-button') @query('editor-menu-button')
accessor menuButton!: EditorMenuButton; accessor menuButton!: EditorMenuButton;
override firstUpdated() {
this.disposables.addFromEvent(
this.menuButton,
'toggle',
(e: CustomEvent<boolean>) => {
const opened = e.detail;
if (opened) return;
this.input.value = '';
}
);
}
override render() { override render() {
const { const {
sizes, sizes,
@@ -134,6 +151,7 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) {
return html` return html`
<editor-menu-button <editor-menu-button
class="${`${label.toLowerCase()}-menu`}"
.contentPadding="${'8px'}" .contentPadding="${'8px'}"
.button=${html` .button=${html`
<editor-icon-button <editor-icon-button
@@ -155,7 +173,7 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) {
({ key, value }) => key ?? value, ({ key, value }) => key ?? value,
({ key, value }) => html` ({ key, value }) => html`
<editor-menu-action <editor-menu-action
aria-label="${key}" aria-label="${key ?? value}"
?data-selected="${size === value}" ?data-selected="${size === value}"
@click=${() => this.select(value)} @click=${() => this.select(value)}
> >

View File

@@ -19,6 +19,7 @@ type ActionBase = {
export type ToolbarAction = ActionBase & { export type ToolbarAction = ActionBase & {
label?: string; label?: string;
showLabel?: boolean;
icon?: TemplateResult; icon?: TemplateResult;
tooltip?: string | TemplateResult; tooltip?: string | TemplateResult;
variant?: 'destructive'; variant?: 'destructive';

View File

@@ -373,8 +373,16 @@ export class AffineToolbarWidget extends WidgetComponent {
// Triggered only when not in editing state. // Triggered only when not in editing state.
disposables.add( disposables.add(
context.gfx.selection.slots.updated.subscribe(selections => { context.gfx.selection.slots.updated.subscribe(selections => {
// TODO(@fundon): should remove it when edgeless element toolbar is removed // Should remove selections when clicking on frame navigator
if (context.isEdgelessMode) return; if (context.isPageMode) {
if (
std.host.contains(std.range.value?.commonAncestorContainer ?? null)
) {
std.range.clear();
}
context.reset();
return;
}
const elementIds = selections const elementIds = selections
.map(s => (s.editing || s.inoperable ? [] : s.elements)) .map(s => (s.editing || s.inoperable ? [] : s.elements))

View File

@@ -232,7 +232,7 @@ export function renderToolbar(
`${flavour}:${key}`, `${flavour}:${key}`,
html` html`
<editor-menu-button <editor-menu-button
class="more-menu" aria-label="more-menu"
.contentPadding="${'8px'}" .contentPadding="${'8px'}"
.button=${html` .button=${html`
<editor-icon-button aria-label="More" .tooltip="${'More'}"> <editor-icon-button aria-label="More" .tooltip="${'More'}">
@@ -319,7 +319,9 @@ function renderActionItem(action: ToolbarAction, context: ToolbarContext) {
@click=${() => action.run?.(context)} @click=${() => action.run?.(context)}
> >
${action.icon} ${action.icon}
${action.label ? html`<span class="label">${action.label}</span>` : null} ${action.showLabel && action.label
? html`<span class="label">${action.label}</span>`
: null}
</editor-icon-button> </editor-icon-button>
`; `;
} }

View File

@@ -45,7 +45,7 @@ import {
actionToErrorResponse, actionToErrorResponse,
actionToGenerating, actionToGenerating,
actionToResponse, actionToResponse,
getElementToolbar, getToolbar,
} from './edgeless-response'; } from './edgeless-response';
async function getContentFromEmbedSyncedDocModel( async function getContentFromEmbedSyncedDocModel(
@@ -400,7 +400,7 @@ export function actionToHandler<T extends keyof BlockSuitePresets.AIActions>(
trackerOptions trackerOptions
); );
const elementToolbar = getElementToolbar(host); const toolbar = getToolbar(host);
const isEmpty = selectedElements.length === 0; const isEmpty = selectedElements.length === 0;
const isCreateImageAction = id === 'createImage'; const isCreateImageAction = id === 'createImage';
const isMakeItRealAction = !isCreateImageAction && id === 'makeItReal'; const isMakeItRealAction = !isCreateImageAction && id === 'makeItReal';
@@ -411,8 +411,8 @@ export function actionToHandler<T extends keyof BlockSuitePresets.AIActions>(
referenceElement = selectedBlocks.at(-1); referenceElement = selectedBlocks.at(-1);
} else if (edgelessCopilot.visible && edgelessCopilot.selectionElem) { } else if (edgelessCopilot.visible && edgelessCopilot.selectionElem) {
referenceElement = edgelessCopilot.selectionElem; referenceElement = edgelessCopilot.selectionElem;
} else if (elementToolbar.toolbarVisible) { } else if (toolbar?.dataset.open) {
referenceElement = getElementToolbar(host); referenceElement = toolbar;
} else if (!isEmpty) { } else if (!isEmpty) {
const lastSelected = selectedElements.at(-1)?.id; const lastSelected = selectedElements.at(-1)?.id;
if (!lastSelected) return; if (!lastSelected) return;

View File

@@ -5,10 +5,6 @@ import {
EDGELESS_TEXT_BLOCK_MIN_WIDTH, EDGELESS_TEXT_BLOCK_MIN_WIDTH,
} from '@blocksuite/affine/blocks/edgeless-text'; } from '@blocksuite/affine/blocks/edgeless-text';
import { addImages } from '@blocksuite/affine/blocks/image'; import { addImages } from '@blocksuite/affine/blocks/image';
import {
EDGELESS_ELEMENT_TOOLBAR_WIDGET,
type EdgelessElementToolbarWidget,
} from '@blocksuite/affine/blocks/root';
import { import {
fitContent, fitContent,
getSurfaceBlock, getSurfaceBlock,
@@ -26,6 +22,10 @@ import {
NoteDisplayMode, NoteDisplayMode,
} from '@blocksuite/affine/model'; } from '@blocksuite/affine/model';
import { TelemetryProvider } from '@blocksuite/affine/shared/services'; import { TelemetryProvider } from '@blocksuite/affine/shared/services';
import {
AFFINE_TOOLBAR_WIDGET,
type AffineToolbarWidget,
} from '@blocksuite/affine/widgets/toolbar';
import { import {
ChatWithAiIcon, ChatWithAiIcon,
DeleteIcon, DeleteIcon,
@@ -65,16 +65,14 @@ type ErrorConfig = Exclude<
null null
>['errorStateConfig']; >['errorStateConfig'];
export function getElementToolbar( export function getToolbar(host: EditorHost) {
host: EditorHost
): EdgelessElementToolbarWidget {
const rootBlockId = host.doc.root?.id as string; const rootBlockId = host.doc.root?.id as string;
const elementToolbar = host.view.getWidget( const toolbar = host.view.getWidget(
EDGELESS_ELEMENT_TOOLBAR_WIDGET, AFFINE_TOOLBAR_WIDGET,
rootBlockId rootBlockId
) as EdgelessElementToolbarWidget; ) as AffineToolbarWidget;
return elementToolbar; return toolbar.querySelector('editor-toolbar');
} }
export function getTriggerEntry(host: EditorHost) { export function getTriggerEntry(host: EditorHost) {

View File

@@ -1,7 +1,3 @@
import type {
EdgelessElementToolbarWidget,
EdgelessRootBlockComponent,
} from '@blocksuite/affine/blocks/root';
import { noop } from '@blocksuite/affine/global/utils'; import { noop } from '@blocksuite/affine/global/utils';
import type { DocMode } from '@blocksuite/affine/model'; import type { DocMode } from '@blocksuite/affine/model';
import { import {
@@ -25,64 +21,6 @@ export function setupEdgelessCopilot(widget: EdgelessCopilotWidget) {
widget.groups = edgelessAIGroups; 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`<edgeless-copilot-toolbar-entry
.host=${edgeless.host}
.groups=${edgelessAIGroups}
.onClick=${handler}
></edgeless-copilot-toolbar-entry>`;
},
});
}
export function edgelessToolbarAIEntryConfig(): ToolbarModuleConfig { export function edgelessToolbarAIEntryConfig(): ToolbarModuleConfig {
return { return {
actions: [ actions: [

View File

@@ -2,10 +2,7 @@ import {
BlockFlavourIdentifier, BlockFlavourIdentifier,
LifeCycleWatcher, LifeCycleWatcher,
} from '@blocksuite/affine/block-std'; } from '@blocksuite/affine/block-std';
import { import { EdgelessRootBlockSpec } from '@blocksuite/affine/blocks/root';
EdgelessElementToolbarWidget,
EdgelessRootBlockSpec,
} from '@blocksuite/affine/blocks/root';
import { ToolbarModuleExtension } from '@blocksuite/affine/shared/services'; import { ToolbarModuleExtension } from '@blocksuite/affine/shared/services';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
import type { FrameworkProvider } from '@toeverything/infra'; import type { FrameworkProvider } from '@toeverything/infra';
@@ -15,7 +12,6 @@ import { toolbarAIEntryConfig } from '../entries';
import { import {
edgelessToolbarAIEntryConfig, edgelessToolbarAIEntryConfig,
setupEdgelessCopilot, setupEdgelessCopilot,
setupEdgelessElementToolbarAIEntry,
} from '../entries/edgeless/index'; } from '../entries/edgeless/index';
import { setupSpaceAIEntry } from '../entries/space/setup-space'; import { setupSpaceAIEntry } from '../entries/space/setup-space';
import { CopilotTool } from '../tool/copilot-tool'; import { CopilotTool } from '../tool/copilot-tool';
@@ -72,10 +68,6 @@ function getAIEdgelessRootWatcher(framework: FrameworkProvider) {
if (component instanceof EdgelessCopilotWidget) { if (component instanceof EdgelessCopilotWidget) {
setupEdgelessCopilot(component); setupEdgelessCopilot(component);
} }
if (component instanceof EdgelessElementToolbarWidget) {
setupEdgelessElementToolbarAIEntry(component);
}
}); });
} }
} }

View File

@@ -54,7 +54,6 @@ import { getSelectedModelsCommand } from '@blocksuite/affine/shared/commands';
import { ImageSelection } from '@blocksuite/affine/shared/selection'; import { ImageSelection } from '@blocksuite/affine/shared/selection';
import { import {
ActionPlacement, ActionPlacement,
FeatureFlagService,
GenerateDocUrlProvider, GenerateDocUrlProvider,
isRemovedUserInfo, isRemovedUserInfo,
OpenDocExtensionIdentifier, OpenDocExtensionIdentifier,
@@ -316,8 +315,7 @@ function createToolbarMoreMenuConfigV2(baseUrl?: string) {
{ {
id: 'block-meta-display', id: 'block-meta-display',
when: ctx => { when: ctx => {
const featureFlag = ctx.std.get(FeatureFlagService); const isEnabled = ctx.features.getFlag('enable_block_meta');
const isEnabled = featureFlag.getFlag('enable_block_meta');
if (!isEnabled) return false; if (!isEnabled) return false;
// only display when one block is selected by block selection // only display when one block is selected by block selection

View File

@@ -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'); const frameTitle = page.locator('affine-frame-title');
await frameTitle.click(); await frameTitle.click();
await page.waitForTimeout(50); 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 toolbar = page.locator('affine-toolbar-widget editor-toolbar');
const insertIntoPageButton = changeFrameButton.locator(
`editor-icon-button[aria-label="Insert into Page"]` const insertIntoPageButton = toolbar.getByLabel('Insert into Page');
);
await insertIntoPageButton.click(); await insertIntoPageButton.click();
await clickPageModeButton(page); await clickPageModeButton(page);

View File

@@ -2,7 +2,7 @@ import { test } from '@affine-test/kit/playwright';
import { import {
clickEdgelessModeButton, clickEdgelessModeButton,
locateEditorContainer, locateEditorContainer,
locateElementToolbar, locateToolbar,
} from '@affine-test/kit/utils/editor'; } from '@affine-test/kit/utils/editor';
import { pressEnter } from '@affine-test/kit/utils/keyboard'; import { pressEnter } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page'; 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') .getByTestId('cmdk-label')
.getByText('Write, Draw, Plan all at Once.') .getByText('Write, Draw, Plan all at Once.')
.click(); .click();
const toolbar = locateElementToolbar(page); const toolbar = locateToolbar(page);
await toolbar.getByLabel('Edit').click(); await toolbar.getByLabel('Edit').click();
const editingModal = page.locator('embed-card-edit-modal'); const editingModal = page.locator('embed-card-edit-modal');

View File

@@ -5,7 +5,7 @@ import {
createEdgelessNoteBlock, createEdgelessNoteBlock,
dragView, dragView,
locateEditorContainer, locateEditorContainer,
locateElementToolbar, locateToolbar,
toViewCoord, toViewCoord,
} from '@affine-test/kit/utils/editor'; } from '@affine-test/kit/utils/editor';
import { import {
@@ -34,7 +34,7 @@ test.beforeEach(async ({ page }) => {
test('should update zindex of element when moving it into frame', async ({ test('should update zindex of element when moving it into frame', async ({
page, page,
}) => { }) => {
const toolbar = locateElementToolbar(page); const toolbar = locateToolbar(page);
// create a top frame // create a top frame
await page.keyboard.press('f'); 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 createEdgelessNoteBlock(page, [500, 500]);
await clickView(page, [0, 100]); await clickView(page, [0, 100]);
await clickView(page, [500, 500]); await clickView(page, [500, 500]);
await toolbar.getByLabel('More').click(); await toolbar.getByLabel('more-menu').click();
await toolbar.getByLabel('Send to Back').click(); await toolbar.getByLabel('Send to Back').click();
await pressEscape(page); await pressEscape(page);

View File

@@ -7,8 +7,8 @@ import {
getPageMode, getPageMode,
getSelectedXYWH, getSelectedXYWH,
locateEditorContainer, locateEditorContainer,
locateElementToolbar,
locateModeSwitchButton, locateModeSwitchButton,
locateToolbar,
moveToView, moveToView,
resizeElementByHandle, resizeElementByHandle,
toViewCoord, toViewCoord,
@@ -201,8 +201,8 @@ test.describe('edgeless note element toolbar', () => {
page, page,
}) => { }) => {
await selectAllByKeyboard(page); await selectAllByKeyboard(page);
const toolbar = locateElementToolbar(page); const toolbar = locateToolbar(page);
const autoHeight = toolbar.getByTestId('edgeless-note-auto-height'); const autoHeight = toolbar.getByTestId('auto-height');
const displayInPage = toolbar.getByTestId('display-in-page'); const displayInPage = toolbar.getByTestId('display-in-page');
await expect(toolbar).toBeVisible(); await expect(toolbar).toBeVisible();
@@ -218,8 +218,8 @@ test.describe('edgeless note element toolbar', () => {
await clickView(page, [0, 0]); await clickView(page, [0, 0]);
await clickView(page, [100, 100]); await clickView(page, [100, 100]);
const toolbar = locateElementToolbar(page); const toolbar = locateToolbar(page);
const autoHeight = toolbar.getByTestId('edgeless-note-auto-height'); const autoHeight = toolbar.getByTestId('auto-height');
const displayInPage = toolbar.getByTestId('display-in-page'); const displayInPage = toolbar.getByTestId('display-in-page');
await expect(toolbar).toBeVisible(); await expect(toolbar).toBeVisible();
@@ -237,7 +237,7 @@ test.describe('edgeless note element toolbar', () => {
await clickView(page, [0, 0]); await clickView(page, [0, 0]);
await clickView(page, [100, 100]); await clickView(page, [100, 100]);
const toolbar = locateElementToolbar(page); const toolbar = locateToolbar(page);
const displayInPage = toolbar.getByTestId('display-in-page'); const displayInPage = toolbar.getByTestId('display-in-page');
await displayInPage.click(); await displayInPage.click();
@@ -300,7 +300,7 @@ test.describe('edgeless note element toolbar', () => {
}, noteId); }, noteId);
}; };
const toolbar = locateElementToolbar(page); const toolbar = locateToolbar(page);
await selectAllByKeyboard(page); await selectAllByKeyboard(page);
const noteId = (await getEdgelessSelectedIds(page))[0]; 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.getByRole('button', { name: 'Border style' }).click();
await toolbar.locator('.mode-solid').click(); await toolbar.locator('.mode-solid').click();
await toolbar.getByRole('button', { name: 'Border style' }).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({ expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: { style: {
@@ -341,7 +345,11 @@ test.describe('edgeless note element toolbar', () => {
}); });
await toolbar.getByRole('button', { name: 'Corners' }).click(); 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({ expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: { style: {

View File

@@ -35,9 +35,7 @@ test('should add text to shape, default to pure black', async ({ page }) => {
await page.keyboard.type('text'); await page.keyboard.type('text');
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
const toolbar = page.locator( const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
'edgeless-element-toolbar-widget editor-toolbar'
);
const textColorContainer = toolbar.locator( const textColorContainer = toolbar.locator(
'edgeless-color-picker-button.text-color' '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.type('text');
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
const toolbar = page.locator( const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
'edgeless-element-toolbar-widget editor-toolbar'
);
const textColorContainer = toolbar.locator( const textColorContainer = toolbar.locator(
'edgeless-color-picker-button.text-color' 'edgeless-color-picker-button.text-color'
); );

View File

@@ -7,7 +7,7 @@ import {
focusDocTitle, focusDocTitle,
getEdgelessSelectedIds, getEdgelessSelectedIds,
getViewportCenter, getViewportCenter,
locateElementToolbar, locateToolbar,
setViewportCenter, setViewportCenter,
} from '@affine-test/kit/utils/editor'; } from '@affine-test/kit/utils/editor';
import { import {
@@ -508,12 +508,10 @@ test.describe('advanced visibility control', () => {
await expect(edgelessCard).toHaveCount(1); await expect(edgelessCard).toHaveCount(1);
await clickView(page, [100, 100]); await clickView(page, [100, 100]);
const noteButtons = locateElementToolbar(page).locator( const toolbar = locateToolbar(page);
'edgeless-change-note-button'
);
await noteButtons.getByRole('button', { name: 'Mode' }).click(); await toolbar.getByRole('button', { name: 'Mode' }).click();
await noteButtons.locator('note-display-mode-panel .item.both').click(); await toolbar.locator('note-display-mode-panel .item.both').click();
await expect(bothCard).toHaveCount(2); await expect(bothCard).toHaveCount(2);
await expect(edgelessCard).toHaveCount(0); await expect(edgelessCard).toHaveCount(0);
@@ -541,10 +539,8 @@ test.describe('advanced visibility control', () => {
await expect(bothCard).toHaveCount(2); await expect(bothCard).toHaveCount(2);
await clickView(page, [200, 100]); await clickView(page, [200, 100]);
const changeNoteButtons = locateElementToolbar(page).locator( const toolbar = locateToolbar(page);
'edgeless-change-note-button' await toolbar.getByRole('button', { name: 'Slicer' }).click();
);
await changeNoteButtons.getByRole('button', { name: 'Slicer' }).click();
await expect(page.locator('.note-slicer-button')).toBeVisible(); await expect(page.locator('.note-slicer-button')).toBeVisible();
await page.locator('.note-slicer-button').click(); await page.locator('.note-slicer-button').click();

View File

@@ -156,7 +156,7 @@ test('not allowed to switch to embed view when linking to block', async ({
await cardLink.click(); await cardLink.click();
await toolbar.getByLabel('More').click(); await toolbar.getByLabel('more-menu').click();
await toolbar.getByLabel('Copy link to block').click(); await toolbar.getByLabel('Copy link to block').click();
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');

View File

@@ -118,10 +118,10 @@ test('can open peek view for embedded frames', async ({ page }) => {
// close affine-banner // close affine-banner
await page.locator('[data-testid=local-demo-tips-close-button]').click(); 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 // insert the frame to page
await page await toolbar.getByLabel('Insert into Page').click();
.locator('edgeless-change-frame-button:has-text("Insert into Page")')
.click();
// switch back to page mode // switch back to page mode
await clickPageModeButton(page); await clickPageModeButton(page);

View File

@@ -134,10 +134,12 @@ test.describe('auto-complete', () => {
await edgelessCommonSetup(page); await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square); await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await assertSelectedBound(page, [0, 0, 100, 100]); await assertSelectedBound(page, [0, 0, 100, 100]);
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); await triggerComponentToolbarAction(page, 'changeShapeColor');
await changeShapeStrokeColor(page, 'MediumRed'); await changeShapeStrokeColor(page, 'MediumRed');
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
await changeShapeFillColor(page, 'HeavyGreen'); await changeShapeFillColor(page, 'HeavyGreen');
// Closes color pickers
await triggerComponentToolbarAction(page, 'changeShapeColor');
await dragBetweenViewCoords(page, [120, 50], [200, 0]); await dragBetweenViewCoords(page, [120, 50], [200, 0]);
const noteButton = getAutoCompletePanelButton(page, 'note'); const noteButton = getAutoCompletePanelButton(page, 'note');

View File

@@ -20,8 +20,8 @@ async function setupWithColorPickerFunction(page: Page) {
await switchEditorMode(page); await switchEditorMode(page);
} }
function getColorPickerButtonWithClass(page: Page, classes: string) { function getColorPanelWithLabel(page: Page, label: string) {
return page.locator(`edgeless-color-picker-button.${classes}`); return page.locator(`edgeless-color-panel[aria-label="${label}"]`);
} }
function getCurrentColorUnitButton(locator: Locator) { function getCurrentColorUnitButton(locator: Locator) {
@@ -71,12 +71,12 @@ test.describe('basic functions', () => {
const end0 = { x: 150, y: 200 }; const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square); await addBasicShapeElement(page, start0, end0, Shape.Square);
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
await expect(fillColorButton).toBeVisible();
await triggerComponentToolbarAction(page, 'changeShapeFillColor'); 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(); await expect(customButton).toBeVisible();
}); });
@@ -91,12 +91,13 @@ test.describe('basic functions', () => {
await triggerComponentToolbarAction(page, 'changeShapeFillColor'); await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
const customButton = getCustomButton(fillColorButton); const customButton = getCustomButton(fillColorPanel);
await customButton.click(); await customButton.click();
const colorPickerPanel = getColorPickerPanel(fillColorButton); const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
const colorPickerPanel = getColorPickerPanel(toolbar);
await expect(colorPickerPanel).toBeVisible(); await expect(colorPickerPanel).toBeVisible();
}); });
@@ -112,17 +113,18 @@ test.describe('basic functions', () => {
await triggerComponentToolbarAction(page, 'changeShapeFillColor'); await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
const currentColorUnit = getCurrentColorUnitButton(fillColorButton); const currentColorUnit = getCurrentColorUnitButton(fillColorPanel);
const value = await getCurrentColor(currentColorUnit); const value = await getCurrentColor(currentColorUnit);
await expect(currentColorUnit.locator('svg')).toHaveCSS('fill', value); await expect(currentColorUnit.locator('svg')).toHaveCSS('fill', value);
const customButton = getCustomButton(fillColorButton); const customButton = getCustomButton(fillColorPanel);
await customButton.click(); await customButton.click();
const colorPickerPanel = getColorPickerPanel(fillColorButton); const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
const colorPickerPanel = getColorPickerPanel(toolbar);
await expect(colorPickerPanel).toBeVisible(); await expect(colorPickerPanel).toBeVisible();
@@ -143,14 +145,16 @@ test.describe('basic functions', () => {
const end0 = { x: 150, y: 200 }; const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square); await addBasicShapeElement(page, start0, end0, Shape.Square);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await triggerComponentToolbarAction(page, 'changeShapeFillColor'); await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
const customButton = getCustomButton(fillColorButton); const customButton = getCustomButton(fillColorPanel);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click(); await customButton.click();
const colorPickerPanel = getColorPickerPanel(toolbar);
await expect(colorPickerPanel).toBeVisible(); await expect(colorPickerPanel).toBeVisible();
await page.mouse.click(0, 0); 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 dragBetweenCoords(page, { x: 125, y: 75 }, { x: 175, y: 225 });
await fillColorButton.click(); await toolbar.getByLabel(/^Color$/).click();
await expect(customButton).toBeVisible(); await expect(customButton).toBeVisible();
await expect(colorPickerPanel).toBeHidden(); await expect(colorPickerPanel).toBeHidden();
@@ -174,14 +178,16 @@ test.describe('basic functions', () => {
const end0 = { x: 150, y: 200 }; const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square); await addBasicShapeElement(page, start0, end0, Shape.Square);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await triggerComponentToolbarAction(page, 'changeShapeFillColor'); await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
const customButton = getCustomButton(fillColorButton); const customButton = getCustomButton(fillColorPanel);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click(); await customButton.click();
const colorPickerPanel = getColorPickerPanel(toolbar);
const paletteControl = getPaletteControl(colorPickerPanel); const paletteControl = getPaletteControl(colorPickerPanel);
const hexInput = getHexInput(colorPickerPanel); const hexInput = getHexInput(colorPickerPanel);
@@ -203,14 +209,16 @@ test.describe('basic functions', () => {
const end0 = { x: 150, y: 200 }; const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square); await addBasicShapeElement(page, start0, end0, Shape.Square);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await triggerComponentToolbarAction(page, 'changeShapeFillColor'); await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
const customButton = getCustomButton(fillColorButton); const customButton = getCustomButton(fillColorPanel);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click(); await customButton.click();
const colorPickerPanel = getColorPickerPanel(toolbar);
const hueControl = getHueControl(colorPickerPanel); const hueControl = getHueControl(colorPickerPanel);
const hexInput = getHexInput(colorPickerPanel); const hexInput = getHexInput(colorPickerPanel);
@@ -230,14 +238,16 @@ test.describe('basic functions', () => {
const end0 = { x: 150, y: 200 }; const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square); await addBasicShapeElement(page, start0, end0, Shape.Square);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await triggerComponentToolbarAction(page, 'changeShapeFillColor'); await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
const customButton = getCustomButton(fillColorButton); const customButton = getCustomButton(fillColorPanel);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click(); await customButton.click();
const colorPickerPanel = getColorPickerPanel(toolbar);
const hexInput = getHexInput(colorPickerPanel); const hexInput = getHexInput(colorPickerPanel);
await hexInput.fill('fff'); await hexInput.fill('fff');
@@ -266,14 +276,16 @@ test.describe('basic functions', () => {
const end0 = { x: 150, y: 200 }; const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square); await addBasicShapeElement(page, start0, end0, Shape.Square);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await triggerComponentToolbarAction(page, 'changeShapeFillColor'); await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
const customButton = getCustomButton(fillColorButton); const customButton = getCustomButton(fillColorPanel);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click(); await customButton.click();
const colorPickerPanel = getColorPickerPanel(toolbar);
const alphaControl = getAlphaControl(colorPickerPanel); const alphaControl = getAlphaControl(colorPickerPanel);
const alphaInput = getAlphaInput(colorPickerPanel); const alphaInput = getAlphaInput(colorPickerPanel);
@@ -295,14 +307,16 @@ test.describe('basic functions', () => {
const end0 = { x: 150, y: 200 }; const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square); await addBasicShapeElement(page, start0, end0, Shape.Square);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await triggerComponentToolbarAction(page, 'changeShapeFillColor'); await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
const customButton = getCustomButton(fillColorButton); const customButton = getCustomButton(fillColorPanel);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click(); await customButton.click();
const colorPickerPanel = getColorPickerPanel(toolbar);
const alphaInput = getAlphaInput(colorPickerPanel); const alphaInput = getAlphaInput(colorPickerPanel);
await alphaInput.fill('101'); await alphaInput.fill('101');
@@ -336,8 +350,9 @@ test.describe('basic functions', () => {
await triggerComponentToolbarAction(page, 'changeShapeFillColor'); await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
const currentColorUnit = getCurrentColorUnitButton(fillColorButton);
const currentColorUnit = getCurrentColorUnitButton(fillColorPanel);
const value = await getCurrentColor(currentColorUnit); const value = await getCurrentColor(currentColorUnit);
let rgba = parseStringToRgba(value); let rgba = parseStringToRgba(value);

View File

@@ -146,7 +146,7 @@ test('change connector line width', async ({ page }) => {
await addBasicConnectorElement(page, start, end); await addBasicConnectorElement(page, start, end);
await page.mouse.click(start.x + 5, start.y); await page.mouse.click(start.x + 5, start.y);
await triggerComponentToolbarAction(page, 'changeConnectorStrokeColor'); await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
await changeConnectorStrokeColor(page, 'MediumGrey'); await changeConnectorStrokeColor(page, 'MediumGrey');
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles'); await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
@@ -171,7 +171,7 @@ test('change connector stroke style', async ({ page }) => {
await addBasicConnectorElement(page, start, end); await addBasicConnectorElement(page, start, end);
await page.mouse.click(start.x + 5, start.y); await page.mouse.click(start.x + 5, start.y);
await triggerComponentToolbarAction(page, 'changeConnectorStrokeColor'); await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
await changeConnectorStrokeColor(page, 'MediumGrey'); await changeConnectorStrokeColor(page, 'MediumGrey');
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles'); await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');

View File

@@ -75,14 +75,15 @@ test('should be hidden when resizing element', async ({ page }) => {
const toolbar = locatorComponentToolbar(page); const toolbar = locatorComponentToolbar(page);
await expect(toolbar).toBeVisible(); 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(); await expect(toolbar).toBeVisible();
}); });

View File

@@ -107,8 +107,13 @@ test.describe('frame copy and paste', () => {
const frameTitles = page.locator('affine-frame-title'); const frameTitles = page.locator('affine-frame-title');
await frameTitles.nth(0).click(); 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 pressEscape(page);
await frameTitles.nth(0).click(); await frameTitles.nth(0).click();

View File

@@ -388,8 +388,8 @@ test('delete frame by click ungroup should not delete its children', async ({
const frameTitle = page.locator('affine-frame-title'); const frameTitle = page.locator('affine-frame-title');
await frameTitle.click(); await frameTitle.click();
const elementToolbar = page.locator('edgeless-element-toolbar-widget'); const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
const ungroupButton = elementToolbar.getByLabel('Ungroup'); const ungroupButton = toolbar.getByLabel('Ungroup');
await ungroupButton.click(); await ungroupButton.click();
await assertCanvasElementsCount(page, 1); await assertCanvasElementsCount(page, 1);

View File

@@ -6,6 +6,7 @@ import {
dragBetweenViewCoords, dragBetweenViewCoords,
edgelessCommonSetup, edgelessCommonSetup,
getFirstContainerId, getFirstContainerId,
locatorComponentToolbar,
Shape, Shape,
shiftClickView, shiftClickView,
triggerComponentToolbarAction, triggerComponentToolbarAction,
@@ -45,15 +46,16 @@ test.describe('group', () => {
page, page,
}) => { }) => {
await clickView(page, [50, 50]); await clickView(page, [50, 50]);
await expect( const toolbar = locatorComponentToolbar(page);
page.locator('edgeless-element-toolbar-widget') await expect(toolbar).toBeVisible();
).toBeVisible(); await expect(toolbar.getByLabel(/^Group$/)).not.toBeVisible();
await expect(page.locator('edgeless-add-group-button')).not.toBeVisible();
}); });
test('create button show up when multi select', async ({ page }) => { test('create button show up when multi select', async ({ page }) => {
await selectAllByKeyboard(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 }) => { test('create group by component toolbar', async ({ page }) => {

View File

@@ -42,12 +42,10 @@ import { test } from '../utils/playwright.js';
test.describe('lock', () => { test.describe('lock', () => {
const getButtons = (page: Page) => { const getButtons = (page: Page) => {
const elementToolbar = page.locator('edgeless-element-toolbar-widget'); const toolbar = page.locator('affine-toolbar-widget');
return { return {
lock: elementToolbar.locator('edgeless-lock-button[data-locked="false"]'), lock: toolbar.getByTestId('lock'),
unlock: elementToolbar.locator( unlock: toolbar.getByTestId('unlock'),
'edgeless-lock-button[data-locked="true"]'
),
}; };
}; };

View File

@@ -262,7 +262,7 @@ test('duplicate note should work correctly', async ({ page }) => {
await triggerComponentToolbarAction(page, 'duplicate'); await triggerComponentToolbarAction(page, 'duplicate');
await waitNextFrame(page, 200); // wait viewport fit animation 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(); await expect(moreActionsContainer).toBeHidden();
const noteLocator = page.locator('affine-edgeless-note'); const noteLocator = page.locator('affine-edgeless-note');

View File

@@ -35,7 +35,7 @@ async function openScalePanel(page: Page, noteId: string) {
await selectNoteInEdgeless(page, noteId); await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'changeNoteScale'); await triggerComponentToolbarAction(page, 'changeNoteScale');
await waitNextFrame(page); await waitNextFrame(page);
const scalePanel = page.locator('edgeless-scale-panel'); const scalePanel = page.locator('.scale-menu');
await expect(scalePanel).toBeVisible(); await expect(scalePanel).toBeVisible();
return scalePanel; return scalePanel;
} }
@@ -90,7 +90,7 @@ test.describe('note scale', () => {
const noteId = await setupAndAddNote(page); const noteId = await setupAndAddNote(page);
const scalePanel = await openScalePanel(page, noteId); const scalePanel = await openScalePanel(page, noteId);
const scaleInput = scalePanel.locator('.scale-input'); const scaleInput = scalePanel.locator('input');
await scaleInput.click(); await scaleInput.click();
await page.keyboard.type('50'); await page.keyboard.type('50');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
@@ -102,7 +102,7 @@ test.describe('note scale', () => {
const noteId = await setupAndAddNote(page); const noteId = await setupAndAddNote(page);
const scalePanel = await openScalePanel(page, noteId); const scalePanel = await openScalePanel(page, noteId);
const scaleInput = scalePanel.locator('.scale-input'); const scaleInput = scalePanel.locator('input');
await scaleInput.click(); await scaleInput.click();
await page.keyboard.type('50'); await page.keyboard.type('50');
await selectAllByKeyboard(page); await selectAllByKeyboard(page);

View File

@@ -8,7 +8,6 @@ import {
changeShapeStrokeColor, changeShapeStrokeColor,
changeShapeStrokeStyle, changeShapeStrokeStyle,
changeShapeStrokeWidth, changeShapeStrokeWidth,
changeShapeStyle,
clickComponentToolbarMoreMenuButton, clickComponentToolbarMoreMenuButton,
getEdgelessSelectedRect, getEdgelessSelectedRect,
locatorComponentToolbar, locatorComponentToolbar,
@@ -347,17 +346,16 @@ test('change shape stroke width', async ({ page }) => {
await addBasicRectShapeElement(page, start, end); await addBasicRectShapeElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5); await page.mouse.click(start.x + 5, start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); await triggerComponentToolbarAction(page, 'changeShapeColor');
await changeShapeStrokeColor(page, 'MediumMagenta'); await changeShapeStrokeColor(page, 'MediumMagenta');
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
await changeShapeStrokeWidth(page); await changeShapeStrokeWidth(page);
await page.mouse.click(start.x + 5, start.y + 5); await page.mouse.click(start.x + 5, start.y + 5);
await assertEdgelessSelectedRect(page, [100, 150, 100, 100]); await assertEdgelessSelectedRect(page, [100, 150, 100, 100]);
await waitNextFrame(page); await waitNextFrame(page);
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles'); await triggerComponentToolbarAction(page, 'changeShapeColor');
}); });
test('change shape stroke style', async ({ page }) => { test('change shape stroke style', async ({ page }) => {
@@ -370,14 +368,12 @@ test('change shape stroke style', async ({ page }) => {
await addBasicRectShapeElement(page, start, end); await addBasicRectShapeElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5); await page.mouse.click(start.x + 5, start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); await triggerComponentToolbarAction(page, 'changeShapeColor');
await changeShapeStrokeColor(page, 'MediumBlue'); await changeShapeStrokeColor(page, 'MediumBlue');
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
await changeShapeStrokeStyle(page, 'dash'); await changeShapeStrokeStyle(page, 'dash');
await waitNextFrame(page); await waitNextFrame(page);
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
const activeButton = locatorShapeStrokeStyleButton(page, 'dash'); const activeButton = locatorShapeStrokeStyleButton(page, 'dash');
const className = await activeButton.evaluate(ele => ele.className); const className = await activeButton.evaluate(ele => ele.className);
expect(className.includes(' active')).toBeTruthy(); expect(className.includes(' active')).toBeTruthy();
@@ -552,12 +548,12 @@ test('change shape style', async ({ page }) => {
await addBasicRectShapeElement(page, start, end); await addBasicRectShapeElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5); await page.mouse.click(start.x + 5, start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeStyle'); await triggerComponentToolbarAction(page, 'changeShapeColor');
await changeShapeStyle(page, 'general'); // The style switching feature has been removed.
//await changeShapeStyle(page, 'general');
await waitNextFrame(page); await waitNextFrame(page);
await page.mouse.click(start.x + 5, start.y + 5); await page.mouse.click(start.x + 5, start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor');
const color = 'LightPurple'; const color = 'LightPurple';
await changeShapeStrokeColor(page, color); await changeShapeStrokeColor(page, color);
await page.waitForTimeout(50); await page.waitForTimeout(50);
@@ -638,8 +634,11 @@ test.describe('shape hit test', () => {
await addBasicRectShapeElement(page, rect.start, rect.end); await addBasicRectShapeElement(page, rect.start, rect.end);
await page.mouse.click(rect.start.x + 5, rect.start.y + 5); 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); await changeShapeFillColorToTransparent(page);
// closes color picker
await triggerComponentToolbarAction(page, 'changeShapeColor');
await page.waitForTimeout(50); await page.waitForTimeout(50);
} }
@@ -715,15 +714,15 @@ test.describe('shape hit test', () => {
await pressEscape(page); await pressEscape(page);
await waitNextFrame(page); await waitNextFrame(page);
const textAlignBtn = locatorComponentToolbar(page).getByRole('button', { const alignmentMenu =
locatorComponentToolbar(page).getByLabel('alignment-menu');
const textAlignBtn = alignmentMenu.getByRole('button', {
name: 'Alignment', name: 'Alignment',
}); });
await textAlignBtn.click(); await textAlignBtn.click();
await page await alignmentMenu.getByRole('button', { name: 'Left' }).click();
.locator('edgeless-align-panel')
.getByRole('button', { name: 'Left' })
.click();
// creates an edgeless-text // creates an edgeless-text
await page.mouse.dblclick(rect.start.x + 80, rect.start.y + 20); await page.mouse.dblclick(rect.start.x + 80, rect.start.y + 20);

View File

@@ -284,7 +284,7 @@ export function locatorEdgelessComponentToolButton(
more: 'More', more: 'More',
}[type]; }[type];
const button = page const button = page
.locator('edgeless-element-toolbar-widget editor-icon-button') .locator('affine-toolbar-widget editor-toolbar editor-icon-button')
.filter({ .filter({
hasText: text, hasText: text,
}); });
@@ -592,7 +592,8 @@ export async function resizeElementByHandle(
| 'top-right' | 'top-right'
| 'bottom-right' | 'bottom-right'
| 'bottom-left' = 'top-left', | 'bottom-left' = 'top-left',
steps = 1 steps = 1,
beforeMouseUp?: () => Promise<void>
) { ) {
const handle = page.locator(`.handle[aria-label="${corner}"] .resize`); const handle = page.locator(`.handle[aria-label="${corner}"] .resize`);
const box = await handle.boundingBox(); 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 }, { x: box.x + delta.x + offset, y: box.y + delta.y + offset },
{ {
steps, steps,
beforeMouseUp,
} }
); );
} }
@@ -757,14 +759,11 @@ export function locatorNoteDisplayModeButton(
page: Page, page: Page,
mode: NoteDisplayMode mode: NoteDisplayMode
) { ) {
return page return page.locator('note-display-mode-panel').locator(`.item.${mode}`);
.locator('edgeless-change-note-button')
.locator('note-display-mode-panel')
.locator(`.item.${mode}`);
} }
export function locatorScalePanelButton(page: Page, scale: number) { 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) { export async function changeNoteDisplayMode(page: Page, mode: NoteDisplayMode) {
@@ -796,9 +795,9 @@ export async function updateExistedBrushElementSize(
} }
export async function openComponentToolbarMoreMenu(page: Page) { export async function openComponentToolbarMoreMenu(page: Page) {
const btn = page.locator( const btn = page
'edgeless-element-toolbar-widget edgeless-more-button editor-menu-button' .locator('affine-toolbar-widget editor-toolbar')
); .getByLabel('more-menu');
await btn.click(); await btn.click();
} }
@@ -1020,13 +1019,11 @@ export async function deleteAllConnectors(page: Page) {
} }
export function locatorComponentToolbar(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) { export function locatorComponentToolbarMoreButton(page: Page) {
const moreButton = locatorComponentToolbar(page).locator( const moreButton = locatorComponentToolbar(page).getByLabel('more-menu');
'edgeless-more-button'
);
return moreButton; return moreButton;
} }
type Action = type Action =
@@ -1037,10 +1034,10 @@ type Action =
| 'copyAsPng' | 'copyAsPng'
| 'changeNoteColor' | 'changeNoteColor'
| 'changeShapeStyle' | 'changeShapeStyle'
| 'changeShapeColor'
| 'changeShapeFillColor' | 'changeShapeFillColor'
| 'changeShapeStrokeColor' | 'changeShapeStrokeColor'
| 'changeShapeStrokeStyles' | 'changeShapeStrokeStyles'
| 'changeConnectorStrokeColor'
| 'changeConnectorStrokeStyles' | 'changeConnectorStrokeStyles'
| 'changeConnectorShape' | 'changeConnectorShape'
| 'addFrame' | 'addFrame'
@@ -1075,11 +1072,9 @@ export async function triggerComponentToolbarAction(
const moreButton = locatorComponentToolbarMoreButton(page); const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click(); await moreButton.click();
const actionButton = moreButton const actionButton = moreButton.locator('editor-menu-action').filter({
.locator('.more-actions-container editor-menu-action') hasText: 'Bring to Front',
.filter({ });
hasText: 'Bring to Front',
});
await actionButton.click(); await actionButton.click();
break; break;
} }
@@ -1087,11 +1082,9 @@ export async function triggerComponentToolbarAction(
const moreButton = locatorComponentToolbarMoreButton(page); const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click(); await moreButton.click();
const actionButton = moreButton const actionButton = moreButton.locator('editor-menu-action').filter({
.locator('.more-actions-container editor-menu-action') hasText: 'Bring Forward',
.filter({ });
hasText: 'Bring Forward',
});
await actionButton.click(); await actionButton.click();
break; break;
} }
@@ -1099,11 +1092,9 @@ export async function triggerComponentToolbarAction(
const moreButton = locatorComponentToolbarMoreButton(page); const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click(); await moreButton.click();
const actionButton = moreButton const actionButton = moreButton.locator('editor-menu-action').filter({
.locator('.more-actions-container editor-menu-action') hasText: 'Send Backward',
.filter({ });
hasText: 'Send Backward',
});
await actionButton.click(); await actionButton.click();
break; break;
} }
@@ -1111,11 +1102,9 @@ export async function triggerComponentToolbarAction(
const moreButton = locatorComponentToolbarMoreButton(page); const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click(); await moreButton.click();
const actionButton = moreButton const actionButton = moreButton.locator('editor-menu-action').filter({
.locator('.more-actions-container editor-menu-action') hasText: 'Send to Back',
.filter({ });
hasText: 'Send to Back',
});
await actionButton.click(); await actionButton.click();
break; break;
} }
@@ -1123,11 +1112,9 @@ export async function triggerComponentToolbarAction(
const moreButton = locatorComponentToolbarMoreButton(page); const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click(); await moreButton.click();
const actionButton = moreButton const actionButton = moreButton.locator('editor-menu-action').filter({
.locator('.more-actions-container editor-menu-action') hasText: 'Copy as PNG',
.filter({ });
hasText: 'Copy as PNG',
});
await actionButton.click(); await actionButton.click();
break; break;
} }
@@ -1135,11 +1122,9 @@ export async function triggerComponentToolbarAction(
const moreButton = locatorComponentToolbarMoreButton(page); const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click(); await moreButton.click();
const actionButton = moreButton const actionButton = moreButton.locator('editor-menu-action').filter({
.locator('.more-actions-container editor-menu-action') hasText: 'Frame Section',
.filter({ });
hasText: 'Frame Section',
});
await actionButton.click(); await actionButton.click();
break; break;
} }
@@ -1147,68 +1132,48 @@ export async function triggerComponentToolbarAction(
const moreButton = locatorComponentToolbarMoreButton(page); const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click(); await moreButton.click();
const actionButton = moreButton const actionButton = moreButton.locator('editor-menu-action').filter({
.locator('.more-actions-container editor-menu-action') hasText: 'Duplicate',
.filter({ });
hasText: 'Duplicate',
});
await actionButton.click(); await actionButton.click();
break; break;
} }
case 'changeShapeFillColor': { case 'changeShapeColor':
const button = locatorComponentToolbar(page) case 'changeShapeFillColor':
.locator('edgeless-change-shape-button')
.getByRole('button', { name: 'Fill color' });
await button.click();
break;
}
case 'changeShapeStrokeColor': case 'changeShapeStrokeColor':
case 'changeShapeStrokeStyles': { case 'changeShapeStrokeStyles': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page)
.locator('edgeless-change-shape-button') .locator('edgeless-shape-color-picker')
.getByRole('button', { name: 'Border style' }); .getByLabel(/^Color$/);
await button.click(); await button.click();
break; break;
} }
case 'changeShapeStyle': { case 'changeShapeStyle': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page).getByLabel(/^Style$/);
.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' });
await button.click(); await button.click();
break; break;
} }
case 'changeConnectorStrokeStyles': { case 'changeConnectorStrokeStyles': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page).getByRole('button', {
.locator('edgeless-change-connector-button') name: 'Stroke style',
.getByRole('button', { name: 'Stroke style' }); });
await button.click(); await button.click();
break; break;
} }
case 'changeConnectorShape': { case 'changeConnectorShape': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page).getByRole('button', {
.locator('edgeless-change-connector-button') name: 'Shape',
.getByRole('button', { name: 'Shape' }); });
await button.click(); await button.click();
break; break;
} }
case 'addFrame': { case 'addFrame': {
const button = locatorComponentToolbar(page).locator( const button = locatorComponentToolbar(page).getByLabel(/^Frame$/);
'edgeless-add-frame-button'
);
await button.click(); await button.click();
break; break;
} }
case 'addGroup': { case 'addGroup': {
const button = locatorComponentToolbar(page).locator( const button = locatorComponentToolbar(page).getByLabel(/^Group$/);
'edgeless-add-group-button'
);
await button.click(); await button.click();
break; break;
} }
@@ -1223,67 +1188,64 @@ export async function triggerComponentToolbarAction(
const moreButton = locatorComponentToolbarMoreButton(page); const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click(); await moreButton.click();
const actionButton = moreButton const actionButton = moreButton.locator('editor-menu-action').filter({
.locator('.more-actions-container editor-menu-action') hasText: 'Group Section',
.filter({ });
hasText: 'Group Section',
});
await actionButton.click(); await actionButton.click();
break; break;
} }
case 'ungroup': { case 'ungroup': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page).getByRole('button', {
.locator('edgeless-change-group-button') name: 'Ungroup',
.getByRole('button', { name: 'Ungroup' }); });
await button.click(); await button.click();
break; break;
} }
case 'renameGroup': { case 'renameGroup': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page).getByRole('button', {
.locator('edgeless-change-group-button') name: 'Rename',
.getByRole('button', { name: 'Rename' }); });
await button.click(); await button.click();
break; break;
} }
case 'releaseFromGroup': { case 'releaseFromGroup': {
const button = locatorComponentToolbar(page).locator( const button =
'edgeless-release-from-group-button' locatorComponentToolbar(page).getByLabel('Release from group');
);
await button.click(); await button.click();
break; break;
} }
case 'changeNoteColor': { case 'changeNoteColor': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page).getByRole('button', {
.locator('edgeless-change-note-button') name: 'Background',
.getByRole('button', { name: 'Background' }); });
await button.click(); await button.click();
break; break;
} }
case 'changeNoteDisplayMode': { case 'changeNoteDisplayMode': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page).getByRole('button', {
.locator('edgeless-change-note-button') name: 'Mode',
.getByRole('button', { name: 'Mode' }); });
await button.click(); await button.click();
break; break;
} }
case 'changeNoteSlicerSetting': { case 'changeNoteSlicerSetting': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page).getByRole('button', {
.locator('edgeless-change-note-button') name: 'Slicer',
.getByRole('button', { name: 'Slicer' }); });
await button.click(); await button.click();
break; break;
} }
case 'changeNoteScale': { case 'changeNoteScale': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page).getByRole('button', {
.locator('edgeless-change-note-button') name: 'Scale',
.getByRole('button', { name: 'Scale' }); });
await button.click(); await button.click();
break; break;
} }
case 'autoSize': { case 'autoSize': {
const button = locatorComponentToolbar(page) const button = locatorComponentToolbar(page).getByRole('button', {
.locator('edgeless-change-note-button') name: 'Size',
.getByRole('button', { name: 'Size' }); });
await button.click(); await button.click();
break; break;
} }
@@ -1305,11 +1267,9 @@ export async function triggerComponentToolbarAction(
const moreButton = locatorComponentToolbarMoreButton(page); const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click(); await moreButton.click();
const actionButton = moreButton const actionButton = moreButton.locator('editor-menu-action').filter({
.locator('.more-actions-container editor-menu-action') hasText: 'Turn into linked doc',
.filter({ });
hasText: 'Turn into linked doc',
});
await actionButton.click(); await actionButton.click();
break; break;
} }
@@ -1317,11 +1277,9 @@ export async function triggerComponentToolbarAction(
const moreButton = locatorComponentToolbarMoreButton(page); const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click(); await moreButton.click();
const actionButton = moreButton const actionButton = moreButton.locator('editor-menu-action').filter({
.locator('.more-actions-container editor-menu-action') hasText: 'Create linked doc',
.filter({ });
hasText: 'Create linked doc',
});
await actionButton.click(); await actionButton.click();
break; break;
} }
@@ -1356,24 +1314,18 @@ export async function triggerComponentToolbarAction(
break; break;
} }
case 'autoArrange': { case 'autoArrange': {
const button = locatorComponentToolbar(page).locator( const toolbar = locatorComponentToolbar(page);
'edgeless-align-button' const button = toolbar.getByLabel('Align objects');
);
await button.click(); await button.click();
const arrange = button.locator('editor-icon-button').filter({ const arrange = toolbar.getByLabel('Auto arrange');
hasText: 'Auto arrange',
});
await arrange.click(); await arrange.click();
break; break;
} }
case 'autoResize': { case 'autoResize': {
const button = locatorComponentToolbar(page).locator( const toolbar = locatorComponentToolbar(page);
'edgeless-align-button' const button = toolbar.getByLabel('Align objects');
);
await button.click(); await button.click();
const resize = button.locator('editor-icon-button').filter({ const resize = toolbar.getByLabel('Resize & Align');
hasText: 'Resize & Align',
});
await resize.click(); await resize.click();
break; break;
} }
@@ -1382,7 +1334,7 @@ export async function triggerComponentToolbarAction(
export async function changeEdgelessNoteBackground(page: Page, label: string) { export async function changeEdgelessNoteBackground(page: Page, label: string) {
const colorButton = page const colorButton = page
.locator('edgeless-change-note-button') .locator('edgeless-color-picker-button')
.locator('edgeless-color-panel') .locator('edgeless-color-panel')
.locator(`.color-unit[aria-label="${label}"]`); .locator(`.color-unit[aria-label="${label}"]`);
await colorButton.click(); await colorButton.click();
@@ -1390,18 +1342,16 @@ export async function changeEdgelessNoteBackground(page: Page, label: string) {
export async function changeShapeFillColor(page: Page, label: string) { export async function changeShapeFillColor(page: Page, label: string) {
const colorButton = page const colorButton = page
.locator('edgeless-change-shape-button') .locator('edgeless-shape-color-picker')
.locator('edgeless-color-picker-button.fill-color') .locator('edgeless-color-panel[aria-label="Fill color"]')
.locator('edgeless-color-panel')
.locator(`.color-unit[aria-label="${label}"]`); .locator(`.color-unit[aria-label="${label}"]`);
await colorButton.click({ force: true }); await colorButton.click({ force: true });
} }
export async function changeShapeFillColorToTransparent(page: Page) { export async function changeShapeFillColorToTransparent(page: Page) {
const colorButton = page const colorButton = page
.locator('edgeless-change-shape-button') .locator('edgeless-shape-color-picker')
.locator('edgeless-color-picker-button.fill-color') .locator('edgeless-color-panel[aria-label="Fill color"]')
.locator('edgeless-color-panel')
.locator('edgeless-color-custom-button'); .locator('edgeless-color-custom-button');
await colorButton.click({ force: true }); await colorButton.click({ force: true });
@@ -1417,9 +1367,8 @@ export async function changeShapeFillColorToTransparent(page: Page) {
export async function changeShapeStrokeColor(page: Page, color: string) { export async function changeShapeStrokeColor(page: Page, color: string) {
const colorButton = page const colorButton = page
.locator('edgeless-change-shape-button') .locator('edgeless-shape-color-picker')
.locator('edgeless-color-picker-button.border-style') .locator('edgeless-color-panel[aria-label="Border color"]')
.locator('edgeless-color-panel')
.locator(`.color-unit[aria-label="${color}"]`); .locator(`.color-unit[aria-label="${color}"]`);
await colorButton.click(); await colorButton.click();
} }
@@ -1446,10 +1395,13 @@ export async function resizeConnectorByStartCapitalHandler(
} }
export function getEdgelessLineWidthPanel(page: Page) { export function getEdgelessLineWidthPanel(page: Page) {
return page return (
.locator('edgeless-change-shape-button') page
.locator('edgeless-line-width-panel') .locator('affine-toolbar-widget editor-toolbar')
.locator('.line-width-panel'); // TODO(@fundon): remove ` edgeless-line-width-panel`
.locator('affine-edgeless-line-width-panel')
.locator('.line-width-panel')
);
} }
export async function changeShapeStrokeWidth(page: Page) { export async function changeShapeStrokeWidth(page: Page) {
const lineWidthPanel = getEdgelessLineWidthPanel(page); const lineWidthPanel = getEdgelessLineWidthPanel(page);
@@ -1468,7 +1420,7 @@ export function locatorShapeStrokeStyleButton(
mode: 'solid' | 'dash' | 'none' mode: 'solid' | 'dash' | 'none'
) { ) {
return page return page
.locator('edgeless-change-shape-button') .locator('affine-toolbar-widget editor-toolbar')
.locator(`.line-style-button.mode-${mode}`); .locator(`.line-style-button.mode-${mode}`);
} }
@@ -1485,7 +1437,7 @@ export function locatorShapeStyleButton(
style: 'general' | 'scribbled' style: 'general' | 'scribbled'
) { ) {
return page return page
.locator('edgeless-change-shape-button') .locator('affine-toolbar-widget editor-toolbar')
.locator('edgeless-shape-style-panel') .locator('edgeless-shape-style-panel')
.getByRole('button', { name: style }); .getByRole('button', { name: style });
} }
@@ -1499,8 +1451,7 @@ export async function changeShapeStyle(
} }
export async function changeConnectorStrokeColor(page: Page, color: string) { export async function changeConnectorStrokeColor(page: Page, color: string) {
const colorButton = page const colorButton = locatorComponentToolbar(page)
.locator('edgeless-change-connector-button')
.locator('edgeless-color-panel') .locator('edgeless-color-panel')
.getByLabel(color); .getByLabel(color);
await colorButton.click(); await colorButton.click();
@@ -1510,10 +1461,12 @@ export function locatorConnectorStrokeWidthButton(
page: Page, page: Page,
buttonPosition: number buttonPosition: number
) { ) {
return page return (
.locator('edgeless-change-connector-button') locatorComponentToolbar(page)
.locator(`edgeless-line-width-panel`) // TODO(@fundon): remove redundant components
.locator(`.line-width-button:nth-child(${buttonPosition})`); .locator('affine-edgeless-line-width-panel')
.locator(`.line-width-button:nth-child(${buttonPosition})`)
);
} }
export async function changeConnectorStrokeWidth( export async function changeConnectorStrokeWidth(
page: Page, page: Page,
@@ -1527,9 +1480,9 @@ export function locatorConnectorStrokeStyleButton(
page: Page, page: Page,
mode: 'solid' | 'dash' | 'none' mode: 'solid' | 'dash' | 'none'
) { ) {
return page return locatorComponentToolbar(page).locator(
.locator('edgeless-change-connector-button') `.line-style-button.mode-${mode}`
.locator(`.line-style-button.mode-${mode}`); );
} }
export async function changeConnectorStrokeStyle( export async function changeConnectorStrokeStyle(
page: Page, page: Page,

View File

@@ -1197,7 +1197,7 @@ export async function assertConnectorStrokeColor(
color: string color: string
) { ) {
const colorButton = page const colorButton = page
.locator('edgeless-change-connector-button') .locator('affine-toolbar-widget editor-toolbar')
.locator('edgeless-color-panel') .locator('edgeless-color-panel')
.locator(`.color-unit[aria-label="${label}"]`) .locator(`.color-unit[aria-label="${label}"]`)
.locator('svg'); .locator('svg');

View File

@@ -4,7 +4,6 @@ import { expect, type Locator, type Page } from '@playwright/test';
declare type _GLOBAL_ = typeof BlocksuiteEffects; declare type _GLOBAL_ = typeof BlocksuiteEffects;
const EDGELESS_ELEMENT_TOOLBAR_WIDGET = 'edgeless-element-toolbar-widget';
const EDGELESS_TOOLBAR_WIDGET = 'edgeless-toolbar-widget'; const EDGELESS_TOOLBAR_WIDGET = 'edgeless-toolbar-widget';
export function locateModeSwitchButton( export function locateModeSwitchButton(
@@ -408,12 +407,6 @@ export async function resizeElementByHandle(
await dragView(page, from, to, editorIndex); 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 * Create a not block in canvas
* @param position the position or xwyh of the note block in canvas * @param position the position or xwyh of the note block in canvas