mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-01 22:29:44 +08:00
refactor(editor): adjust ui of surface-ref inner toolbar (#11129)
Close [BS-2803](https://linear.app/affine-design/issue/BS-2803/inserted-frame-ui%E8%B0%83%E6%95%B4) Close [BS-2815](https://linear.app/affine-design/issue/BS-2815/inserted-group-ui调整) ### What Changes - Add an inner toolbar for hovered `surface-ref-block` - Simplify viewport related codes of `surface-ref-block` - Expose popover floating options from `affine-menu-button` https://github.com/user-attachments/assets/916b0a22-6271-4a6f-b338-6630e0426967
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
|
||||
import { type SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
@@ -19,7 +19,7 @@ export const BUILT_IN_GROUPS: MenuItemGroup<SurfaceRefToolbarContext>[] = [
|
||||
items: [
|
||||
{
|
||||
type: 'copy',
|
||||
label: 'Copy',
|
||||
label: 'Copy as Image',
|
||||
icon: CopyIcon,
|
||||
action: ctx => {
|
||||
if (!(ctx.blockComponent.referenceModel && ctx.doc.root?.id)) {
|
||||
@@ -56,6 +56,7 @@ export const BUILT_IN_GROUPS: MenuItemGroup<SurfaceRefToolbarContext>[] = [
|
||||
ctx.close();
|
||||
},
|
||||
},
|
||||
// TODO(@L-Sun): add duplicate action after refactoring toolbar
|
||||
{
|
||||
type: 'download',
|
||||
label: 'Download',
|
||||
|
||||
@@ -1,33 +1,44 @@
|
||||
import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
|
||||
import { HoverController } from '@blocksuite/affine-components/hover';
|
||||
import { CaptionIcon, OpenIcon } from '@blocksuite/affine-components/icons';
|
||||
import { isPeekable, peek } from '@blocksuite/affine-components/peek';
|
||||
import { peek } from '@blocksuite/affine-components/peek';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
cloneGroups,
|
||||
getMoreMenuConfig,
|
||||
type MenuItem,
|
||||
type MenuItemGroup,
|
||||
renderGroups,
|
||||
renderToolbarSeparator,
|
||||
} from '@blocksuite/affine-components/toolbar';
|
||||
import type { SurfaceRefBlockModel } from '@blocksuite/affine-model';
|
||||
import { PAGE_HEADER_HEIGHT } from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
BlockSelection,
|
||||
TextSelection,
|
||||
WidgetComponent,
|
||||
} from '@blocksuite/block-std';
|
||||
FrameBlockModel,
|
||||
GroupElementModel,
|
||||
MindmapElementModel,
|
||||
ShapeElementModel,
|
||||
type SurfaceRefBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
copySelectedModelsCommand,
|
||||
draftSelectedModelsCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import type { ButtonPopperOptions } from '@blocksuite/affine-shared/utils';
|
||||
import { WidgetComponent } from '@blocksuite/block-std';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
CaptionIcon,
|
||||
CenterPeekIcon,
|
||||
CopyIcon,
|
||||
EdgelessIcon,
|
||||
FrameIcon,
|
||||
GroupIcon,
|
||||
MindmapIcon,
|
||||
MoreVerticalIcon,
|
||||
OpenInNewIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { offset, shift } from '@floating-ui/dom';
|
||||
import { html, nothing } from 'lit';
|
||||
import { css, html, nothing, type TemplateResult } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { join } from 'lit/directives/join.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
import { BUILT_IN_GROUPS } from './config.js';
|
||||
import { SurfaceRefToolbarContext } from './context.js';
|
||||
@@ -38,6 +49,59 @@ export class AffineSurfaceRefToolbar extends WidgetComponent<
|
||||
SurfaceRefBlockModel,
|
||||
SurfaceRefBlockComponent
|
||||
> {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
editor-icon-button,
|
||||
editor-menu-button {
|
||||
background: ${unsafeCSSVarV2('button/iconButtonSolid')};
|
||||
color: ${unsafeCSSVarV2('text/primary')};
|
||||
box-shadow: ${unsafeCSSVar('shadow1')};
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.surface-ref-toolbar-title {
|
||||
display: flex;
|
||||
padding: 2px 4px;
|
||||
margin-right: auto;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
background: ${unsafeCSSVarV2('button/iconButtonSolid')};
|
||||
|
||||
svg {
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: ${unsafeCSSVarV2('text/primary')};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _popoverOptions: Partial<ButtonPopperOptions> = {
|
||||
mainAxis: 4,
|
||||
stateUpdated: ({ display }) => {
|
||||
this.dataset.openMenuDisplay = display;
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
* Caches the more menu items.
|
||||
* Currently only supports configuring more menu.
|
||||
@@ -45,68 +109,167 @@ export class AffineSurfaceRefToolbar extends WidgetComponent<
|
||||
moreGroups: MenuItemGroup<SurfaceRefToolbarContext>[] =
|
||||
cloneGroups(BUILT_IN_GROUPS);
|
||||
|
||||
private readonly _hoverController = new HoverController(
|
||||
this,
|
||||
({ abortController }) => {
|
||||
const surfaceRefBlock = this.block;
|
||||
if (!surfaceRefBlock) {
|
||||
return null;
|
||||
}
|
||||
const selection = this.host.selection;
|
||||
|
||||
const textSelection = selection.find(TextSelection);
|
||||
if (
|
||||
!!textSelection &&
|
||||
(!!textSelection.to || !!textSelection.from.length)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blockSelections = selection.filter(BlockSelection);
|
||||
if (
|
||||
blockSelections.length > 1 ||
|
||||
(blockSelections.length === 1 &&
|
||||
blockSelections[0].blockId !== surfaceRefBlock.blockId)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
template: SurfaceRefToolbarOptions({
|
||||
context: new SurfaceRefToolbarContext(this.block, abortController),
|
||||
groups: this.moreGroups,
|
||||
}),
|
||||
computePosition: {
|
||||
referenceElement: this.block,
|
||||
placement: 'top-start',
|
||||
middleware: [
|
||||
offset({
|
||||
mainAxis: 12,
|
||||
crossAxis: 10,
|
||||
}),
|
||||
shift({
|
||||
crossAxis: true,
|
||||
padding: {
|
||||
top: PAGE_HEADER_HEIGHT + 12,
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
},
|
||||
}),
|
||||
],
|
||||
autoUpdate: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.moreGroups = getMoreMenuConfig(this.std).configure(this.moreGroups);
|
||||
if (!this.block) {
|
||||
return;
|
||||
|
||||
this.disposables.addFromEvent(this, 'dblclick', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
private _renderTitle() {
|
||||
if (!this.block) return nothing;
|
||||
const { referenceModel } = this.block;
|
||||
if (!referenceModel) return nothing;
|
||||
|
||||
let title = '';
|
||||
let icon: TemplateResult<1> | null = null;
|
||||
if (referenceModel instanceof GroupElementModel) {
|
||||
title = referenceModel.title.toString();
|
||||
icon = GroupIcon();
|
||||
} else if (referenceModel instanceof FrameBlockModel) {
|
||||
title = referenceModel.props.title.toString();
|
||||
icon = FrameIcon();
|
||||
} else if (referenceModel instanceof MindmapElementModel) {
|
||||
const rootElement = referenceModel.tree.element;
|
||||
if (rootElement instanceof ShapeElementModel) {
|
||||
title = rootElement.text?.toString() ?? '';
|
||||
}
|
||||
icon = MindmapIcon();
|
||||
}
|
||||
this._hoverController.setReference(this.block);
|
||||
|
||||
return html`<div class="surface-ref-toolbar-title">
|
||||
${icon}
|
||||
<span>${title}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderOpenButton() {
|
||||
const referenceModel = this.block?.referenceModel;
|
||||
if (!referenceModel) return nothing;
|
||||
|
||||
const openMenuActions: MenuItem[] = [
|
||||
{
|
||||
type: 'open-in-edgeless',
|
||||
label: 'Open in Edgeless',
|
||||
icon: EdgelessIcon(),
|
||||
action: () => this.block?.viewInEdgeless(),
|
||||
disabled: this.block.model.doc.readonly,
|
||||
},
|
||||
{
|
||||
type: 'open-in-center-peek',
|
||||
label: 'Open in center peek',
|
||||
icon: CenterPeekIcon(),
|
||||
action: () => this.block && peek(this.block),
|
||||
},
|
||||
// TODO(@L-Sun): add split view and new tab
|
||||
];
|
||||
|
||||
return html`<editor-menu-button
|
||||
data-show
|
||||
aria-label="Open"
|
||||
style=${styleMap({
|
||||
'--content-padding': '8px',
|
||||
})}
|
||||
.button=${html`
|
||||
<editor-icon-button .iconContainerPadding=${4} .iconSize=${'16px'}>
|
||||
${OpenInNewIcon()} ${ArrowDownSmallIcon()}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
.popperOptions=${this._popoverOptions}
|
||||
>
|
||||
<div data-orientation="vertical">
|
||||
${repeat(
|
||||
openMenuActions,
|
||||
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 _moreButton() {
|
||||
if (!this.block) return nothing;
|
||||
|
||||
const moreMenuActions = renderGroups(
|
||||
this.moreGroups,
|
||||
new SurfaceRefToolbarContext(this.block, new AbortController())
|
||||
);
|
||||
|
||||
return html`<editor-menu-button
|
||||
data-show
|
||||
style=${styleMap({
|
||||
'--content-padding': '8px',
|
||||
})}
|
||||
.button=${html`
|
||||
<editor-icon-button .iconContainerPadding=${4} .iconSize=${'16px'}>
|
||||
${MoreVerticalIcon()}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
.popperOptions=${this._popoverOptions}
|
||||
>
|
||||
<div data-orientation="vertical">${moreMenuActions}</div>
|
||||
</editor-menu-button>`;
|
||||
}
|
||||
|
||||
private _renderButtons() {
|
||||
if (!this.block) return nothing;
|
||||
const readonly = this.block.model.doc.readonly;
|
||||
const buttons = [
|
||||
this._renderOpenButton(),
|
||||
when(
|
||||
!readonly,
|
||||
() =>
|
||||
html`<editor-icon-button
|
||||
.iconContainerPadding=${4}
|
||||
.iconSize=${'16px'}
|
||||
@click=${() => {
|
||||
if (!this.block) return;
|
||||
this.std.command
|
||||
.chain()
|
||||
.pipe(draftSelectedModelsCommand, {
|
||||
selectedModels: [this.block.model],
|
||||
})
|
||||
.pipe(copySelectedModelsCommand)
|
||||
.run();
|
||||
toast(this.block.std.host, 'Copied to clipboard');
|
||||
}}
|
||||
>
|
||||
${CopyIcon()}
|
||||
</editor-icon-button>`
|
||||
),
|
||||
when(
|
||||
!readonly,
|
||||
() =>
|
||||
html`<editor-icon-button
|
||||
.iconContainerPadding=${4}
|
||||
.iconSize=${'16px'}
|
||||
@click=${() => {
|
||||
if (!this.block) return;
|
||||
this.block.captionElement.show();
|
||||
}}
|
||||
>
|
||||
${CaptionIcon()}
|
||||
</editor-icon-button>`
|
||||
),
|
||||
this._moreButton(),
|
||||
];
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this.block) return nothing;
|
||||
return html`${this._renderTitle()} ${this._renderButtons()}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,120 +278,3 @@ declare global {
|
||||
[AFFINE_SURFACE_REF_TOOLBAR]: AffineSurfaceRefToolbar;
|
||||
}
|
||||
}
|
||||
|
||||
function SurfaceRefToolbarOptions({
|
||||
context,
|
||||
groups,
|
||||
}: {
|
||||
context: SurfaceRefToolbarContext;
|
||||
groups: MenuItemGroup<SurfaceRefToolbarContext>[];
|
||||
}) {
|
||||
const { blockComponent, abortController } = context;
|
||||
const readonly = blockComponent.model.doc.readonly;
|
||||
const hasValidReference = !!blockComponent.referenceModel;
|
||||
|
||||
const openMenuActions: MenuItem[] = [];
|
||||
|
||||
const iconSize = { width: '20px', height: '20px' };
|
||||
if (hasValidReference) {
|
||||
openMenuActions.push({
|
||||
type: 'open-in-edgeless',
|
||||
label: 'Open in edgeless',
|
||||
icon: EdgelessIcon(iconSize),
|
||||
action: () => blockComponent.viewInEdgeless(),
|
||||
disabled: readonly,
|
||||
});
|
||||
|
||||
if (isPeekable(blockComponent)) {
|
||||
openMenuActions.push({
|
||||
type: 'open-in-center-peek',
|
||||
label: 'Open in center peek',
|
||||
icon: CenterPeekIcon(iconSize),
|
||||
action: () => peek(blockComponent),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const moreMenuActions = renderGroups(groups, context);
|
||||
|
||||
const buttons = [
|
||||
openMenuActions.length
|
||||
? html`
|
||||
<editor-menu-button
|
||||
.contentPadding=${'8px'}
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Open doc"
|
||||
.justify=${'space-between'}
|
||||
.labelHeight=${'20px'}
|
||||
>
|
||||
${OpenIcon}${ArrowDownSmallIcon({
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
})}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="large" data-orientation="vertical">
|
||||
${repeat(
|
||||
openMenuActions,
|
||||
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>
|
||||
`
|
||||
: nothing,
|
||||
|
||||
readonly
|
||||
? nothing
|
||||
: html`
|
||||
<editor-icon-button
|
||||
aria-label="Caption"
|
||||
.tooltip=${'Add Caption'}
|
||||
@click=${() => {
|
||||
abortController.abort();
|
||||
blockComponent.captionElement.show();
|
||||
}}
|
||||
>
|
||||
${CaptionIcon}
|
||||
</editor-icon-button>
|
||||
`,
|
||||
|
||||
html`
|
||||
<editor-menu-button
|
||||
.contentPadding=${'8px'}
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="More"
|
||||
.tooltip=${'More'}
|
||||
.iconSize=${'20px'}
|
||||
>
|
||||
${MoreVerticalIcon()}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="large" data-orientation="vertical">
|
||||
${moreMenuActions}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`,
|
||||
];
|
||||
|
||||
return html`
|
||||
<editor-toolbar class="surface-ref-toolbar-container">
|
||||
${join(
|
||||
buttons.filter(button => button !== nothing),
|
||||
renderToolbarSeparator
|
||||
)}
|
||||
</editor-toolbar>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user