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