mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-01 09:30:01 +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,6 +1,6 @@
|
|||||||
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
|
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
|
||||||
import { EdgelessCRUDExtension } from '@blocksuite/affine-block-surface';
|
import { EdgelessCRUDExtension } from '@blocksuite/affine-block-surface';
|
||||||
import { MindmapStyle } from '@blocksuite/affine-model';
|
import { MindmapStyle, SurfaceRefBlockSchema } from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
type SlashMenuActionItem,
|
type SlashMenuActionItem,
|
||||||
type SlashMenuConfig,
|
type SlashMenuConfig,
|
||||||
@@ -20,8 +20,7 @@ const surfaceRefSlashMenuConfig: SlashMenuConfig = {
|
|||||||
const crud = std.get(EdgelessCRUDExtension);
|
const crud = std.get(EdgelessCRUDExtension);
|
||||||
const frameMgr = std.get(EdgelessFrameManagerIdentifier);
|
const frameMgr = std.get(EdgelessFrameManagerIdentifier);
|
||||||
|
|
||||||
const findSpace = (bound: Bound) => {
|
const findSpace = (bound: Bound, padding = 20) => {
|
||||||
const padding = 20;
|
|
||||||
const gfx = std.get(GfxControllerIdentifier);
|
const gfx = std.get(GfxControllerIdentifier);
|
||||||
let elementInFrameBound = gfx.grid.search(bound);
|
let elementInFrameBound = gfx.grid.search(bound);
|
||||||
while (elementInFrameBound.length > 0) {
|
while (elementInFrameBound.length > 0) {
|
||||||
@@ -78,7 +77,7 @@ const surfaceRefSlashMenuConfig: SlashMenuConfig = {
|
|||||||
},
|
},
|
||||||
group: `5_Edgeless Element@${index++}`,
|
group: `5_Edgeless Element@${index++}`,
|
||||||
action: () => {
|
action: () => {
|
||||||
const bound = findSpace(Bound.fromXYWH([0, 0, 200, 200]));
|
const bound = findSpace(Bound.fromXYWH([0, 0, 200, 200]), 150);
|
||||||
const { x, y, h } = bound;
|
const { x, y, h } = bound;
|
||||||
|
|
||||||
const rootW = 145;
|
const rootW = 145;
|
||||||
@@ -160,6 +159,6 @@ const surfaceRefSlashMenuConfig: SlashMenuConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SurfaceRefSlashMenuConfigExtension = SlashMenuConfigExtension(
|
export const SurfaceRefSlashMenuConfigExtension = SlashMenuConfigExtension(
|
||||||
'affine:surface-ref',
|
SurfaceRefBlockSchema.model.flavour,
|
||||||
surfaceRefSlashMenuConfig
|
surfaceRefSlashMenuConfig
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,4 +2,3 @@ export * from './commands.js';
|
|||||||
export * from './surface-ref-block.js';
|
export * from './surface-ref-block.js';
|
||||||
export * from './surface-ref-block-edgeless.js';
|
export * from './surface-ref-block-edgeless.js';
|
||||||
export * from './surface-ref-spec.js';
|
export * from './surface-ref-spec.js';
|
||||||
export * from './utils.js';
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
|
import { type FrameBlockComponent } from '@blocksuite/affine-block-frame';
|
||||||
import {
|
import {
|
||||||
EdgelessCRUDIdentifier,
|
EdgelessCRUDIdentifier,
|
||||||
getSurfaceBlock,
|
getSurfaceBlock,
|
||||||
type SurfaceBlockModel,
|
|
||||||
SurfaceElementModel,
|
|
||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
|
import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
|
||||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||||
import {
|
import {
|
||||||
FrameBlockModel,
|
FrameBlockModel,
|
||||||
RootBlockModel,
|
|
||||||
type SurfaceRefBlockModel,
|
type SurfaceRefBlockModel,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
@@ -17,8 +15,8 @@ import {
|
|||||||
ThemeProvider,
|
ThemeProvider,
|
||||||
ViewportElementExtension,
|
ViewportElementExtension,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
|
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||||
import {
|
import {
|
||||||
matchModels,
|
|
||||||
requestConnectedFrame,
|
requestConnectedFrame,
|
||||||
SpecProvider,
|
SpecProvider,
|
||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
@@ -43,20 +41,18 @@ import {
|
|||||||
deserializeXYWH,
|
deserializeXYWH,
|
||||||
type SerializedXYWH,
|
type SerializedXYWH,
|
||||||
} from '@blocksuite/global/gfx';
|
} from '@blocksuite/global/gfx';
|
||||||
import { DeleteIcon, EdgelessIcon, FrameIcon } from '@blocksuite/icons/lit';
|
import { assertType } from '@blocksuite/global/utils';
|
||||||
|
import { DeleteIcon } from '@blocksuite/icons/lit';
|
||||||
import type { BaseSelection, Store } from '@blocksuite/store';
|
import type { BaseSelection, Store } from '@blocksuite/store';
|
||||||
import { css, html, nothing, type TemplateResult } from 'lit';
|
import { effect, signal } from '@preact/signals-core';
|
||||||
|
import { css, html, nothing } from 'lit';
|
||||||
import { query, state } from 'lit/decorators.js';
|
import { query, state } from 'lit/decorators.js';
|
||||||
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
import { guard } from 'lit/directives/guard.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import { noContentPlaceholder } from './utils.js';
|
import { noContentPlaceholder } from './utils.js';
|
||||||
|
|
||||||
const iconSize = { width: '20px', height: '20px' };
|
|
||||||
const REF_LABEL_ICON = {
|
|
||||||
'affine:frame': FrameIcon(iconSize),
|
|
||||||
DEFAULT_NOTE_HEIGHT: EdgelessIcon(iconSize),
|
|
||||||
} as Record<string, TemplateResult>;
|
|
||||||
|
|
||||||
const NO_CONTENT_TITLE = {
|
const NO_CONTENT_TITLE = {
|
||||||
'affine:frame': 'Frame',
|
'affine:frame': 'Frame',
|
||||||
group: 'Group',
|
group: 'Group',
|
||||||
@@ -71,11 +67,28 @@ const NO_CONTENT_REASON = {
|
|||||||
@Peekable()
|
@Peekable()
|
||||||
export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockModel> {
|
export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockModel> {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
|
affine-surface-ref {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
affine-surface-ref:not(:hover)
|
||||||
|
affine-surface-ref-toolbar:not([data-open-menu-display='show']) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.affine-surface-ref {
|
.affine-surface-ref {
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid ${unsafeCSSVarV2('edgeless/frame/border/default')};
|
||||||
|
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.affine-surface-ref.focused {
|
||||||
|
border-color: ${unsafeCSSVarV2('edgeless/frame/border/active')};
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
@@ -150,7 +163,6 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
|
|
||||||
.ref-content {
|
.ref-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 20px;
|
|
||||||
background-color: var(--affine-background-primary-color);
|
background-color: var(--affine-background-primary-color);
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
var(--affine-edgeless-grid-color) 1px,
|
var(--affine-edgeless-grid-color) 1px,
|
||||||
@@ -166,75 +178,6 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ref-viewport.frame {
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid var(--affine-black-30);
|
|
||||||
}
|
|
||||||
|
|
||||||
.surface-ref-mask {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.surface-ref-mask:hover {
|
|
||||||
background-color: rgba(211, 211, 211, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.surface-ref-mask:hover .ref-label {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-label {
|
|
||||||
display: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-label {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: 1px solid var(--affine-border-color);
|
|
||||||
gap: 14px;
|
|
||||||
|
|
||||||
background: var(--affine-background-primary-color);
|
|
||||||
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-label .title {
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: 600;
|
|
||||||
font-family: var(--affine-font-family);
|
|
||||||
line-height: 20px;
|
|
||||||
|
|
||||||
color: var(--affine-text-secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-label .title > svg {
|
|
||||||
color: var(--affine-icon-secondary);
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: baseline;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-label .suffix {
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--affine-text-disable-color);
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
private _previewDoc: Store | null = null;
|
private _previewDoc: Store | null = null;
|
||||||
@@ -245,9 +188,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
|
|
||||||
private _referencedModel: GfxModel | null = null;
|
private _referencedModel: GfxModel | null = null;
|
||||||
|
|
||||||
private _referenceXYWH: SerializedXYWH | null = null;
|
private readonly _referenceXYWH$ = signal<SerializedXYWH | null>(null);
|
||||||
|
|
||||||
private _viewportEditor: EditorHost | null = null;
|
|
||||||
|
|
||||||
private get _shouldRender() {
|
private get _shouldRender() {
|
||||||
return (
|
return (
|
||||||
@@ -312,55 +253,31 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _initReferencedModel() {
|
private _initReferencedModel() {
|
||||||
const surfaceModel = getSurfaceBlock(this.doc);
|
|
||||||
this._surfaceModel = surfaceModel;
|
|
||||||
|
|
||||||
const findReferencedModel = (): [GfxModel | null, string] => {
|
const findReferencedModel = (): [GfxModel | null, string] => {
|
||||||
if (!this.model.props.reference) return [null, this.doc.id];
|
if (!this.model.props.reference) return [null, this.doc.id];
|
||||||
|
const referenceId = this.model.props.reference;
|
||||||
|
|
||||||
if (this.doc.getBlock(this.model.props.reference)) {
|
const find = (doc: Store): [GfxModel | null, string] => {
|
||||||
return [
|
const block = doc.getBlock(referenceId)?.model;
|
||||||
this.doc.getBlock(this.model.props.reference)
|
if (block instanceof GfxBlockElementModel) {
|
||||||
?.model as GfxBlockElementModel,
|
return [block, doc.id];
|
||||||
this.doc.id,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._surfaceModel?.getElementById(this.model.props.reference)) {
|
|
||||||
return [
|
|
||||||
this._surfaceModel.getElementById(this.model.props.reference),
|
|
||||||
this.doc.id,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = [...this.std.workspace.docs.values()]
|
|
||||||
.map(doc => doc.getStore())
|
|
||||||
.find(
|
|
||||||
doc =>
|
|
||||||
doc.getBlock(this.model.props.reference) ||
|
|
||||||
getSurfaceBlock(doc)?.getElementById(this.model.props.reference)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (doc) {
|
|
||||||
this._surfaceModel = getSurfaceBlock(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doc && doc.getBlock(this.model.props.reference)) {
|
|
||||||
return [
|
|
||||||
doc.getBlock(this.model.props.reference)
|
|
||||||
?.model as GfxBlockElementModel,
|
|
||||||
doc.id,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doc) {
|
|
||||||
const surfaceBlock = getSurfaceBlock(doc);
|
|
||||||
if (surfaceBlock) {
|
|
||||||
return [
|
|
||||||
surfaceBlock.getElementById(this.model.props.reference),
|
|
||||||
doc.id,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
const surfaceBlock = getSurfaceBlock(doc);
|
||||||
|
if (!surfaceBlock) return [null, doc.id];
|
||||||
|
|
||||||
|
const element = surfaceBlock.getElementById(referenceId);
|
||||||
|
if (element) return [element, doc.id];
|
||||||
|
|
||||||
|
return [null, doc.id];
|
||||||
|
};
|
||||||
|
|
||||||
|
// find current doc first
|
||||||
|
let result = find(this.doc);
|
||||||
|
if (result[0]) return result;
|
||||||
|
|
||||||
|
for (const doc of this.std.workspace.docs.values()) {
|
||||||
|
result = find(doc.getStore());
|
||||||
|
if (result[0]) return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [null, this.doc.id];
|
return [null, this.doc.id];
|
||||||
@@ -371,10 +288,10 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
|
|
||||||
this._referencedModel =
|
this._referencedModel =
|
||||||
referencedModel && referencedModel.xywh ? referencedModel : null;
|
referencedModel && referencedModel.xywh ? referencedModel : null;
|
||||||
|
// TODO(@L-Sun): clear query cache
|
||||||
this._previewDoc = this.doc.workspace.getDoc(docId, {
|
this._previewDoc = this.doc.workspace.getDoc(docId, {
|
||||||
readonly: true,
|
readonly: true,
|
||||||
});
|
});
|
||||||
this._referenceXYWH = this._referencedModel?.xywh ?? null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
@@ -390,9 +307,9 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (surfaceModel && this._referencedModel instanceof SurfaceElementModel) {
|
if (this._referencedModel instanceof GfxPrimitiveElementModel) {
|
||||||
this._disposables.add(
|
this._disposables.add(
|
||||||
surfaceModel.elementRemoved.subscribe(({ id }) => {
|
this._referencedModel.surface.elementRemoved.subscribe(({ id }) => {
|
||||||
if (this.model.props.reference === id) {
|
if (this.model.props.reference === id) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
@@ -422,45 +339,37 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _initSpec() {
|
private _initViewport() {
|
||||||
const refreshViewport = this._refreshViewport.bind(this);
|
const refreshViewport = () => {
|
||||||
class SurfaceRefViewportInitializer extends LifeCycleWatcher {
|
if (!this._referenceXYWH$.value) return;
|
||||||
static override readonly key = 'surfaceRefViewportInitializer';
|
const previewEditorHost = this.previewEditor;
|
||||||
|
if (!previewEditorHost) return;
|
||||||
|
const gfx = previewEditorHost.std.get(GfxControllerIdentifier);
|
||||||
|
const viewport = gfx.viewport;
|
||||||
|
|
||||||
override mounted() {
|
let bound = Bound.deserialize(this._referenceXYWH$.value);
|
||||||
const disposable = this.std.view.viewUpdated.subscribe(payload => {
|
const w = Math.max(this.getBoundingClientRect().width, bound.w);
|
||||||
if (payload.type !== 'block') return;
|
const aspectRatio = bound.w / bound.h;
|
||||||
if (
|
const h = w / aspectRatio;
|
||||||
payload.method === 'add' &&
|
|
||||||
matchModels(payload.view.model, [RootBlockModel])
|
bound = Bound.fromCenter(bound.center, w, h);
|
||||||
) {
|
|
||||||
disposable.unsubscribe();
|
viewport.setViewportByBound(bound);
|
||||||
queueMicrotask(() => refreshViewport());
|
};
|
||||||
const gfx = this.std.get(GfxControllerIdentifier);
|
this.disposables.add(effect(refreshViewport));
|
||||||
gfx.viewport.sizeUpdated.subscribe(() => {
|
|
||||||
refreshViewport();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._previewSpec.extend([SurfaceRefViewportInitializer]);
|
|
||||||
|
|
||||||
const referenceId = this.model.props.reference;
|
const referenceId = this.model.props.reference;
|
||||||
const setReferenceXYWH = (xywh: typeof this._referenceXYWH) => {
|
const referenceXYWH$ = this._referenceXYWH$;
|
||||||
this._referenceXYWH = xywh;
|
class SurfaceRefViewportWatcher extends LifeCycleWatcher {
|
||||||
};
|
static override readonly key = 'surface-ref-viewport-watcher';
|
||||||
|
|
||||||
class FrameGroupViewWatcher extends LifeCycleWatcher {
|
|
||||||
static override readonly key = 'surface-ref-group-view-watcher';
|
|
||||||
|
|
||||||
private readonly _disposable = new DisposableGroup();
|
private readonly _disposable = new DisposableGroup();
|
||||||
|
|
||||||
override mounted() {
|
override mounted() {
|
||||||
const crud = this.std.get(EdgelessCRUDIdentifier);
|
const crud = this.std.get(EdgelessCRUDIdentifier);
|
||||||
const { _disposable } = this;
|
const gfx = this.std.get(GfxControllerIdentifier);
|
||||||
const surfaceModel = getSurfaceBlock(this.std.store);
|
const { surface, viewport } = gfx;
|
||||||
if (!surfaceModel) return;
|
if (!surface) return;
|
||||||
|
|
||||||
const referenceElement = crud.getElementById(referenceId);
|
const referenceElement = crud.getElementById(referenceId);
|
||||||
if (!referenceElement) {
|
if (!referenceElement) {
|
||||||
@@ -469,23 +378,39 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
`can not find element(id:${referenceElement})`
|
`can not find element(id:${referenceElement})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
referenceXYWH$.value = referenceElement.xywh;
|
||||||
|
|
||||||
|
const { _disposable } = this;
|
||||||
|
_disposable.add(viewport.sizeUpdated.subscribe(refreshViewport));
|
||||||
|
|
||||||
if (referenceElement instanceof FrameBlockModel) {
|
if (referenceElement instanceof FrameBlockModel) {
|
||||||
_disposable.add(
|
_disposable.add(
|
||||||
referenceElement.xywh$.subscribe(xywh => {
|
referenceElement.xywh$.subscribe(xywh => {
|
||||||
setReferenceXYWH(xywh);
|
referenceXYWH$.value = xywh;
|
||||||
refreshViewport();
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
const subscription = this.std.view.viewUpdated.subscribe(
|
||||||
|
({ id, type, method, view }) => {
|
||||||
|
if (
|
||||||
|
id === referenceElement.id &&
|
||||||
|
type === 'block' &&
|
||||||
|
method === 'add'
|
||||||
|
) {
|
||||||
|
assertType<FrameBlockComponent>(view);
|
||||||
|
view.showBorder = false;
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
_disposable.add(subscription);
|
||||||
} else if (referenceElement instanceof GfxPrimitiveElementModel) {
|
} else if (referenceElement instanceof GfxPrimitiveElementModel) {
|
||||||
_disposable.add(
|
_disposable.add(
|
||||||
surfaceModel.elementUpdated.subscribe(({ id, oldValues }) => {
|
surface.elementUpdated.subscribe(({ id, oldValues }) => {
|
||||||
if (
|
if (
|
||||||
id === referenceId &&
|
id === referenceId &&
|
||||||
oldValues.xywh !== referenceElement.xywh
|
oldValues.xywh !== referenceElement.xywh
|
||||||
) {
|
) {
|
||||||
setReferenceXYWH(referenceElement.xywh);
|
referenceXYWH$.value = referenceElement.xywh;
|
||||||
refreshViewport();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -497,69 +422,29 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._previewSpec.extend([FrameGroupViewWatcher]);
|
this._previewSpec.extend([SurfaceRefViewportWatcher]);
|
||||||
}
|
|
||||||
|
|
||||||
private _refreshViewport() {
|
|
||||||
if (!this._referenceXYWH) return;
|
|
||||||
|
|
||||||
const previewEditorHost = this.previewEditor;
|
|
||||||
|
|
||||||
if (!previewEditorHost) return;
|
|
||||||
|
|
||||||
const gfx = previewEditorHost.std.get(GfxControllerIdentifier);
|
|
||||||
const viewport = gfx.viewport;
|
|
||||||
|
|
||||||
viewport.setViewportByBound(Bound.deserialize(this._referenceXYWH));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderMask(referencedModel: GfxModel, flavourOrType: string) {
|
|
||||||
const title = 'title' in referencedModel ? referencedModel.title : '';
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="surface-ref-mask">
|
|
||||||
<div class="ref-label">
|
|
||||||
<div class="title">
|
|
||||||
${REF_LABEL_ICON[flavourOrType ?? 'DEFAULT'] ??
|
|
||||||
REF_LABEL_ICON.DEFAULT}
|
|
||||||
<span>${title}</span>
|
|
||||||
</div>
|
|
||||||
<div class="suffix">from edgeless mode</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderRefContent(referencedModel: GfxModel) {
|
private _renderRefContent(referencedModel: GfxModel) {
|
||||||
const [, , w, h] = deserializeXYWH(referencedModel.xywh);
|
const [, , w, h] = deserializeXYWH(referencedModel.xywh);
|
||||||
const flavourOrType =
|
|
||||||
'flavour' in referencedModel
|
|
||||||
? referencedModel.flavour
|
|
||||||
: referencedModel.type;
|
|
||||||
const _previewSpec = this._previewSpec.value;
|
const _previewSpec = this._previewSpec.value;
|
||||||
|
|
||||||
if (!this._viewportEditor) {
|
|
||||||
if (this._previewDoc) {
|
|
||||||
this._viewportEditor = new BlockStdScope({
|
|
||||||
store: this._previewDoc,
|
|
||||||
extensions: _previewSpec,
|
|
||||||
}).render();
|
|
||||||
} else {
|
|
||||||
console.error('Preview doc is not found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`<div class="ref-content">
|
return html`<div class="ref-content">
|
||||||
<div
|
<div
|
||||||
class="ref-viewport ${flavourOrType === 'affine:frame' ? 'frame' : ''}"
|
class="ref-viewport"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
width: `${w}px`,
|
|
||||||
aspectRatio: `${w} / ${h}`,
|
aspectRatio: `${w} / ${h}`,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${this._viewportEditor}
|
${guard(this._previewDoc, () => {
|
||||||
|
return this._previewDoc
|
||||||
|
? new BlockStdScope({
|
||||||
|
store: this._previewDoc,
|
||||||
|
extensions: _previewSpec,
|
||||||
|
}).render()
|
||||||
|
: nothing;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
${this._renderMask(referencedModel, flavourOrType)}
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,7 +477,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
if (!this._shouldRender) return;
|
if (!this._shouldRender) return;
|
||||||
|
|
||||||
this._initHotkey();
|
this._initHotkey();
|
||||||
this._initSpec();
|
this._initViewport();
|
||||||
this._initReferencedModel();
|
this._initReferencedModel();
|
||||||
this._initSelection();
|
this._initSelection();
|
||||||
}
|
}
|
||||||
@@ -600,9 +485,8 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
override render() {
|
override render() {
|
||||||
if (!this._shouldRender) return nothing;
|
if (!this._shouldRender) return nothing;
|
||||||
|
|
||||||
const { _surfaceModel, _referencedModel, model } = this;
|
const { _referencedModel, model } = this;
|
||||||
const isEmpty =
|
const isEmpty = !_referencedModel || !_referencedModel.xywh;
|
||||||
!_surfaceModel || !_referencedModel || !_referencedModel.xywh;
|
|
||||||
const content = isEmpty
|
const content = isEmpty
|
||||||
? this._renderRefPlaceholder(model)
|
? this._renderRefPlaceholder(model)
|
||||||
: this._renderRefContent(_referencedModel);
|
: this._renderRefContent(_referencedModel);
|
||||||
@@ -610,14 +494,12 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="affine-surface-ref"
|
class=${classMap({
|
||||||
|
'affine-surface-ref': true,
|
||||||
|
focused: this._focused,
|
||||||
|
})}
|
||||||
data-theme=${edgelessTheme}
|
data-theme=${edgelessTheme}
|
||||||
@click=${this._focusBlock}
|
@click=${this._focusBlock}
|
||||||
style=${styleMap({
|
|
||||||
outline: this._focused
|
|
||||||
? '2px solid var(--affine-primary-color)'
|
|
||||||
: undefined,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
${content}
|
${content}
|
||||||
</div>
|
</div>
|
||||||
@@ -629,10 +511,10 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewInEdgeless() {
|
viewInEdgeless() {
|
||||||
if (!this._referenceXYWH) return;
|
if (!this._referenceXYWH$.value) return;
|
||||||
|
|
||||||
const viewport = {
|
const viewport = {
|
||||||
xywh: this._referenceXYWH,
|
xywh: this._referenceXYWH$.value,
|
||||||
padding: [60, 20, 20, 20] as [number, number, number, number],
|
padding: [60, 20, 20, 20] as [number, number, number, number],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -640,18 +522,9 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
this.std.get(DocModeProvider).setEditorMode('edgeless');
|
this.std.get(DocModeProvider).setEditorMode('edgeless');
|
||||||
}
|
}
|
||||||
|
|
||||||
override willUpdate(_changedProperties: Map<PropertyKey, unknown>): void {
|
|
||||||
if (_changedProperties.has('_referencedModel')) {
|
|
||||||
this._refreshViewport();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor _focused: boolean = false;
|
private accessor _focused: boolean = false;
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor _surfaceModel: SurfaceBlockModel | null = null;
|
|
||||||
|
|
||||||
@query('affine-surface-ref > block-caption-editor')
|
@query('affine-surface-ref > block-caption-editor')
|
||||||
accessor captionElement!: BlockCaptionEditor;
|
accessor captionElement!: BlockCaptionEditor;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
|
import { type SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
|
||||||
import {
|
import {
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
@@ -19,7 +19,7 @@ export const BUILT_IN_GROUPS: MenuItemGroup<SurfaceRefToolbarContext>[] = [
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'copy',
|
type: 'copy',
|
||||||
label: 'Copy',
|
label: 'Copy as Image',
|
||||||
icon: CopyIcon,
|
icon: CopyIcon,
|
||||||
action: ctx => {
|
action: ctx => {
|
||||||
if (!(ctx.blockComponent.referenceModel && ctx.doc.root?.id)) {
|
if (!(ctx.blockComponent.referenceModel && ctx.doc.root?.id)) {
|
||||||
@@ -56,6 +56,7 @@ export const BUILT_IN_GROUPS: MenuItemGroup<SurfaceRefToolbarContext>[] = [
|
|||||||
ctx.close();
|
ctx.close();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO(@L-Sun): add duplicate action after refactoring toolbar
|
||||||
{
|
{
|
||||||
type: 'download',
|
type: 'download',
|
||||||
label: 'Download',
|
label: 'Download',
|
||||||
|
|||||||
@@ -1,33 +1,44 @@
|
|||||||
import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
|
import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
|
||||||
import { HoverController } from '@blocksuite/affine-components/hover';
|
import { peek } from '@blocksuite/affine-components/peek';
|
||||||
import { CaptionIcon, OpenIcon } from '@blocksuite/affine-components/icons';
|
import { toast } from '@blocksuite/affine-components/toast';
|
||||||
import { isPeekable, peek } from '@blocksuite/affine-components/peek';
|
|
||||||
import {
|
import {
|
||||||
cloneGroups,
|
cloneGroups,
|
||||||
getMoreMenuConfig,
|
getMoreMenuConfig,
|
||||||
type MenuItem,
|
type MenuItem,
|
||||||
type MenuItemGroup,
|
type MenuItemGroup,
|
||||||
renderGroups,
|
renderGroups,
|
||||||
renderToolbarSeparator,
|
|
||||||
} from '@blocksuite/affine-components/toolbar';
|
} from '@blocksuite/affine-components/toolbar';
|
||||||
import type { SurfaceRefBlockModel } from '@blocksuite/affine-model';
|
|
||||||
import { PAGE_HEADER_HEIGHT } from '@blocksuite/affine-shared/consts';
|
|
||||||
import {
|
import {
|
||||||
BlockSelection,
|
FrameBlockModel,
|
||||||
TextSelection,
|
GroupElementModel,
|
||||||
WidgetComponent,
|
MindmapElementModel,
|
||||||
} from '@blocksuite/block-std';
|
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 {
|
import {
|
||||||
ArrowDownSmallIcon,
|
ArrowDownSmallIcon,
|
||||||
|
CaptionIcon,
|
||||||
CenterPeekIcon,
|
CenterPeekIcon,
|
||||||
|
CopyIcon,
|
||||||
EdgelessIcon,
|
EdgelessIcon,
|
||||||
|
FrameIcon,
|
||||||
|
GroupIcon,
|
||||||
|
MindmapIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
|
OpenInNewIcon,
|
||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
import { offset, shift } from '@floating-ui/dom';
|
import { css, html, nothing, type TemplateResult } from 'lit';
|
||||||
import { html, nothing } from 'lit';
|
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { join } from 'lit/directives/join.js';
|
|
||||||
import { repeat } from 'lit/directives/repeat.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 { BUILT_IN_GROUPS } from './config.js';
|
||||||
import { SurfaceRefToolbarContext } from './context.js';
|
import { SurfaceRefToolbarContext } from './context.js';
|
||||||
@@ -38,6 +49,59 @@ export class AffineSurfaceRefToolbar extends WidgetComponent<
|
|||||||
SurfaceRefBlockModel,
|
SurfaceRefBlockModel,
|
||||||
SurfaceRefBlockComponent
|
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.
|
* Caches the more menu items.
|
||||||
* Currently only supports configuring more menu.
|
* Currently only supports configuring more menu.
|
||||||
@@ -45,68 +109,167 @@ export class AffineSurfaceRefToolbar extends WidgetComponent<
|
|||||||
moreGroups: MenuItemGroup<SurfaceRefToolbarContext>[] =
|
moreGroups: MenuItemGroup<SurfaceRefToolbarContext>[] =
|
||||||
cloneGroups(BUILT_IN_GROUPS);
|
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() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
this.moreGroups = getMoreMenuConfig(this.std).configure(this.moreGroups);
|
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;
|
[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>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ export class EditorMenuButton extends WithDisposable(LitElement) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
private _popper!: ReturnType<typeof createButtonPopper>;
|
private _popper: ReturnType<typeof createButtonPopper> | null = null;
|
||||||
|
|
||||||
override firstUpdated() {
|
private _updatePopper() {
|
||||||
|
this._popper?.dispose();
|
||||||
this._popper = createButtonPopper({
|
this._popper = createButtonPopper({
|
||||||
reference: this._trigger,
|
reference: this._trigger,
|
||||||
popperElement: this._content,
|
popperElement: this._content,
|
||||||
@@ -48,16 +49,30 @@ export class EditorMenuButton extends WithDisposable(LitElement) {
|
|||||||
offsetHeight: 6 * 4,
|
offsetHeight: 6 * 4,
|
||||||
...this.popperOptions,
|
...this.popperOptions,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override willUpdate(changedProperties: PropertyValues) {
|
||||||
|
if (changedProperties.has('contentPadding')) {
|
||||||
|
this.style.setProperty('--content-padding', this.contentPadding ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasUpdated && changedProperties.has('popperOptions')) {
|
||||||
|
this._updatePopper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override firstUpdated() {
|
||||||
|
this._updatePopper();
|
||||||
this._disposables.addFromEvent(this, 'keydown', (e: KeyboardEvent) => {
|
this._disposables.addFromEvent(this, 'keydown', (e: KeyboardEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
this._popper.hide();
|
this._popper?.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this._disposables.addFromEvent(this._trigger, 'click', (_: MouseEvent) => {
|
this._disposables.addFromEvent(this._trigger, 'click', (_: MouseEvent) => {
|
||||||
this._popper.toggle();
|
this._popper?.toggle();
|
||||||
});
|
});
|
||||||
this._disposables.add(this._popper);
|
this._disposables.add(() => this._popper?.dispose());
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
@@ -77,12 +92,6 @@ export class EditorMenuButton extends WithDisposable(LitElement) {
|
|||||||
this._popper?.show(force);
|
this._popper?.show(force);
|
||||||
}
|
}
|
||||||
|
|
||||||
override willUpdate(changedProperties: PropertyValues) {
|
|
||||||
if (changedProperties.has('contentPadding')) {
|
|
||||||
this.style.setProperty('--content-padding', this.contentPadding ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@query('editor-menu-content')
|
@query('editor-menu-content')
|
||||||
private accessor _content!: EditorMenuContent;
|
private accessor _content!: EditorMenuContent;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
autoUpdate,
|
autoUpdate,
|
||||||
computePosition,
|
computePosition,
|
||||||
offset,
|
offset,
|
||||||
|
type Placement,
|
||||||
type Rect,
|
type Rect,
|
||||||
shift,
|
shift,
|
||||||
size,
|
size,
|
||||||
@@ -39,6 +40,7 @@ export type ButtonPopperOptions = {
|
|||||||
stateUpdated?: (state: { display: Display }) => void;
|
stateUpdated?: (state: { display: Display }) => void;
|
||||||
mainAxis?: number;
|
mainAxis?: number;
|
||||||
crossAxis?: number;
|
crossAxis?: number;
|
||||||
|
allowedPlacements?: Placement[];
|
||||||
rootBoundary?: Rect | (() => Rect | undefined);
|
rootBoundary?: Rect | (() => Rect | undefined);
|
||||||
ignoreShift?: boolean;
|
ignoreShift?: boolean;
|
||||||
offsetHeight?: number;
|
offsetHeight?: number;
|
||||||
@@ -64,6 +66,7 @@ export function createButtonPopper(options: ButtonPopperOptions) {
|
|||||||
stateUpdated = () => {},
|
stateUpdated = () => {},
|
||||||
mainAxis,
|
mainAxis,
|
||||||
crossAxis,
|
crossAxis,
|
||||||
|
allowedPlacements = ['top', 'bottom'],
|
||||||
rootBoundary,
|
rootBoundary,
|
||||||
ignoreShift,
|
ignoreShift,
|
||||||
offsetHeight,
|
offsetHeight,
|
||||||
@@ -84,7 +87,7 @@ export function createButtonPopper(options: ButtonPopperOptions) {
|
|||||||
crossAxis: crossAxis ?? 0,
|
crossAxis: crossAxis ?? 0,
|
||||||
}),
|
}),
|
||||||
autoPlacement({
|
autoPlacement({
|
||||||
allowedPlacements: ['top', 'bottom'],
|
allowedPlacements,
|
||||||
...overflowOptions,
|
...overflowOptions,
|
||||||
}),
|
}),
|
||||||
shift(overflowOptions),
|
shift(overflowOptions),
|
||||||
|
|||||||
@@ -132,13 +132,13 @@ test('can open peek view for embedded frames', async ({ page }) => {
|
|||||||
await page.locator('affine-surface-ref .affine-surface-ref').hover();
|
await page.locator('affine-surface-ref .affine-surface-ref').hover();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator('.surface-ref-toolbar-container')
|
.locator('affine-surface-ref-toolbar editor-menu-button[aria-label="Open"]')
|
||||||
.locator('editor-icon-button[aria-label="Open doc"]')
|
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator('.surface-ref-toolbar-container')
|
.locator(
|
||||||
.locator('editor-menu-action:has-text("center peek")')
|
'affine-surface-ref-toolbar editor-menu-action[aria-label="Open in center peek"]'
|
||||||
|
)
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
// verify peek view is opened
|
// verify peek view is opened
|
||||||
|
|||||||
Reference in New Issue
Block a user