mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 15:46:29 +08:00
chore(editor): remove edgeless element toolbar (#10900)
This commit is contained in:
@@ -351,6 +351,7 @@ const builtinSurfaceToolbarConfig = {
|
||||
},
|
||||
{
|
||||
id: 'e.slicer',
|
||||
label: 'Slicer',
|
||||
icon: ScissorsIcon(),
|
||||
tooltip: html`<affine-tooltip-content-with-shortcut
|
||||
data-tip="${'Cutting mode'}"
|
||||
@@ -369,6 +370,7 @@ const builtinSurfaceToolbarConfig = {
|
||||
},
|
||||
{
|
||||
id: 'f.auto-height',
|
||||
label: 'Size',
|
||||
when(ctx) {
|
||||
const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
return (
|
||||
@@ -397,6 +399,8 @@ const builtinSurfaceToolbarConfig = {
|
||||
return {
|
||||
...options,
|
||||
run(ctx) {
|
||||
ctx.store.captureSync();
|
||||
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
|
||||
@@ -470,15 +474,12 @@ const builtinSurfaceToolbarConfig = {
|
||||
};
|
||||
const format = (value: number) => `${value}%`;
|
||||
|
||||
return html`${keyed(
|
||||
firstModel,
|
||||
html`<affine-size-dropdown-menu
|
||||
@select=${onSelect}
|
||||
@toggle=${onToggle}
|
||||
.format=${format}
|
||||
.size$=${scale$}
|
||||
></affine-size-dropdown-menu>`
|
||||
)}`;
|
||||
return html`<affine-size-dropdown-menu
|
||||
@select=${onSelect}
|
||||
@toggle=${onToggle}
|
||||
.format=${format}
|
||||
.size$=${scale$}
|
||||
></affine-size-dropdown-menu>`;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -266,6 +266,7 @@ export function renderAlignmentMenu(
|
||||
) {
|
||||
return html`
|
||||
<editor-menu-button
|
||||
aria-label="alignment-menu"
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
|
||||
@@ -319,7 +319,8 @@ export const builtinConnectorToolbarConfig = {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'g.add-text',
|
||||
id: 'g.text',
|
||||
tooltip: 'Add text',
|
||||
icon: AddTextIcon(),
|
||||
when(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(ConnectorElementModel);
|
||||
|
||||
@@ -37,6 +37,7 @@ const builtinSurfaceToolbarConfig = {
|
||||
{
|
||||
id: 'a.insert-into-page',
|
||||
label: 'Insert into Page',
|
||||
showLabel: true,
|
||||
tooltip: 'Insert into Page',
|
||||
icon: PageIcon(),
|
||||
when: ctx => ctx.getSurfaceModelsByType(FrameBlockModel).length === 1,
|
||||
|
||||
@@ -20,6 +20,7 @@ export const builtinGroupToolbarConfig = {
|
||||
{
|
||||
id: 'a.insert-into-page',
|
||||
label: 'Insert into Page',
|
||||
showLabel: true,
|
||||
tooltip: 'Insert into Page',
|
||||
icon: PageIcon(),
|
||||
when: ctx => ctx.getSurfaceModelsByType(GroupElementModel).length === 1,
|
||||
|
||||
@@ -66,6 +66,7 @@ export const builtinMiscToolbarConfig = {
|
||||
placement: ActionPlacement.Start,
|
||||
id: 'b.add-frame',
|
||||
label: 'Frame',
|
||||
showLabel: true,
|
||||
tooltip: 'Frame',
|
||||
icon: FrameIcon(),
|
||||
when(ctx) {
|
||||
@@ -106,6 +107,7 @@ export const builtinMiscToolbarConfig = {
|
||||
placement: ActionPlacement.Start,
|
||||
id: 'c.add-group',
|
||||
label: 'Group',
|
||||
showLabel: true,
|
||||
tooltip: 'Group',
|
||||
icon: GroupingIcon(),
|
||||
when(ctx) {
|
||||
@@ -166,6 +168,7 @@ export const builtinMiscToolbarConfig = {
|
||||
{
|
||||
placement: ActionPlacement.End,
|
||||
id: 'a.draw-connector',
|
||||
label: 'Draw connector',
|
||||
tooltip: 'Draw connector',
|
||||
icon: ConnectorCIcon(),
|
||||
when(ctx) {
|
||||
@@ -177,7 +180,7 @@ export const builtinMiscToolbarConfig = {
|
||||
const models = ctx.getSurfaceModels();
|
||||
if (!models.length) return null;
|
||||
|
||||
const { id, label, icon, tooltip } = this;
|
||||
const { label, icon, tooltip } = this;
|
||||
|
||||
const quickConnect = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
@@ -194,7 +197,7 @@ export const builtinMiscToolbarConfig = {
|
||||
|
||||
return html`
|
||||
<editor-icon-button
|
||||
data-testid=${id}
|
||||
data-testid="${'draw-connector'}"
|
||||
aria-label=${label}
|
||||
.tooltip=${tooltip}
|
||||
@click=${quickConnect}
|
||||
@@ -316,6 +319,7 @@ export const builtinLockedToolbarConfig = {
|
||||
placement: ActionPlacement.End,
|
||||
id: 'b.unlock',
|
||||
label: 'Click to unlock',
|
||||
showLabel: true,
|
||||
icon: UnlockIcon(),
|
||||
run(ctx) {
|
||||
const models = ctx.getSurfaceModels();
|
||||
|
||||
@@ -42,14 +42,14 @@ import {
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
|
||||
import {
|
||||
createLinkedDocFromEdgelessElements,
|
||||
createLinkedDocFromNote,
|
||||
} from '../../../widgets/element-toolbar/more-menu/render-linked-doc';
|
||||
import { duplicate } from '../../utils/clipboard-utils';
|
||||
import { getSortedCloneElements } from '../../utils/clone-utils';
|
||||
import { moveConnectors } from '../../utils/connector';
|
||||
import { deleteElements } from '../../utils/crud';
|
||||
import {
|
||||
createLinkedDocFromEdgelessElements,
|
||||
createLinkedDocFromNote,
|
||||
} from './render-linked-doc';
|
||||
import { getEdgelessWith } from './utils';
|
||||
|
||||
export const moreActions = [
|
||||
|
||||
@@ -306,6 +306,7 @@ export const builtinShapeToolbarConfig = {
|
||||
},
|
||||
{
|
||||
id: 'f.text',
|
||||
tooltip: 'Add text',
|
||||
icon: AddTextIcon(),
|
||||
when(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||
|
||||
@@ -25,6 +25,7 @@ export function renderMenu<T>({
|
||||
}: Menu<T>) {
|
||||
return html`
|
||||
<editor-menu-button
|
||||
aria-label="${`${label.toLowerCase()}-menu`}"
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="${label}"
|
||||
|
||||
@@ -17,7 +17,6 @@ import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
import { CommonSpecs } from '../common-specs/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 { EDGELESS_ELEMENT_TOOLBAR_WIDGET } from '../widgets/element-toolbar/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_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js';
|
||||
@@ -29,11 +28,6 @@ export const edgelessZoomToolbarWidget = WidgetViewExtension(
|
||||
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(
|
||||
'affine:page',
|
||||
EDGELESS_DRAGGING_AREA_WIDGET,
|
||||
@@ -77,7 +71,6 @@ export const EdgelessRootBlockSpec: ExtensionType[] = [
|
||||
edgelessRemoteSelectionWidget,
|
||||
edgelessZoomToolbarWidget,
|
||||
frameTitleWidget,
|
||||
elementToolbarWidget,
|
||||
autoConnectWidget,
|
||||
edgelessDraggingAreaWidget,
|
||||
noteSlicerWidget,
|
||||
|
||||
@@ -85,7 +85,6 @@ import {
|
||||
} from './widgets/edgeless-zoom-toolbar/index.js';
|
||||
import { ZoomBarToggleButton } from './widgets/edgeless-zoom-toolbar/zoom-bar-toggle-button.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 { AFFINE_IMAGE_TOOLBAR_WIDGET } from './widgets/image-toolbar/index.js';
|
||||
import {
|
||||
@@ -108,7 +107,6 @@ import {
|
||||
|
||||
export function effects() {
|
||||
// Run other effects
|
||||
widgetEdgelessElementToolbarEffects();
|
||||
widgetMobileToolbarEffects();
|
||||
widgetLinkedDocEffects();
|
||||
|
||||
|
||||
@@ -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 { PageRootBlockComponent } from './page/page-root-block.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_INNER_MODAL_WIDGET } from './widgets/inner-modal/inner-modal.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_EDGELESS_REMOTE_SELECTION_WIDGET
|
||||
| typeof AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET
|
||||
| typeof EDGELESS_ELEMENT_TOOLBAR_WIDGET
|
||||
| typeof AFFINE_VIEWPORT_OVERLAY_WIDGET
|
||||
| typeof AFFINE_FRAME_TITLE_WIDGET;
|
||||
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
-159
@@ -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>
|
||||
`;
|
||||
}
|
||||
-151
@@ -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>
|
||||
`;
|
||||
}
|
||||
-604
@@ -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>
|
||||
`;
|
||||
}
|
||||
-19
@@ -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>
|
||||
`;
|
||||
}
|
||||
-912
@@ -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,
|
||||
});
|
||||
}
|
||||
-215
@@ -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>
|
||||
`;
|
||||
}
|
||||
-131
@@ -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>
|
||||
`;
|
||||
}
|
||||
-87
@@ -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>
|
||||
`;
|
||||
}
|
||||
-299
@@ -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>
|
||||
`;
|
||||
}
|
||||
-576
@@ -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>
|
||||
`;
|
||||
}
|
||||
-421
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons/lit';
|
||||
|
||||
export const SmallArrowDownIcon = ArrowDownSmallIcon({
|
||||
width: '16',
|
||||
height: '16',
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
-54
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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'),
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,5 @@
|
||||
export { EDGELESS_TOOLBAR_WIDGET } from '../edgeless/components/toolbar/edgeless-toolbar.js';
|
||||
export { AffineEdgelessZoomToolbarWidget } from './edgeless-zoom-toolbar/index.js';
|
||||
export {
|
||||
EDGELESS_ELEMENT_TOOLBAR_WIDGET,
|
||||
EdgelessElementToolbarWidget,
|
||||
} from './element-toolbar/index.js';
|
||||
export { AffineImageToolbarWidget } from './image-toolbar/index.js';
|
||||
export { AffineInnerModalWidget } from './inner-modal/inner-modal.js';
|
||||
export * from './keyboard-toolbar/index.js';
|
||||
|
||||
@@ -229,6 +229,7 @@ export class EdgelessShapeColorPicker extends WithDisposable(
|
||||
({ label, type, value, onPick, hollowCircle }) => html`
|
||||
<div class="picker-label">${label}</div>
|
||||
<edgeless-color-panel
|
||||
aria-label="${label}"
|
||||
role="listbox"
|
||||
.hasTransparent=${false}
|
||||
.hollowCircle=${hollowCircle}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
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 type { ReadonlySignal, Signal } from '@preact/signals-core';
|
||||
import { css, html, LitElement, type TemplateResult } from 'lit';
|
||||
@@ -24,7 +24,9 @@ const SIZE_LIST: SizeItem[] = [
|
||||
@requiredProperties({
|
||||
size$: PropTypes.object,
|
||||
})
|
||||
export class SizeDropdownMenu extends SignalWatcher(LitElement) {
|
||||
export class SizeDropdownMenu extends SignalWatcher(
|
||||
WithDisposable(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
div[data-orientation] {
|
||||
width: 68px;
|
||||
@@ -117,9 +119,24 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) {
|
||||
this.menuButton.hide();
|
||||
};
|
||||
|
||||
@query('input')
|
||||
accessor input!: HTMLInputElement;
|
||||
|
||||
@query('editor-menu-button')
|
||||
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() {
|
||||
const {
|
||||
sizes,
|
||||
@@ -134,6 +151,7 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) {
|
||||
|
||||
return html`
|
||||
<editor-menu-button
|
||||
class="${`${label.toLowerCase()}-menu`}"
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
@@ -155,7 +173,7 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) {
|
||||
({ key, value }) => key ?? value,
|
||||
({ key, value }) => html`
|
||||
<editor-menu-action
|
||||
aria-label="${key}"
|
||||
aria-label="${key ?? value}"
|
||||
?data-selected="${size === value}"
|
||||
@click=${() => this.select(value)}
|
||||
>
|
||||
|
||||
@@ -19,6 +19,7 @@ type ActionBase = {
|
||||
|
||||
export type ToolbarAction = ActionBase & {
|
||||
label?: string;
|
||||
showLabel?: boolean;
|
||||
icon?: TemplateResult;
|
||||
tooltip?: string | TemplateResult;
|
||||
variant?: 'destructive';
|
||||
|
||||
@@ -373,8 +373,16 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
// Triggered only when not in editing state.
|
||||
disposables.add(
|
||||
context.gfx.selection.slots.updated.subscribe(selections => {
|
||||
// TODO(@fundon): should remove it when edgeless element toolbar is removed
|
||||
if (context.isEdgelessMode) return;
|
||||
// Should remove selections when clicking on frame navigator
|
||||
if (context.isPageMode) {
|
||||
if (
|
||||
std.host.contains(std.range.value?.commonAncestorContainer ?? null)
|
||||
) {
|
||||
std.range.clear();
|
||||
}
|
||||
context.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const elementIds = selections
|
||||
.map(s => (s.editing || s.inoperable ? [] : s.elements))
|
||||
|
||||
@@ -232,7 +232,7 @@ export function renderToolbar(
|
||||
`${flavour}:${key}`,
|
||||
html`
|
||||
<editor-menu-button
|
||||
class="more-menu"
|
||||
aria-label="more-menu"
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button aria-label="More" .tooltip="${'More'}">
|
||||
@@ -319,7 +319,9 @@ function renderActionItem(action: ToolbarAction, context: ToolbarContext) {
|
||||
@click=${() => action.run?.(context)}
|
||||
>
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
actionToErrorResponse,
|
||||
actionToGenerating,
|
||||
actionToResponse,
|
||||
getElementToolbar,
|
||||
getToolbar,
|
||||
} from './edgeless-response';
|
||||
|
||||
async function getContentFromEmbedSyncedDocModel(
|
||||
@@ -400,7 +400,7 @@ export function actionToHandler<T extends keyof BlockSuitePresets.AIActions>(
|
||||
trackerOptions
|
||||
);
|
||||
|
||||
const elementToolbar = getElementToolbar(host);
|
||||
const toolbar = getToolbar(host);
|
||||
const isEmpty = selectedElements.length === 0;
|
||||
const isCreateImageAction = id === 'createImage';
|
||||
const isMakeItRealAction = !isCreateImageAction && id === 'makeItReal';
|
||||
@@ -411,8 +411,8 @@ export function actionToHandler<T extends keyof BlockSuitePresets.AIActions>(
|
||||
referenceElement = selectedBlocks.at(-1);
|
||||
} else if (edgelessCopilot.visible && edgelessCopilot.selectionElem) {
|
||||
referenceElement = edgelessCopilot.selectionElem;
|
||||
} else if (elementToolbar.toolbarVisible) {
|
||||
referenceElement = getElementToolbar(host);
|
||||
} else if (toolbar?.dataset.open) {
|
||||
referenceElement = toolbar;
|
||||
} else if (!isEmpty) {
|
||||
const lastSelected = selectedElements.at(-1)?.id;
|
||||
if (!lastSelected) return;
|
||||
|
||||
@@ -5,10 +5,6 @@ import {
|
||||
EDGELESS_TEXT_BLOCK_MIN_WIDTH,
|
||||
} from '@blocksuite/affine/blocks/edgeless-text';
|
||||
import { addImages } from '@blocksuite/affine/blocks/image';
|
||||
import {
|
||||
EDGELESS_ELEMENT_TOOLBAR_WIDGET,
|
||||
type EdgelessElementToolbarWidget,
|
||||
} from '@blocksuite/affine/blocks/root';
|
||||
import {
|
||||
fitContent,
|
||||
getSurfaceBlock,
|
||||
@@ -26,6 +22,10 @@ import {
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/affine/model';
|
||||
import { TelemetryProvider } from '@blocksuite/affine/shared/services';
|
||||
import {
|
||||
AFFINE_TOOLBAR_WIDGET,
|
||||
type AffineToolbarWidget,
|
||||
} from '@blocksuite/affine/widgets/toolbar';
|
||||
import {
|
||||
ChatWithAiIcon,
|
||||
DeleteIcon,
|
||||
@@ -65,16 +65,14 @@ type ErrorConfig = Exclude<
|
||||
null
|
||||
>['errorStateConfig'];
|
||||
|
||||
export function getElementToolbar(
|
||||
host: EditorHost
|
||||
): EdgelessElementToolbarWidget {
|
||||
export function getToolbar(host: EditorHost) {
|
||||
const rootBlockId = host.doc.root?.id as string;
|
||||
const elementToolbar = host.view.getWidget(
|
||||
EDGELESS_ELEMENT_TOOLBAR_WIDGET,
|
||||
const toolbar = host.view.getWidget(
|
||||
AFFINE_TOOLBAR_WIDGET,
|
||||
rootBlockId
|
||||
) as EdgelessElementToolbarWidget;
|
||||
) as AffineToolbarWidget;
|
||||
|
||||
return elementToolbar;
|
||||
return toolbar.querySelector('editor-toolbar');
|
||||
}
|
||||
|
||||
export function getTriggerEntry(host: EditorHost) {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import type {
|
||||
EdgelessElementToolbarWidget,
|
||||
EdgelessRootBlockComponent,
|
||||
} from '@blocksuite/affine/blocks/root';
|
||||
import { noop } from '@blocksuite/affine/global/utils';
|
||||
import type { DocMode } from '@blocksuite/affine/model';
|
||||
import {
|
||||
@@ -25,64 +21,6 @@ export function setupEdgelessCopilot(widget: EdgelessCopilotWidget) {
|
||||
widget.groups = edgelessAIGroups;
|
||||
}
|
||||
|
||||
export function setupEdgelessElementToolbarAIEntry(
|
||||
widget: EdgelessElementToolbarWidget
|
||||
) {
|
||||
widget.registerEntry({
|
||||
when: () => {
|
||||
return true;
|
||||
},
|
||||
render: (edgeless: EdgelessRootBlockComponent) => {
|
||||
const chain = edgeless.service.std.command.chain();
|
||||
const filteredGroups = edgelessAIGroups.reduce((pre, group) => {
|
||||
const filtered = group.items.filter(item =>
|
||||
item.showWhen?.(chain, 'edgeless' as DocMode, edgeless.host)
|
||||
);
|
||||
|
||||
if (filtered.length > 0) pre.push({ ...group, items: filtered });
|
||||
|
||||
return pre;
|
||||
}, [] as AIItemGroupConfig[]);
|
||||
|
||||
if (filteredGroups.every(group => group.items.length === 0)) return null;
|
||||
|
||||
const handler = () => {
|
||||
const aiPanel = getAIPanelWidget(edgeless.host);
|
||||
if (aiPanel.config) {
|
||||
aiPanel.config.generateAnswer = ({ finish, input }) => {
|
||||
finish('success');
|
||||
aiPanel.hide();
|
||||
extractSelectedContent(edgeless.host)
|
||||
.then(context => {
|
||||
AIProvider.slots.requestSendWithChat.next({
|
||||
input,
|
||||
context,
|
||||
host: edgeless.host,
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
aiPanel.config.inputCallback = text => {
|
||||
const copilotWidget = getEdgelessCopilotWidget(edgeless.host);
|
||||
const panel = copilotWidget.shadowRoot?.querySelector(
|
||||
'edgeless-copilot-panel'
|
||||
);
|
||||
if (panel instanceof HTMLElement) {
|
||||
panel.style.visibility = text ? 'hidden' : 'visible';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return html`<edgeless-copilot-toolbar-entry
|
||||
.host=${edgeless.host}
|
||||
.groups=${edgelessAIGroups}
|
||||
.onClick=${handler}
|
||||
></edgeless-copilot-toolbar-entry>`;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function edgelessToolbarAIEntryConfig(): ToolbarModuleConfig {
|
||||
return {
|
||||
actions: [
|
||||
|
||||
@@ -2,10 +2,7 @@ import {
|
||||
BlockFlavourIdentifier,
|
||||
LifeCycleWatcher,
|
||||
} from '@blocksuite/affine/block-std';
|
||||
import {
|
||||
EdgelessElementToolbarWidget,
|
||||
EdgelessRootBlockSpec,
|
||||
} from '@blocksuite/affine/blocks/root';
|
||||
import { EdgelessRootBlockSpec } from '@blocksuite/affine/blocks/root';
|
||||
import { ToolbarModuleExtension } from '@blocksuite/affine/shared/services';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
@@ -15,7 +12,6 @@ import { toolbarAIEntryConfig } from '../entries';
|
||||
import {
|
||||
edgelessToolbarAIEntryConfig,
|
||||
setupEdgelessCopilot,
|
||||
setupEdgelessElementToolbarAIEntry,
|
||||
} from '../entries/edgeless/index';
|
||||
import { setupSpaceAIEntry } from '../entries/space/setup-space';
|
||||
import { CopilotTool } from '../tool/copilot-tool';
|
||||
@@ -72,10 +68,6 @@ function getAIEdgelessRootWatcher(framework: FrameworkProvider) {
|
||||
if (component instanceof EdgelessCopilotWidget) {
|
||||
setupEdgelessCopilot(component);
|
||||
}
|
||||
|
||||
if (component instanceof EdgelessElementToolbarWidget) {
|
||||
setupEdgelessElementToolbarAIEntry(component);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ import { getSelectedModelsCommand } from '@blocksuite/affine/shared/commands';
|
||||
import { ImageSelection } from '@blocksuite/affine/shared/selection';
|
||||
import {
|
||||
ActionPlacement,
|
||||
FeatureFlagService,
|
||||
GenerateDocUrlProvider,
|
||||
isRemovedUserInfo,
|
||||
OpenDocExtensionIdentifier,
|
||||
@@ -316,8 +315,7 @@ function createToolbarMoreMenuConfigV2(baseUrl?: string) {
|
||||
{
|
||||
id: 'block-meta-display',
|
||||
when: ctx => {
|
||||
const featureFlag = ctx.std.get(FeatureFlagService);
|
||||
const isEnabled = featureFlag.getFlag('enable_block_meta');
|
||||
const isEnabled = ctx.features.getFlag('enable_block_meta');
|
||||
if (!isEnabled) return false;
|
||||
|
||||
// only display when one block is selected by block selection
|
||||
|
||||
@@ -171,11 +171,10 @@ test('paste surface-ref block to another doc as embed-linked-doc block', async (
|
||||
const frameTitle = page.locator('affine-frame-title');
|
||||
await frameTitle.click();
|
||||
await page.waitForTimeout(50);
|
||||
const changeFrameButton = page.locator('edgeless-change-frame-button');
|
||||
// get insert into page button which with aria-label 'Insert into Page'
|
||||
const insertIntoPageButton = changeFrameButton.locator(
|
||||
`editor-icon-button[aria-label="Insert into Page"]`
|
||||
);
|
||||
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
|
||||
const insertIntoPageButton = toolbar.getByLabel('Insert into Page');
|
||||
await insertIntoPageButton.click();
|
||||
|
||||
await clickPageModeButton(page);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
clickEdgelessModeButton,
|
||||
locateEditorContainer,
|
||||
locateElementToolbar,
|
||||
locateToolbar,
|
||||
} from '@affine-test/kit/utils/editor';
|
||||
import { pressEnter } from '@affine-test/kit/utils/keyboard';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
@@ -31,7 +31,7 @@ test('should close embed editing modal when editor switching to page mode by sho
|
||||
.getByTestId('cmdk-label')
|
||||
.getByText('Write, Draw, Plan all at Once.')
|
||||
.click();
|
||||
const toolbar = locateElementToolbar(page);
|
||||
const toolbar = locateToolbar(page);
|
||||
await toolbar.getByLabel('Edit').click();
|
||||
|
||||
const editingModal = page.locator('embed-card-edit-modal');
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
createEdgelessNoteBlock,
|
||||
dragView,
|
||||
locateEditorContainer,
|
||||
locateElementToolbar,
|
||||
locateToolbar,
|
||||
toViewCoord,
|
||||
} from '@affine-test/kit/utils/editor';
|
||||
import {
|
||||
@@ -34,7 +34,7 @@ test.beforeEach(async ({ page }) => {
|
||||
test('should update zindex of element when moving it into frame', async ({
|
||||
page,
|
||||
}) => {
|
||||
const toolbar = locateElementToolbar(page);
|
||||
const toolbar = locateToolbar(page);
|
||||
|
||||
// create a top frame
|
||||
await page.keyboard.press('f');
|
||||
@@ -47,7 +47,7 @@ test('should update zindex of element when moving it into frame', async ({
|
||||
await createEdgelessNoteBlock(page, [500, 500]);
|
||||
await clickView(page, [0, 100]);
|
||||
await clickView(page, [500, 500]);
|
||||
await toolbar.getByLabel('More').click();
|
||||
await toolbar.getByLabel('more-menu').click();
|
||||
await toolbar.getByLabel('Send to Back').click();
|
||||
await pressEscape(page);
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
getPageMode,
|
||||
getSelectedXYWH,
|
||||
locateEditorContainer,
|
||||
locateElementToolbar,
|
||||
locateModeSwitchButton,
|
||||
locateToolbar,
|
||||
moveToView,
|
||||
resizeElementByHandle,
|
||||
toViewCoord,
|
||||
@@ -201,8 +201,8 @@ test.describe('edgeless note element toolbar', () => {
|
||||
page,
|
||||
}) => {
|
||||
await selectAllByKeyboard(page);
|
||||
const toolbar = locateElementToolbar(page);
|
||||
const autoHeight = toolbar.getByTestId('edgeless-note-auto-height');
|
||||
const toolbar = locateToolbar(page);
|
||||
const autoHeight = toolbar.getByTestId('auto-height');
|
||||
const displayInPage = toolbar.getByTestId('display-in-page');
|
||||
|
||||
await expect(toolbar).toBeVisible();
|
||||
@@ -218,8 +218,8 @@ test.describe('edgeless note element toolbar', () => {
|
||||
await clickView(page, [0, 0]);
|
||||
await clickView(page, [100, 100]);
|
||||
|
||||
const toolbar = locateElementToolbar(page);
|
||||
const autoHeight = toolbar.getByTestId('edgeless-note-auto-height');
|
||||
const toolbar = locateToolbar(page);
|
||||
const autoHeight = toolbar.getByTestId('auto-height');
|
||||
const displayInPage = toolbar.getByTestId('display-in-page');
|
||||
|
||||
await expect(toolbar).toBeVisible();
|
||||
@@ -237,7 +237,7 @@ test.describe('edgeless note element toolbar', () => {
|
||||
await clickView(page, [0, 0]);
|
||||
await clickView(page, [100, 100]);
|
||||
|
||||
const toolbar = locateElementToolbar(page);
|
||||
const toolbar = locateToolbar(page);
|
||||
const displayInPage = toolbar.getByTestId('display-in-page');
|
||||
|
||||
await displayInPage.click();
|
||||
@@ -300,7 +300,7 @@ test.describe('edgeless note element toolbar', () => {
|
||||
}, noteId);
|
||||
};
|
||||
|
||||
const toolbar = locateElementToolbar(page);
|
||||
const toolbar = locateToolbar(page);
|
||||
|
||||
await selectAllByKeyboard(page);
|
||||
const noteId = (await getEdgelessSelectedIds(page))[0];
|
||||
@@ -329,7 +329,11 @@ test.describe('edgeless note element toolbar', () => {
|
||||
await toolbar.getByRole('button', { name: 'Border style' }).click();
|
||||
await toolbar.locator('.mode-solid').click();
|
||||
await toolbar.getByRole('button', { name: 'Border style' }).click();
|
||||
await toolbar.locator('edgeless-line-width-panel').getByLabel('8').click();
|
||||
// TODO(@fundon): delete duplicate components
|
||||
await toolbar
|
||||
.locator('affine-edgeless-line-width-panel')
|
||||
.getByLabel('8')
|
||||
.click();
|
||||
|
||||
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
|
||||
style: {
|
||||
@@ -341,7 +345,11 @@ test.describe('edgeless note element toolbar', () => {
|
||||
});
|
||||
|
||||
await toolbar.getByRole('button', { name: 'Corners' }).click();
|
||||
await toolbar.locator('edgeless-size-panel').getByText('Large').click();
|
||||
// TODO(@fundon): delete duplicate components
|
||||
await toolbar
|
||||
.locator('affine-size-dropdown-menu')
|
||||
.getByText('Large')
|
||||
.click();
|
||||
|
||||
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
|
||||
style: {
|
||||
|
||||
@@ -35,9 +35,7 @@ test('should add text to shape, default to pure black', async ({ page }) => {
|
||||
await page.keyboard.type('text');
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
const toolbar = page.locator(
|
||||
'edgeless-element-toolbar-widget editor-toolbar'
|
||||
);
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
const textColorContainer = toolbar.locator(
|
||||
'edgeless-color-picker-button.text-color'
|
||||
);
|
||||
@@ -76,9 +74,7 @@ test('should add text to shape with pure white', async ({ page }) => {
|
||||
await page.keyboard.type('text');
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
const toolbar = page.locator(
|
||||
'edgeless-element-toolbar-widget editor-toolbar'
|
||||
);
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
const textColorContainer = toolbar.locator(
|
||||
'edgeless-color-picker-button.text-color'
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
focusDocTitle,
|
||||
getEdgelessSelectedIds,
|
||||
getViewportCenter,
|
||||
locateElementToolbar,
|
||||
locateToolbar,
|
||||
setViewportCenter,
|
||||
} from '@affine-test/kit/utils/editor';
|
||||
import {
|
||||
@@ -508,12 +508,10 @@ test.describe('advanced visibility control', () => {
|
||||
await expect(edgelessCard).toHaveCount(1);
|
||||
|
||||
await clickView(page, [100, 100]);
|
||||
const noteButtons = locateElementToolbar(page).locator(
|
||||
'edgeless-change-note-button'
|
||||
);
|
||||
const toolbar = locateToolbar(page);
|
||||
|
||||
await noteButtons.getByRole('button', { name: 'Mode' }).click();
|
||||
await noteButtons.locator('note-display-mode-panel .item.both').click();
|
||||
await toolbar.getByRole('button', { name: 'Mode' }).click();
|
||||
await toolbar.locator('note-display-mode-panel .item.both').click();
|
||||
|
||||
await expect(bothCard).toHaveCount(2);
|
||||
await expect(edgelessCard).toHaveCount(0);
|
||||
@@ -541,10 +539,8 @@ test.describe('advanced visibility control', () => {
|
||||
await expect(bothCard).toHaveCount(2);
|
||||
|
||||
await clickView(page, [200, 100]);
|
||||
const changeNoteButtons = locateElementToolbar(page).locator(
|
||||
'edgeless-change-note-button'
|
||||
);
|
||||
await changeNoteButtons.getByRole('button', { name: 'Slicer' }).click();
|
||||
const toolbar = locateToolbar(page);
|
||||
await toolbar.getByRole('button', { name: 'Slicer' }).click();
|
||||
await expect(page.locator('.note-slicer-button')).toBeVisible();
|
||||
await page.locator('.note-slicer-button').click();
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ test('not allowed to switch to embed view when linking to block', async ({
|
||||
|
||||
await cardLink.click();
|
||||
|
||||
await toolbar.getByLabel('More').click();
|
||||
await toolbar.getByLabel('more-menu').click();
|
||||
await toolbar.getByLabel('Copy link to block').click();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
@@ -118,10 +118,10 @@ test('can open peek view for embedded frames', async ({ page }) => {
|
||||
// close affine-banner
|
||||
await page.locator('[data-testid=local-demo-tips-close-button]').click();
|
||||
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
|
||||
// insert the frame to page
|
||||
await page
|
||||
.locator('edgeless-change-frame-button:has-text("Insert into Page")')
|
||||
.click();
|
||||
await toolbar.getByLabel('Insert into Page').click();
|
||||
|
||||
// switch back to page mode
|
||||
await clickPageModeButton(page);
|
||||
|
||||
@@ -134,10 +134,12 @@ test.describe('auto-complete', () => {
|
||||
await edgelessCommonSetup(page);
|
||||
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
|
||||
await assertSelectedBound(page, [0, 0, 100, 100]);
|
||||
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor');
|
||||
await triggerComponentToolbarAction(page, 'changeShapeColor');
|
||||
await changeShapeStrokeColor(page, 'MediumRed');
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
await changeShapeFillColor(page, 'HeavyGreen');
|
||||
// Closes color pickers
|
||||
await triggerComponentToolbarAction(page, 'changeShapeColor');
|
||||
|
||||
await dragBetweenViewCoords(page, [120, 50], [200, 0]);
|
||||
|
||||
const noteButton = getAutoCompletePanelButton(page, 'note');
|
||||
|
||||
@@ -20,8 +20,8 @@ async function setupWithColorPickerFunction(page: Page) {
|
||||
await switchEditorMode(page);
|
||||
}
|
||||
|
||||
function getColorPickerButtonWithClass(page: Page, classes: string) {
|
||||
return page.locator(`edgeless-color-picker-button.${classes}`);
|
||||
function getColorPanelWithLabel(page: Page, label: string) {
|
||||
return page.locator(`edgeless-color-panel[aria-label="${label}"]`);
|
||||
}
|
||||
|
||||
function getCurrentColorUnitButton(locator: Locator) {
|
||||
@@ -71,12 +71,12 @@ test.describe('basic functions', () => {
|
||||
const end0 = { x: 150, y: 200 };
|
||||
await addBasicShapeElement(page, start0, end0, Shape.Square);
|
||||
|
||||
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
|
||||
await expect(fillColorButton).toBeVisible();
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
|
||||
const customButton = getCustomButton(fillColorButton);
|
||||
const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
|
||||
await expect(fillColorPanel).toBeVisible();
|
||||
|
||||
const customButton = getCustomButton(fillColorPanel);
|
||||
await expect(customButton).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -91,12 +91,13 @@ test.describe('basic functions', () => {
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
|
||||
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
|
||||
const customButton = getCustomButton(fillColorButton);
|
||||
const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
|
||||
const customButton = getCustomButton(fillColorPanel);
|
||||
|
||||
await customButton.click();
|
||||
|
||||
const colorPickerPanel = getColorPickerPanel(fillColorButton);
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
const colorPickerPanel = getColorPickerPanel(toolbar);
|
||||
|
||||
await expect(colorPickerPanel).toBeVisible();
|
||||
});
|
||||
@@ -112,17 +113,18 @@ test.describe('basic functions', () => {
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
|
||||
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
|
||||
const currentColorUnit = getCurrentColorUnitButton(fillColorButton);
|
||||
const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
|
||||
const currentColorUnit = getCurrentColorUnitButton(fillColorPanel);
|
||||
|
||||
const value = await getCurrentColor(currentColorUnit);
|
||||
await expect(currentColorUnit.locator('svg')).toHaveCSS('fill', value);
|
||||
|
||||
const customButton = getCustomButton(fillColorButton);
|
||||
const customButton = getCustomButton(fillColorPanel);
|
||||
|
||||
await customButton.click();
|
||||
|
||||
const colorPickerPanel = getColorPickerPanel(fillColorButton);
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
const colorPickerPanel = getColorPickerPanel(toolbar);
|
||||
|
||||
await expect(colorPickerPanel).toBeVisible();
|
||||
|
||||
@@ -143,14 +145,16 @@ test.describe('basic functions', () => {
|
||||
const end0 = { x: 150, y: 200 };
|
||||
await addBasicShapeElement(page, start0, end0, Shape.Square);
|
||||
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
|
||||
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
|
||||
const customButton = getCustomButton(fillColorButton);
|
||||
const colorPickerPanel = getColorPickerPanel(fillColorButton);
|
||||
const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
|
||||
const customButton = getCustomButton(fillColorPanel);
|
||||
|
||||
await customButton.click();
|
||||
|
||||
const colorPickerPanel = getColorPickerPanel(toolbar);
|
||||
await expect(colorPickerPanel).toBeVisible();
|
||||
|
||||
await page.mouse.click(0, 0);
|
||||
@@ -159,7 +163,7 @@ test.describe('basic functions', () => {
|
||||
|
||||
await dragBetweenCoords(page, { x: 125, y: 75 }, { x: 175, y: 225 });
|
||||
|
||||
await fillColorButton.click();
|
||||
await toolbar.getByLabel(/^Color$/).click();
|
||||
|
||||
await expect(customButton).toBeVisible();
|
||||
await expect(colorPickerPanel).toBeHidden();
|
||||
@@ -174,14 +178,16 @@ test.describe('basic functions', () => {
|
||||
const end0 = { x: 150, y: 200 };
|
||||
await addBasicShapeElement(page, start0, end0, Shape.Square);
|
||||
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
|
||||
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
|
||||
const customButton = getCustomButton(fillColorButton);
|
||||
const colorPickerPanel = getColorPickerPanel(fillColorButton);
|
||||
const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
|
||||
const customButton = getCustomButton(fillColorPanel);
|
||||
|
||||
await customButton.click();
|
||||
|
||||
const colorPickerPanel = getColorPickerPanel(toolbar);
|
||||
const paletteControl = getPaletteControl(colorPickerPanel);
|
||||
const hexInput = getHexInput(colorPickerPanel);
|
||||
|
||||
@@ -203,14 +209,16 @@ test.describe('basic functions', () => {
|
||||
const end0 = { x: 150, y: 200 };
|
||||
await addBasicShapeElement(page, start0, end0, Shape.Square);
|
||||
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
|
||||
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
|
||||
const customButton = getCustomButton(fillColorButton);
|
||||
const colorPickerPanel = getColorPickerPanel(fillColorButton);
|
||||
const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
|
||||
const customButton = getCustomButton(fillColorPanel);
|
||||
|
||||
await customButton.click();
|
||||
|
||||
const colorPickerPanel = getColorPickerPanel(toolbar);
|
||||
const hueControl = getHueControl(colorPickerPanel);
|
||||
const hexInput = getHexInput(colorPickerPanel);
|
||||
|
||||
@@ -230,14 +238,16 @@ test.describe('basic functions', () => {
|
||||
const end0 = { x: 150, y: 200 };
|
||||
await addBasicShapeElement(page, start0, end0, Shape.Square);
|
||||
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
|
||||
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
|
||||
const customButton = getCustomButton(fillColorButton);
|
||||
const colorPickerPanel = getColorPickerPanel(fillColorButton);
|
||||
const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
|
||||
const customButton = getCustomButton(fillColorPanel);
|
||||
|
||||
await customButton.click();
|
||||
|
||||
const colorPickerPanel = getColorPickerPanel(toolbar);
|
||||
const hexInput = getHexInput(colorPickerPanel);
|
||||
|
||||
await hexInput.fill('fff');
|
||||
@@ -266,14 +276,16 @@ test.describe('basic functions', () => {
|
||||
const end0 = { x: 150, y: 200 };
|
||||
await addBasicShapeElement(page, start0, end0, Shape.Square);
|
||||
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
|
||||
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
|
||||
const customButton = getCustomButton(fillColorButton);
|
||||
const colorPickerPanel = getColorPickerPanel(fillColorButton);
|
||||
const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
|
||||
const customButton = getCustomButton(fillColorPanel);
|
||||
|
||||
await customButton.click();
|
||||
|
||||
const colorPickerPanel = getColorPickerPanel(toolbar);
|
||||
const alphaControl = getAlphaControl(colorPickerPanel);
|
||||
const alphaInput = getAlphaInput(colorPickerPanel);
|
||||
|
||||
@@ -295,14 +307,16 @@ test.describe('basic functions', () => {
|
||||
const end0 = { x: 150, y: 200 };
|
||||
await addBasicShapeElement(page, start0, end0, Shape.Square);
|
||||
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
|
||||
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
|
||||
const customButton = getCustomButton(fillColorButton);
|
||||
const colorPickerPanel = getColorPickerPanel(fillColorButton);
|
||||
const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
|
||||
const customButton = getCustomButton(fillColorPanel);
|
||||
|
||||
await customButton.click();
|
||||
|
||||
const colorPickerPanel = getColorPickerPanel(toolbar);
|
||||
const alphaInput = getAlphaInput(colorPickerPanel);
|
||||
|
||||
await alphaInput.fill('101');
|
||||
@@ -336,8 +350,9 @@ test.describe('basic functions', () => {
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
|
||||
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
|
||||
const currentColorUnit = getCurrentColorUnitButton(fillColorButton);
|
||||
const fillColorPanel = getColorPanelWithLabel(page, 'Fill color');
|
||||
|
||||
const currentColorUnit = getCurrentColorUnitButton(fillColorPanel);
|
||||
|
||||
const value = await getCurrentColor(currentColorUnit);
|
||||
let rgba = parseStringToRgba(value);
|
||||
|
||||
@@ -146,7 +146,7 @@ test('change connector line width', async ({ page }) => {
|
||||
await addBasicConnectorElement(page, start, end);
|
||||
|
||||
await page.mouse.click(start.x + 5, start.y);
|
||||
await triggerComponentToolbarAction(page, 'changeConnectorStrokeColor');
|
||||
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
||||
await changeConnectorStrokeColor(page, 'MediumGrey');
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
||||
@@ -171,7 +171,7 @@ test('change connector stroke style', async ({ page }) => {
|
||||
await addBasicConnectorElement(page, start, end);
|
||||
|
||||
await page.mouse.click(start.x + 5, start.y);
|
||||
await triggerComponentToolbarAction(page, 'changeConnectorStrokeColor');
|
||||
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
||||
await changeConnectorStrokeColor(page, 'MediumGrey');
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
||||
|
||||
@@ -75,14 +75,15 @@ test('should be hidden when resizing element', async ({ page }) => {
|
||||
const toolbar = locatorComponentToolbar(page);
|
||||
await expect(toolbar).toBeVisible();
|
||||
|
||||
await resizeElementByHandle(page, { x: 400, y: 300 }, 'top-left', 30);
|
||||
await resizeElementByHandle(
|
||||
page,
|
||||
{ x: 400, y: 300 },
|
||||
'top-left',
|
||||
30,
|
||||
async () => {
|
||||
await expect(toolbar).toBeHidden();
|
||||
}
|
||||
);
|
||||
|
||||
await page.mouse.move(450, 300);
|
||||
await expect(toolbar).toBeEmpty();
|
||||
|
||||
await page.mouse.move(320, 220);
|
||||
await expect(toolbar).toBeEmpty();
|
||||
|
||||
await page.mouse.up();
|
||||
await expect(toolbar).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -107,8 +107,13 @@ test.describe('frame copy and paste', () => {
|
||||
const frameTitles = page.locator('affine-frame-title');
|
||||
|
||||
await frameTitles.nth(0).click();
|
||||
await page.locator('edgeless-more-button').click();
|
||||
await page.locator('editor-menu-action', { hasText: 'Duplicate' }).click();
|
||||
|
||||
const moreMenu = page.getByLabel('more-menu');
|
||||
|
||||
await moreMenu.click();
|
||||
await moreMenu
|
||||
.locator('editor-menu-action', { hasText: 'Duplicate' })
|
||||
.click();
|
||||
await pressEscape(page);
|
||||
|
||||
await frameTitles.nth(0).click();
|
||||
|
||||
@@ -388,8 +388,8 @@ test('delete frame by click ungroup should not delete its children', async ({
|
||||
|
||||
const frameTitle = page.locator('affine-frame-title');
|
||||
await frameTitle.click();
|
||||
const elementToolbar = page.locator('edgeless-element-toolbar-widget');
|
||||
const ungroupButton = elementToolbar.getByLabel('Ungroup');
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
const ungroupButton = toolbar.getByLabel('Ungroup');
|
||||
await ungroupButton.click();
|
||||
|
||||
await assertCanvasElementsCount(page, 1);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
dragBetweenViewCoords,
|
||||
edgelessCommonSetup,
|
||||
getFirstContainerId,
|
||||
locatorComponentToolbar,
|
||||
Shape,
|
||||
shiftClickView,
|
||||
triggerComponentToolbarAction,
|
||||
@@ -45,15 +46,16 @@ test.describe('group', () => {
|
||||
page,
|
||||
}) => {
|
||||
await clickView(page, [50, 50]);
|
||||
await expect(
|
||||
page.locator('edgeless-element-toolbar-widget')
|
||||
).toBeVisible();
|
||||
await expect(page.locator('edgeless-add-group-button')).not.toBeVisible();
|
||||
const toolbar = locatorComponentToolbar(page);
|
||||
await expect(toolbar).toBeVisible();
|
||||
await expect(toolbar.getByLabel(/^Group$/)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('create button show up when multi select', async ({ page }) => {
|
||||
await selectAllByKeyboard(page);
|
||||
await expect(page.locator('edgeless-add-group-button')).toBeVisible();
|
||||
const toolbar = locatorComponentToolbar(page);
|
||||
await expect(toolbar).toBeVisible();
|
||||
await expect(toolbar.getByLabel(/^Group$/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('create group by component toolbar', async ({ page }) => {
|
||||
|
||||
@@ -42,12 +42,10 @@ import { test } from '../utils/playwright.js';
|
||||
|
||||
test.describe('lock', () => {
|
||||
const getButtons = (page: Page) => {
|
||||
const elementToolbar = page.locator('edgeless-element-toolbar-widget');
|
||||
const toolbar = page.locator('affine-toolbar-widget');
|
||||
return {
|
||||
lock: elementToolbar.locator('edgeless-lock-button[data-locked="false"]'),
|
||||
unlock: elementToolbar.locator(
|
||||
'edgeless-lock-button[data-locked="true"]'
|
||||
),
|
||||
lock: toolbar.getByTestId('lock'),
|
||||
unlock: toolbar.getByTestId('unlock'),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ test('duplicate note should work correctly', async ({ page }) => {
|
||||
|
||||
await triggerComponentToolbarAction(page, 'duplicate');
|
||||
await waitNextFrame(page, 200); // wait viewport fit animation
|
||||
const moreActionsContainer = page.locator('.more-actions-container');
|
||||
const moreActionsContainer = page.getByLabel('more-menu').getByRole('menu');
|
||||
await expect(moreActionsContainer).toBeHidden();
|
||||
|
||||
const noteLocator = page.locator('affine-edgeless-note');
|
||||
|
||||
@@ -35,7 +35,7 @@ async function openScalePanel(page: Page, noteId: string) {
|
||||
await selectNoteInEdgeless(page, noteId);
|
||||
await triggerComponentToolbarAction(page, 'changeNoteScale');
|
||||
await waitNextFrame(page);
|
||||
const scalePanel = page.locator('edgeless-scale-panel');
|
||||
const scalePanel = page.locator('.scale-menu');
|
||||
await expect(scalePanel).toBeVisible();
|
||||
return scalePanel;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ test.describe('note scale', () => {
|
||||
const noteId = await setupAndAddNote(page);
|
||||
const scalePanel = await openScalePanel(page, noteId);
|
||||
|
||||
const scaleInput = scalePanel.locator('.scale-input');
|
||||
const scaleInput = scalePanel.locator('input');
|
||||
await scaleInput.click();
|
||||
await page.keyboard.type('50');
|
||||
await page.keyboard.press('Enter');
|
||||
@@ -102,7 +102,7 @@ test.describe('note scale', () => {
|
||||
const noteId = await setupAndAddNote(page);
|
||||
const scalePanel = await openScalePanel(page, noteId);
|
||||
|
||||
const scaleInput = scalePanel.locator('.scale-input');
|
||||
const scaleInput = scalePanel.locator('input');
|
||||
await scaleInput.click();
|
||||
await page.keyboard.type('50');
|
||||
await selectAllByKeyboard(page);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
changeShapeStrokeColor,
|
||||
changeShapeStrokeStyle,
|
||||
changeShapeStrokeWidth,
|
||||
changeShapeStyle,
|
||||
clickComponentToolbarMoreMenuButton,
|
||||
getEdgelessSelectedRect,
|
||||
locatorComponentToolbar,
|
||||
@@ -347,17 +346,16 @@ test('change shape stroke width', async ({ page }) => {
|
||||
await addBasicRectShapeElement(page, start, end);
|
||||
|
||||
await page.mouse.click(start.x + 5, start.y + 5);
|
||||
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor');
|
||||
await triggerComponentToolbarAction(page, 'changeShapeColor');
|
||||
await changeShapeStrokeColor(page, 'MediumMagenta');
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
|
||||
await changeShapeStrokeWidth(page);
|
||||
await page.mouse.click(start.x + 5, start.y + 5);
|
||||
await assertEdgelessSelectedRect(page, [100, 150, 100, 100]);
|
||||
|
||||
await waitNextFrame(page);
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
|
||||
await triggerComponentToolbarAction(page, 'changeShapeColor');
|
||||
});
|
||||
|
||||
test('change shape stroke style', async ({ page }) => {
|
||||
@@ -370,14 +368,12 @@ test('change shape stroke style', async ({ page }) => {
|
||||
await addBasicRectShapeElement(page, start, end);
|
||||
|
||||
await page.mouse.click(start.x + 5, start.y + 5);
|
||||
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor');
|
||||
await triggerComponentToolbarAction(page, 'changeShapeColor');
|
||||
await changeShapeStrokeColor(page, 'MediumBlue');
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
|
||||
await changeShapeStrokeStyle(page, 'dash');
|
||||
await waitNextFrame(page);
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
|
||||
const activeButton = locatorShapeStrokeStyleButton(page, 'dash');
|
||||
const className = await activeButton.evaluate(ele => ele.className);
|
||||
expect(className.includes(' active')).toBeTruthy();
|
||||
@@ -552,12 +548,12 @@ test('change shape style', async ({ page }) => {
|
||||
await addBasicRectShapeElement(page, start, end);
|
||||
|
||||
await page.mouse.click(start.x + 5, start.y + 5);
|
||||
await triggerComponentToolbarAction(page, 'changeShapeStyle');
|
||||
await changeShapeStyle(page, 'general');
|
||||
await triggerComponentToolbarAction(page, 'changeShapeColor');
|
||||
// The style switching feature has been removed.
|
||||
//await changeShapeStyle(page, 'general');
|
||||
await waitNextFrame(page);
|
||||
|
||||
await page.mouse.click(start.x + 5, start.y + 5);
|
||||
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor');
|
||||
const color = 'LightPurple';
|
||||
await changeShapeStrokeColor(page, color);
|
||||
await page.waitForTimeout(50);
|
||||
@@ -638,8 +634,11 @@ test.describe('shape hit test', () => {
|
||||
await addBasicRectShapeElement(page, rect.start, rect.end);
|
||||
|
||||
await page.mouse.click(rect.start.x + 5, rect.start.y + 5);
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
// opens color picker
|
||||
await triggerComponentToolbarAction(page, 'changeShapeColor');
|
||||
await changeShapeFillColorToTransparent(page);
|
||||
// closes color picker
|
||||
await triggerComponentToolbarAction(page, 'changeShapeColor');
|
||||
await page.waitForTimeout(50);
|
||||
}
|
||||
|
||||
@@ -715,15 +714,15 @@ test.describe('shape hit test', () => {
|
||||
await pressEscape(page);
|
||||
await waitNextFrame(page);
|
||||
|
||||
const textAlignBtn = locatorComponentToolbar(page).getByRole('button', {
|
||||
const alignmentMenu =
|
||||
locatorComponentToolbar(page).getByLabel('alignment-menu');
|
||||
|
||||
const textAlignBtn = alignmentMenu.getByRole('button', {
|
||||
name: 'Alignment',
|
||||
});
|
||||
await textAlignBtn.click();
|
||||
|
||||
await page
|
||||
.locator('edgeless-align-panel')
|
||||
.getByRole('button', { name: 'Left' })
|
||||
.click();
|
||||
await alignmentMenu.getByRole('button', { name: 'Left' }).click();
|
||||
|
||||
// creates an edgeless-text
|
||||
await page.mouse.dblclick(rect.start.x + 80, rect.start.y + 20);
|
||||
|
||||
@@ -284,7 +284,7 @@ export function locatorEdgelessComponentToolButton(
|
||||
more: 'More',
|
||||
}[type];
|
||||
const button = page
|
||||
.locator('edgeless-element-toolbar-widget editor-icon-button')
|
||||
.locator('affine-toolbar-widget editor-toolbar editor-icon-button')
|
||||
.filter({
|
||||
hasText: text,
|
||||
});
|
||||
@@ -592,7 +592,8 @@ export async function resizeElementByHandle(
|
||||
| 'top-right'
|
||||
| 'bottom-right'
|
||||
| 'bottom-left' = 'top-left',
|
||||
steps = 1
|
||||
steps = 1,
|
||||
beforeMouseUp?: () => Promise<void>
|
||||
) {
|
||||
const handle = page.locator(`.handle[aria-label="${corner}"] .resize`);
|
||||
const box = await handle.boundingBox();
|
||||
@@ -604,6 +605,7 @@ export async function resizeElementByHandle(
|
||||
{ x: box.x + delta.x + offset, y: box.y + delta.y + offset },
|
||||
{
|
||||
steps,
|
||||
beforeMouseUp,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -757,14 +759,11 @@ export function locatorNoteDisplayModeButton(
|
||||
page: Page,
|
||||
mode: NoteDisplayMode
|
||||
) {
|
||||
return page
|
||||
.locator('edgeless-change-note-button')
|
||||
.locator('note-display-mode-panel')
|
||||
.locator(`.item.${mode}`);
|
||||
return page.locator('note-display-mode-panel').locator(`.item.${mode}`);
|
||||
}
|
||||
|
||||
export function locatorScalePanelButton(page: Page, scale: number) {
|
||||
return page.locator('edgeless-scale-panel').locator(`.scale-${scale}`);
|
||||
return page.locator('affine-size-dropdown-menu').getByLabel(String(scale));
|
||||
}
|
||||
|
||||
export async function changeNoteDisplayMode(page: Page, mode: NoteDisplayMode) {
|
||||
@@ -796,9 +795,9 @@ export async function updateExistedBrushElementSize(
|
||||
}
|
||||
|
||||
export async function openComponentToolbarMoreMenu(page: Page) {
|
||||
const btn = page.locator(
|
||||
'edgeless-element-toolbar-widget edgeless-more-button editor-menu-button'
|
||||
);
|
||||
const btn = page
|
||||
.locator('affine-toolbar-widget editor-toolbar')
|
||||
.getByLabel('more-menu');
|
||||
|
||||
await btn.click();
|
||||
}
|
||||
@@ -1020,13 +1019,11 @@ export async function deleteAllConnectors(page: Page) {
|
||||
}
|
||||
|
||||
export function locatorComponentToolbar(page: Page) {
|
||||
return page.locator('edgeless-element-toolbar-widget');
|
||||
return page.locator('affine-toolbar-widget editor-toolbar');
|
||||
}
|
||||
|
||||
export function locatorComponentToolbarMoreButton(page: Page) {
|
||||
const moreButton = locatorComponentToolbar(page).locator(
|
||||
'edgeless-more-button'
|
||||
);
|
||||
const moreButton = locatorComponentToolbar(page).getByLabel('more-menu');
|
||||
return moreButton;
|
||||
}
|
||||
type Action =
|
||||
@@ -1037,10 +1034,10 @@ type Action =
|
||||
| 'copyAsPng'
|
||||
| 'changeNoteColor'
|
||||
| 'changeShapeStyle'
|
||||
| 'changeShapeColor'
|
||||
| 'changeShapeFillColor'
|
||||
| 'changeShapeStrokeColor'
|
||||
| 'changeShapeStrokeStyles'
|
||||
| 'changeConnectorStrokeColor'
|
||||
| 'changeConnectorStrokeStyles'
|
||||
| 'changeConnectorShape'
|
||||
| 'addFrame'
|
||||
@@ -1075,11 +1072,9 @@ export async function triggerComponentToolbarAction(
|
||||
const moreButton = locatorComponentToolbarMoreButton(page);
|
||||
await moreButton.click();
|
||||
|
||||
const actionButton = moreButton
|
||||
.locator('.more-actions-container editor-menu-action')
|
||||
.filter({
|
||||
hasText: 'Bring to Front',
|
||||
});
|
||||
const actionButton = moreButton.locator('editor-menu-action').filter({
|
||||
hasText: 'Bring to Front',
|
||||
});
|
||||
await actionButton.click();
|
||||
break;
|
||||
}
|
||||
@@ -1087,11 +1082,9 @@ export async function triggerComponentToolbarAction(
|
||||
const moreButton = locatorComponentToolbarMoreButton(page);
|
||||
await moreButton.click();
|
||||
|
||||
const actionButton = moreButton
|
||||
.locator('.more-actions-container editor-menu-action')
|
||||
.filter({
|
||||
hasText: 'Bring Forward',
|
||||
});
|
||||
const actionButton = moreButton.locator('editor-menu-action').filter({
|
||||
hasText: 'Bring Forward',
|
||||
});
|
||||
await actionButton.click();
|
||||
break;
|
||||
}
|
||||
@@ -1099,11 +1092,9 @@ export async function triggerComponentToolbarAction(
|
||||
const moreButton = locatorComponentToolbarMoreButton(page);
|
||||
await moreButton.click();
|
||||
|
||||
const actionButton = moreButton
|
||||
.locator('.more-actions-container editor-menu-action')
|
||||
.filter({
|
||||
hasText: 'Send Backward',
|
||||
});
|
||||
const actionButton = moreButton.locator('editor-menu-action').filter({
|
||||
hasText: 'Send Backward',
|
||||
});
|
||||
await actionButton.click();
|
||||
break;
|
||||
}
|
||||
@@ -1111,11 +1102,9 @@ export async function triggerComponentToolbarAction(
|
||||
const moreButton = locatorComponentToolbarMoreButton(page);
|
||||
await moreButton.click();
|
||||
|
||||
const actionButton = moreButton
|
||||
.locator('.more-actions-container editor-menu-action')
|
||||
.filter({
|
||||
hasText: 'Send to Back',
|
||||
});
|
||||
const actionButton = moreButton.locator('editor-menu-action').filter({
|
||||
hasText: 'Send to Back',
|
||||
});
|
||||
await actionButton.click();
|
||||
break;
|
||||
}
|
||||
@@ -1123,11 +1112,9 @@ export async function triggerComponentToolbarAction(
|
||||
const moreButton = locatorComponentToolbarMoreButton(page);
|
||||
await moreButton.click();
|
||||
|
||||
const actionButton = moreButton
|
||||
.locator('.more-actions-container editor-menu-action')
|
||||
.filter({
|
||||
hasText: 'Copy as PNG',
|
||||
});
|
||||
const actionButton = moreButton.locator('editor-menu-action').filter({
|
||||
hasText: 'Copy as PNG',
|
||||
});
|
||||
await actionButton.click();
|
||||
break;
|
||||
}
|
||||
@@ -1135,11 +1122,9 @@ export async function triggerComponentToolbarAction(
|
||||
const moreButton = locatorComponentToolbarMoreButton(page);
|
||||
await moreButton.click();
|
||||
|
||||
const actionButton = moreButton
|
||||
.locator('.more-actions-container editor-menu-action')
|
||||
.filter({
|
||||
hasText: 'Frame Section',
|
||||
});
|
||||
const actionButton = moreButton.locator('editor-menu-action').filter({
|
||||
hasText: 'Frame Section',
|
||||
});
|
||||
await actionButton.click();
|
||||
break;
|
||||
}
|
||||
@@ -1147,68 +1132,48 @@ export async function triggerComponentToolbarAction(
|
||||
const moreButton = locatorComponentToolbarMoreButton(page);
|
||||
await moreButton.click();
|
||||
|
||||
const actionButton = moreButton
|
||||
.locator('.more-actions-container editor-menu-action')
|
||||
.filter({
|
||||
hasText: 'Duplicate',
|
||||
});
|
||||
const actionButton = moreButton.locator('editor-menu-action').filter({
|
||||
hasText: 'Duplicate',
|
||||
});
|
||||
await actionButton.click();
|
||||
break;
|
||||
}
|
||||
case 'changeShapeFillColor': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-shape-button')
|
||||
.getByRole('button', { name: 'Fill color' });
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'changeShapeColor':
|
||||
case 'changeShapeFillColor':
|
||||
case 'changeShapeStrokeColor':
|
||||
case 'changeShapeStrokeStyles': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-shape-button')
|
||||
.getByRole('button', { name: 'Border style' });
|
||||
.locator('edgeless-shape-color-picker')
|
||||
.getByLabel(/^Color$/);
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'changeShapeStyle': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-shape-button')
|
||||
.getByRole('button', { name: /^Style$/ });
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'changeConnectorStrokeColor': {
|
||||
const button = page
|
||||
.locator('edgeless-change-connector-button')
|
||||
.getByRole('button', { name: 'Stroke style' });
|
||||
const button = locatorComponentToolbar(page).getByLabel(/^Style$/);
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'changeConnectorStrokeStyles': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-connector-button')
|
||||
.getByRole('button', { name: 'Stroke style' });
|
||||
const button = locatorComponentToolbar(page).getByRole('button', {
|
||||
name: 'Stroke style',
|
||||
});
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'changeConnectorShape': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-connector-button')
|
||||
.getByRole('button', { name: 'Shape' });
|
||||
const button = locatorComponentToolbar(page).getByRole('button', {
|
||||
name: 'Shape',
|
||||
});
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'addFrame': {
|
||||
const button = locatorComponentToolbar(page).locator(
|
||||
'edgeless-add-frame-button'
|
||||
);
|
||||
const button = locatorComponentToolbar(page).getByLabel(/^Frame$/);
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'addGroup': {
|
||||
const button = locatorComponentToolbar(page).locator(
|
||||
'edgeless-add-group-button'
|
||||
);
|
||||
const button = locatorComponentToolbar(page).getByLabel(/^Group$/);
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
@@ -1223,67 +1188,64 @@ export async function triggerComponentToolbarAction(
|
||||
const moreButton = locatorComponentToolbarMoreButton(page);
|
||||
await moreButton.click();
|
||||
|
||||
const actionButton = moreButton
|
||||
.locator('.more-actions-container editor-menu-action')
|
||||
.filter({
|
||||
hasText: 'Group Section',
|
||||
});
|
||||
const actionButton = moreButton.locator('editor-menu-action').filter({
|
||||
hasText: 'Group Section',
|
||||
});
|
||||
await actionButton.click();
|
||||
break;
|
||||
}
|
||||
case 'ungroup': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-group-button')
|
||||
.getByRole('button', { name: 'Ungroup' });
|
||||
const button = locatorComponentToolbar(page).getByRole('button', {
|
||||
name: 'Ungroup',
|
||||
});
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'renameGroup': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-group-button')
|
||||
.getByRole('button', { name: 'Rename' });
|
||||
const button = locatorComponentToolbar(page).getByRole('button', {
|
||||
name: 'Rename',
|
||||
});
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'releaseFromGroup': {
|
||||
const button = locatorComponentToolbar(page).locator(
|
||||
'edgeless-release-from-group-button'
|
||||
);
|
||||
const button =
|
||||
locatorComponentToolbar(page).getByLabel('Release from group');
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'changeNoteColor': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-note-button')
|
||||
.getByRole('button', { name: 'Background' });
|
||||
const button = locatorComponentToolbar(page).getByRole('button', {
|
||||
name: 'Background',
|
||||
});
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'changeNoteDisplayMode': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-note-button')
|
||||
.getByRole('button', { name: 'Mode' });
|
||||
const button = locatorComponentToolbar(page).getByRole('button', {
|
||||
name: 'Mode',
|
||||
});
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'changeNoteSlicerSetting': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-note-button')
|
||||
.getByRole('button', { name: 'Slicer' });
|
||||
const button = locatorComponentToolbar(page).getByRole('button', {
|
||||
name: 'Slicer',
|
||||
});
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'changeNoteScale': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-note-button')
|
||||
.getByRole('button', { name: 'Scale' });
|
||||
const button = locatorComponentToolbar(page).getByRole('button', {
|
||||
name: 'Scale',
|
||||
});
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
case 'autoSize': {
|
||||
const button = locatorComponentToolbar(page)
|
||||
.locator('edgeless-change-note-button')
|
||||
.getByRole('button', { name: 'Size' });
|
||||
const button = locatorComponentToolbar(page).getByRole('button', {
|
||||
name: 'Size',
|
||||
});
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
@@ -1305,11 +1267,9 @@ export async function triggerComponentToolbarAction(
|
||||
const moreButton = locatorComponentToolbarMoreButton(page);
|
||||
await moreButton.click();
|
||||
|
||||
const actionButton = moreButton
|
||||
.locator('.more-actions-container editor-menu-action')
|
||||
.filter({
|
||||
hasText: 'Turn into linked doc',
|
||||
});
|
||||
const actionButton = moreButton.locator('editor-menu-action').filter({
|
||||
hasText: 'Turn into linked doc',
|
||||
});
|
||||
await actionButton.click();
|
||||
break;
|
||||
}
|
||||
@@ -1317,11 +1277,9 @@ export async function triggerComponentToolbarAction(
|
||||
const moreButton = locatorComponentToolbarMoreButton(page);
|
||||
await moreButton.click();
|
||||
|
||||
const actionButton = moreButton
|
||||
.locator('.more-actions-container editor-menu-action')
|
||||
.filter({
|
||||
hasText: 'Create linked doc',
|
||||
});
|
||||
const actionButton = moreButton.locator('editor-menu-action').filter({
|
||||
hasText: 'Create linked doc',
|
||||
});
|
||||
await actionButton.click();
|
||||
break;
|
||||
}
|
||||
@@ -1356,24 +1314,18 @@ export async function triggerComponentToolbarAction(
|
||||
break;
|
||||
}
|
||||
case 'autoArrange': {
|
||||
const button = locatorComponentToolbar(page).locator(
|
||||
'edgeless-align-button'
|
||||
);
|
||||
const toolbar = locatorComponentToolbar(page);
|
||||
const button = toolbar.getByLabel('Align objects');
|
||||
await button.click();
|
||||
const arrange = button.locator('editor-icon-button').filter({
|
||||
hasText: 'Auto arrange',
|
||||
});
|
||||
const arrange = toolbar.getByLabel('Auto arrange');
|
||||
await arrange.click();
|
||||
break;
|
||||
}
|
||||
case 'autoResize': {
|
||||
const button = locatorComponentToolbar(page).locator(
|
||||
'edgeless-align-button'
|
||||
);
|
||||
const toolbar = locatorComponentToolbar(page);
|
||||
const button = toolbar.getByLabel('Align objects');
|
||||
await button.click();
|
||||
const resize = button.locator('editor-icon-button').filter({
|
||||
hasText: 'Resize & Align',
|
||||
});
|
||||
const resize = toolbar.getByLabel('Resize & Align');
|
||||
await resize.click();
|
||||
break;
|
||||
}
|
||||
@@ -1382,7 +1334,7 @@ export async function triggerComponentToolbarAction(
|
||||
|
||||
export async function changeEdgelessNoteBackground(page: Page, label: string) {
|
||||
const colorButton = page
|
||||
.locator('edgeless-change-note-button')
|
||||
.locator('edgeless-color-picker-button')
|
||||
.locator('edgeless-color-panel')
|
||||
.locator(`.color-unit[aria-label="${label}"]`);
|
||||
await colorButton.click();
|
||||
@@ -1390,18 +1342,16 @@ export async function changeEdgelessNoteBackground(page: Page, label: string) {
|
||||
|
||||
export async function changeShapeFillColor(page: Page, label: string) {
|
||||
const colorButton = page
|
||||
.locator('edgeless-change-shape-button')
|
||||
.locator('edgeless-color-picker-button.fill-color')
|
||||
.locator('edgeless-color-panel')
|
||||
.locator('edgeless-shape-color-picker')
|
||||
.locator('edgeless-color-panel[aria-label="Fill color"]')
|
||||
.locator(`.color-unit[aria-label="${label}"]`);
|
||||
await colorButton.click({ force: true });
|
||||
}
|
||||
|
||||
export async function changeShapeFillColorToTransparent(page: Page) {
|
||||
const colorButton = page
|
||||
.locator('edgeless-change-shape-button')
|
||||
.locator('edgeless-color-picker-button.fill-color')
|
||||
.locator('edgeless-color-panel')
|
||||
.locator('edgeless-shape-color-picker')
|
||||
.locator('edgeless-color-panel[aria-label="Fill color"]')
|
||||
.locator('edgeless-color-custom-button');
|
||||
await colorButton.click({ force: true });
|
||||
|
||||
@@ -1417,9 +1367,8 @@ export async function changeShapeFillColorToTransparent(page: Page) {
|
||||
|
||||
export async function changeShapeStrokeColor(page: Page, color: string) {
|
||||
const colorButton = page
|
||||
.locator('edgeless-change-shape-button')
|
||||
.locator('edgeless-color-picker-button.border-style')
|
||||
.locator('edgeless-color-panel')
|
||||
.locator('edgeless-shape-color-picker')
|
||||
.locator('edgeless-color-panel[aria-label="Border color"]')
|
||||
.locator(`.color-unit[aria-label="${color}"]`);
|
||||
await colorButton.click();
|
||||
}
|
||||
@@ -1446,10 +1395,13 @@ export async function resizeConnectorByStartCapitalHandler(
|
||||
}
|
||||
|
||||
export function getEdgelessLineWidthPanel(page: Page) {
|
||||
return page
|
||||
.locator('edgeless-change-shape-button')
|
||||
.locator('edgeless-line-width-panel')
|
||||
.locator('.line-width-panel');
|
||||
return (
|
||||
page
|
||||
.locator('affine-toolbar-widget editor-toolbar')
|
||||
// TODO(@fundon): remove ` edgeless-line-width-panel`
|
||||
.locator('affine-edgeless-line-width-panel')
|
||||
.locator('.line-width-panel')
|
||||
);
|
||||
}
|
||||
export async function changeShapeStrokeWidth(page: Page) {
|
||||
const lineWidthPanel = getEdgelessLineWidthPanel(page);
|
||||
@@ -1468,7 +1420,7 @@ export function locatorShapeStrokeStyleButton(
|
||||
mode: 'solid' | 'dash' | 'none'
|
||||
) {
|
||||
return page
|
||||
.locator('edgeless-change-shape-button')
|
||||
.locator('affine-toolbar-widget editor-toolbar')
|
||||
.locator(`.line-style-button.mode-${mode}`);
|
||||
}
|
||||
|
||||
@@ -1485,7 +1437,7 @@ export function locatorShapeStyleButton(
|
||||
style: 'general' | 'scribbled'
|
||||
) {
|
||||
return page
|
||||
.locator('edgeless-change-shape-button')
|
||||
.locator('affine-toolbar-widget editor-toolbar')
|
||||
.locator('edgeless-shape-style-panel')
|
||||
.getByRole('button', { name: style });
|
||||
}
|
||||
@@ -1499,8 +1451,7 @@ export async function changeShapeStyle(
|
||||
}
|
||||
|
||||
export async function changeConnectorStrokeColor(page: Page, color: string) {
|
||||
const colorButton = page
|
||||
.locator('edgeless-change-connector-button')
|
||||
const colorButton = locatorComponentToolbar(page)
|
||||
.locator('edgeless-color-panel')
|
||||
.getByLabel(color);
|
||||
await colorButton.click();
|
||||
@@ -1510,10 +1461,12 @@ export function locatorConnectorStrokeWidthButton(
|
||||
page: Page,
|
||||
buttonPosition: number
|
||||
) {
|
||||
return page
|
||||
.locator('edgeless-change-connector-button')
|
||||
.locator(`edgeless-line-width-panel`)
|
||||
.locator(`.line-width-button:nth-child(${buttonPosition})`);
|
||||
return (
|
||||
locatorComponentToolbar(page)
|
||||
// TODO(@fundon): remove redundant components
|
||||
.locator('affine-edgeless-line-width-panel')
|
||||
.locator(`.line-width-button:nth-child(${buttonPosition})`)
|
||||
);
|
||||
}
|
||||
export async function changeConnectorStrokeWidth(
|
||||
page: Page,
|
||||
@@ -1527,9 +1480,9 @@ export function locatorConnectorStrokeStyleButton(
|
||||
page: Page,
|
||||
mode: 'solid' | 'dash' | 'none'
|
||||
) {
|
||||
return page
|
||||
.locator('edgeless-change-connector-button')
|
||||
.locator(`.line-style-button.mode-${mode}`);
|
||||
return locatorComponentToolbar(page).locator(
|
||||
`.line-style-button.mode-${mode}`
|
||||
);
|
||||
}
|
||||
export async function changeConnectorStrokeStyle(
|
||||
page: Page,
|
||||
|
||||
@@ -1197,7 +1197,7 @@ export async function assertConnectorStrokeColor(
|
||||
color: string
|
||||
) {
|
||||
const colorButton = page
|
||||
.locator('edgeless-change-connector-button')
|
||||
.locator('affine-toolbar-widget editor-toolbar')
|
||||
.locator('edgeless-color-panel')
|
||||
.locator(`.color-unit[aria-label="${label}"]`)
|
||||
.locator('svg');
|
||||
|
||||
@@ -4,7 +4,6 @@ import { expect, type Locator, type Page } from '@playwright/test';
|
||||
|
||||
declare type _GLOBAL_ = typeof BlocksuiteEffects;
|
||||
|
||||
const EDGELESS_ELEMENT_TOOLBAR_WIDGET = 'edgeless-element-toolbar-widget';
|
||||
const EDGELESS_TOOLBAR_WIDGET = 'edgeless-toolbar-widget';
|
||||
|
||||
export function locateModeSwitchButton(
|
||||
@@ -408,12 +407,6 @@ export async function resizeElementByHandle(
|
||||
await dragView(page, from, to, editorIndex);
|
||||
}
|
||||
|
||||
export function locateElementToolbar(page: Page, editorIndex = 0) {
|
||||
return locateEditorContainer(page, editorIndex).locator(
|
||||
EDGELESS_ELEMENT_TOOLBAR_WIDGET
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a not block in canvas
|
||||
* @param position the position or xwyh of the note block in canvas
|
||||
|
||||
Reference in New Issue
Block a user