refactor(editor): move frame related component to frame panel (#10735)

This commit is contained in:
Saul-Mirone
2025-03-10 05:45:18 +00:00
parent 6b0639facd
commit 36cf973372
13 changed files with 64 additions and 54 deletions
@@ -1,244 +0,0 @@
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import {
BlockStdScope,
type EditorHost,
LifeCycleWatcher,
ShadowlessElement,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, deserializeXYWH } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
import { DisposableGroup } from '@blocksuite/global/slot';
import { type Query, type Store } from '@blocksuite/store';
import { css, html, nothing, type PropertyValues } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { guard } from 'lit/directives/guard.js';
import { styleMap } from 'lit/directives/style-map.js';
import debounce from 'lodash-es/debounce';
const DEFAULT_PREVIEW_CONTAINER_WIDTH = 280;
const DEFAULT_PREVIEW_CONTAINER_HEIGHT = 166;
const styles = css`
.frame-preview-container {
display: block;
width: 100%;
height: 100%;
box-sizing: border-box;
position: relative;
}
.frame-preview-surface-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
overflow: hidden;
}
.frame-preview-viewport {
max-width: 100%;
box-sizing: border-box;
margin: 0 auto;
position: relative;
overflow: hidden;
pointer-events: none;
user-select: none;
.edgeless-background {
background-color: transparent;
background-image: none;
}
}
`;
export class FramePreview extends WithDisposable(ShadowlessElement) {
static override styles = styles;
private readonly _clearFrameDisposables = () => {
this._frameDisposables?.dispose();
this._frameDisposables = null;
};
private readonly _docFilter: Query = {
mode: 'loose',
match: [
{
flavour: 'affine:frame',
viewType: 'hidden',
},
],
};
private _frameDisposables: DisposableGroup | null = null;
private _previewDoc: Store | null = null;
private readonly _previewSpec = SpecProvider._.getSpec('preview:edgeless');
private readonly _updateFrameViewportWH = () => {
const [, , w, h] = deserializeXYWH(this.frame.xywh);
let scale = 1;
if (this.fillScreen) {
scale = Math.max(this.surfaceWidth / w, this.surfaceHeight / h);
} else {
scale = Math.min(this.surfaceWidth / w, this.surfaceHeight / h);
}
this.frameViewportWH = {
width: w * scale,
height: h * scale,
};
};
get _originalDoc() {
return this.frame.doc;
}
private _initPreviewDoc() {
this._previewDoc = this._originalDoc.workspace.getDoc(
this._originalDoc.id,
{
query: this._docFilter,
readonly: true,
}
);
this.disposables.add(() => {
this._originalDoc.doc.clearQuery(this._docFilter);
});
}
private _initSpec() {
const refreshViewport = this._refreshViewport.bind(this);
class FramePreviewWatcher extends LifeCycleWatcher {
static override key = 'frame-preview-watcher';
override mounted() {
const std = this.std;
const { view } = std;
view.viewUpdated.on(payload => {
if (
payload.type !== 'block' ||
payload.method !== 'add' ||
payload.view.model.flavour !== 'affine:page'
) {
return;
}
const viewport = std.get(GfxControllerIdentifier).viewport;
viewport.sizeUpdated.once(() => {
refreshViewport();
});
});
}
}
this._previewSpec.extend([
ViewportElementExtension('.frame-preview-viewport'),
FramePreviewWatcher,
]);
}
private _refreshViewport() {
const previewEditorHost = this.previewEditor;
if (!previewEditorHost) return;
const { viewport } = previewEditorHost.std.get(GfxControllerIdentifier);
const frameBound = Bound.deserialize(this.frame.xywh);
viewport.setViewportByBound(frameBound);
}
private _renderSurfaceContent() {
const { width, height } = this.frameViewportWH;
const _previewSpec = this._previewSpec.value;
return html`<div
class="frame-preview-surface-container"
style=${styleMap({
width: `${this.surfaceWidth}px`,
height: `${this.surfaceHeight}px`,
})}
>
<div
class="frame-preview-viewport"
style=${styleMap({
width: `${width}px`,
height: `${height}px`,
})}
>
${guard([this._previewDoc, this.frame], () => {
if (!this._previewDoc || !this.frame) return nothing;
return new BlockStdScope({
store: this._previewDoc,
extensions: _previewSpec,
}).render();
})}
</div>
</div>`;
}
private _setFrameDisposables(frame: FrameBlockModel) {
this._clearFrameDisposables();
this._frameDisposables = new DisposableGroup();
this._frameDisposables.add(
frame.propsUpdated.on(
debounce(this._updateFrameViewportWH, 10, { leading: true })
)
);
}
override connectedCallback() {
super.connectedCallback();
this._initSpec();
this._initPreviewDoc();
this._updateFrameViewportWH();
this._setFrameDisposables(this.frame);
}
override disconnectedCallback() {
super.disconnectedCallback();
this._clearFrameDisposables();
}
override render() {
const { frame } = this;
const noContent = !frame || !frame.xywh;
return html`<div class="frame-preview-container">
${noContent ? nothing : this._renderSurfaceContent()}
</div>`;
}
override updated(_changedProperties: PropertyValues) {
if (_changedProperties.has('frame')) {
this._setFrameDisposables(this.frame);
}
if (_changedProperties.has('frameViewportWH')) {
this._refreshViewport();
}
}
@state()
accessor fillScreen = false;
@property({ attribute: false })
accessor frame!: FrameBlockModel;
@state()
accessor frameViewportWH = {
width: 0,
height: 0,
};
@query('editor-host')
accessor previewEditor: EditorHost | null = null;
@property({ attribute: false })
accessor surfaceHeight: number = DEFAULT_PREVIEW_CONTAINER_HEIGHT;
@property({ attribute: false })
accessor surfaceWidth: number = DEFAULT_PREVIEW_CONTAINER_WIDTH;
}
@@ -1,6 +1,7 @@
import {
EditPropsStore,
ThemeProvider,
ViewportElementProvider,
} from '@blocksuite/affine-shared/services';
import { Bound } from '@blocksuite/global/gfx';
import {
@@ -240,7 +241,8 @@ export class EdgelessDraggableElementController<T>
// Cannot get edgeless.host.getBoundingClientRect().width in Safari (Always 0)
const edgelessRect = edgeless.host.getBoundingClientRect();
if (edgelessRect.width === 0) {
edgelessRect.width = edgeless.viewport.clientWidth;
const { viewport } = edgeless.std.get(ViewportElementProvider);
edgelessRect.width = viewport.clientWidth;
}
this.info = {
@@ -6,6 +6,7 @@ import type {
import {
EditPropsStore,
ThemeProvider,
ViewportElementProvider,
} from '@blocksuite/affine-shared/services';
import type { GfxToolsFullOptionValue } from '@blocksuite/block-std/gfx';
import type { Bound } from '@blocksuite/global/gfx';
@@ -309,7 +310,8 @@ export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin(
this.setEdgelessTool({ type: 'empty' });
const icon = this.mindmapElement;
const { x, y } = service.gfx.tool.lastMousePos$.peek();
const { left, top } = this.edgeless.viewport;
const { viewport } = this.edgeless.std.get(ViewportElementProvider);
const { left, top } = viewport;
const clientPos = { x: x + left, y: y + top };
this.draggableController.clickToDrag(icon, clientPos);
},
@@ -11,6 +11,7 @@ import {
EditPropsStore,
TelemetryProvider,
ThemeProvider,
ViewportElementProvider,
} from '@blocksuite/affine-shared/services';
import { SignalWatcher } from '@blocksuite/global/lit';
import { css, html, LitElement, nothing } from 'lit';
@@ -253,7 +254,8 @@ export class EdgelessToolbarShapeDraggable extends EdgelessToolbarToolMixin(
return;
}
const { x, y } = service.gfx.tool.lastMousePos$.peek();
const { left, top } = this.edgeless.viewport;
const { viewport } = this.edgeless.std.get(ViewportElementProvider);
const { left, top } = viewport;
const clientPos = { x: x + left, y: y + top };
this.draggableController.clickToDrag(el, clientPos);
},
@@ -22,7 +22,6 @@ import {
ThemeProvider,
ViewportElementProvider,
} from '@blocksuite/affine-shared/services';
import type { Viewport } from '@blocksuite/affine-shared/types';
import {
isTouchPadPinchEvent,
matchModels,
@@ -158,14 +157,6 @@ export class EdgelessRootBlockComponent extends BlockComponent<
) as SurfaceBlockModel;
}
/**
* Don't confuse with `gfx.viewport` which is edgeless-only concept.
* This refers to the wrapper element of the EditorHost.
*/
get viewport(): Viewport {
return this.std.get(ViewportElementProvider).viewport;
}
get viewportElement(): HTMLElement {
return this.std.get(ViewportElementProvider).viewportElement;
}
@@ -1,4 +1,5 @@
import {
EdgelessCRUDIdentifier,
getBgGridGap,
type SurfaceBlockComponent,
type SurfaceBlockModel,
@@ -16,7 +17,10 @@ import {
type GfxBlockComponent,
SurfaceSelection,
} from '@blocksuite/block-std';
import type { GfxViewportElement } from '@blocksuite/block-std/gfx';
import {
GfxControllerIdentifier,
type GfxViewportElement,
} from '@blocksuite/block-std/gfx';
import { css, html } from 'lit';
import { query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
@@ -87,6 +91,10 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
return this.service?.uiEventDispatcher;
}
private get _gfx() {
return this.std.get(GfxControllerIdentifier);
}
get surfaceBlockModel() {
return this.model.children.find(
child => child.flavour === 'affine:surface'
@@ -118,7 +126,7 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
});
this._disposables.add(
this.service.layer.slots.layerUpdated.on(() => updateLayers())
this._gfx.layer.slots.layerUpdated.on(() => updateLayers())
);
}
@@ -127,7 +135,7 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
const onPixelRatioChange = () => {
if (media) {
this.service.viewport.onResize();
this._gfx.viewport.onResize();
media.removeEventListener('change', onPixelRatioChange);
}
@@ -144,13 +152,8 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
private _initResizeEffect() {
const resizeObserver = new ResizeObserver((_: ResizeObserverEntry[]) => {
// FIXME: find a better way to get rid of empty check
if (!this.service || !this.service.selection || !this.service.viewport) {
console.error('Service is not ready');
return;
}
this.service.selection.set(this.service.selection.surfaceSelections);
this.service.viewport.onResize();
this._gfx.selection.set(this._gfx.selection.surfaceSelections);
this._gfx.viewport.onResize();
});
try {
@@ -177,6 +180,10 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
return editorSetting?.peek().edgelessDisableScheduleUpdate ?? false;
}
private get _crud() {
return this.std.get(EdgelessCRUDIdentifier);
}
override connectedCallback() {
super.connectedCallback();
@@ -186,7 +193,7 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
);
if (!surface) return;
const el = this.service.crud.getElementById(surface.elements[0]);
const el = this._crud.getElementById(surface.elements[0]);
if (isCanvasElement(el)) {
return true;
}
@@ -212,7 +219,7 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
this._initLayerUpdateEffect();
this._disposables.add(
this.service.viewport.viewportUpdated.on(() => {
this._gfx.viewport.viewportUpdated.on(() => {
this._refreshLayerViewport();
})
);
@@ -229,10 +236,10 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
<div class="edgeless-background edgeless-container" style=${background}>
<gfx-viewport
.enableChildrenSchedule=${!this._disableScheduleUpdate}
.viewport=${this.service.viewport}
.viewport=${this._gfx.viewport}
.getModelsInViewport=${() => {
const blocks = this.service.gfx.grid.search(
this.service.viewport.viewportBounds,
const blocks = this._gfx.grid.search(
this._gfx.viewport.viewportBounds,
{
useSet: true,
filter: ['block'],
@@ -15,10 +15,10 @@ import type { ExtensionType } from '@blocksuite/store';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { CommonSpecs } from '../common-specs/index.js';
import { edgelessNavigatorBgWidget } from '../widgets/edgeless-navigator-bg/index.js';
import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from '../widgets/edgeless-zoom-toolbar/index.js';
import { EDGELESS_ELEMENT_TOOLBAR_WIDGET } from '../widgets/element-toolbar/index.js';
import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js';
import { EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET } from './components/presentation/edgeless-navigator-black-background.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_TOOLBAR_WIDGET } from './components/toolbar/edgeless-toolbar.js';
@@ -44,11 +44,6 @@ export const noteSlicerWidget = WidgetViewExtension(
NOTE_SLICER_WIDGET,
literal`${unsafeStatic(NOTE_SLICER_WIDGET)}`
);
export const edgelessNavigatorBlackBackgroundWidget = WidgetViewExtension(
'affine:page',
EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET,
literal`${unsafeStatic(EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET)}`
);
export const edgelessSelectedRectWidget = WidgetViewExtension(
'affine:page',
EDGELESS_SELECTED_RECT_WIDGET,
@@ -86,7 +81,7 @@ export const EdgelessRootBlockSpec: ExtensionType[] = [
autoConnectWidget,
edgelessDraggingAreaWidget,
noteSlicerWidget,
edgelessNavigatorBlackBackgroundWidget,
edgelessNavigatorBgWidget,
edgelessSelectedRectWidget,
edgelessToolbarWidget,
];
@@ -1,4 +1,3 @@
export { FramePreview } from './components/frame/frame-preview.js';
export { EdgelessTemplatePanel } from './components/toolbar/template/template-panel.js';
export * from './components/toolbar/template/template-type.js';
export * from './edgeless-root-block.js';
@@ -24,10 +24,6 @@ import { EdgelessShapePanel } from './edgeless/components/panel/shape-panel.js';
import { EdgelessShapeStylePanel } from './edgeless/components/panel/shape-style-panel.js';
import { EdgelessSizePanel } from './edgeless/components/panel/size-panel.js';
import { StrokeStylePanel } from './edgeless/components/panel/stroke-style-panel.js';
import {
EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET,
EdgelessNavigatorBlackBackgroundWidget,
} from './edgeless/components/presentation/edgeless-navigator-black-background.js';
import {
EDGELESS_DRAGGING_AREA_WIDGET,
EdgelessDraggingAreaRectWidget,
@@ -80,10 +76,13 @@ import {
EDGELESS_TOOLBAR_WIDGET,
EdgelessRootBlockComponent,
EdgelessRootPreviewBlockComponent,
FramePreview,
PageRootBlockComponent,
PreviewRootBlockComponent,
} from './index.js';
import {
EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET,
EdgelessNavigatorBlackBackgroundWidget,
} from './widgets/edgeless-navigator-bg/index.js';
import {
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
AffineEdgelessZoomToolbarWidget,
@@ -268,7 +267,6 @@ function registerMiscComponents() {
// Loading and preview components
customElements.define('loader-element', Loader);
customElements.define('frame-preview', FramePreview);
customElements.define('affine-template-loading', AffineTemplateLoading);
// Toolbar and UI components
@@ -327,7 +325,6 @@ declare global {
'edgeless-tool-icon-button': EdgelessToolIconButton;
'edgeless-toolbar-button': EdgelessToolbarButton;
'edgeless-connector-handle': EdgelessConnectorHandle;
'frame-preview': FramePreview;
'note-slicer': NoteSlicer;
'edgeless-align-panel': EdgelessAlignPanel;
'card-style-panel': CardStylePanel;
@@ -1,20 +1,17 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import type { FrameBlockModel, RootBlockModel } from '@blocksuite/affine-model';
import { EditPropsStore } from '@blocksuite/affine-shared/services';
import { WidgetComponent } from '@blocksuite/block-std';
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound } from '@blocksuite/global/gfx';
import { effect } from '@preact/signals-core';
import { css, html, nothing } from 'lit';
import { state } from 'lit/decorators.js';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
export const EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET =
'edgeless-navigator-black-background';
export class EdgelessNavigatorBlackBackgroundWidget extends WidgetComponent<
RootBlockModel,
EdgelessRootBlockComponent
> {
export class EdgelessNavigatorBlackBackgroundWidget extends WidgetComponent<RootBlockModel> {
static override styles = css`
.edgeless-navigator-black-background {
background-color: black;
@@ -31,6 +28,10 @@ export class EdgelessNavigatorBlackBackgroundWidget extends WidgetComponent<
return this.std.get(GfxControllerIdentifier);
}
private get _slots() {
return this.std.get(EdgelessLegacySlotIdentifier);
}
private _tryLoadBlackBackground() {
const value = this.std
.get(EditPropsStore)
@@ -39,15 +40,15 @@ export class EdgelessNavigatorBlackBackgroundWidget extends WidgetComponent<
}
override firstUpdated() {
const { _disposables, gfx, block } = this;
const { _disposables, gfx } = this;
_disposables.add(
block.slots.navigatorFrameChanged.on(frame => {
this._slots.navigatorFrameChanged.on(frame => {
this.frame = frame;
})
);
_disposables.add(
block.slots.navigatorSettingUpdated.on(({ blackBackground }) => {
this._slots.navigatorSettingUpdated.on(({ blackBackground }) => {
if (blackBackground !== undefined) {
this.std
.get(EditPropsStore)
@@ -57,7 +58,7 @@ export class EdgelessNavigatorBlackBackgroundWidget extends WidgetComponent<
this.show =
blackBackground &&
block.gfx.tool.currentToolOption$.peek().type === 'frameNavigator';
this.gfx.tool.currentToolOption$.peek().type === 'frameNavigator';
}
})
);
@@ -75,7 +76,7 @@ export class EdgelessNavigatorBlackBackgroundWidget extends WidgetComponent<
);
_disposables.add(
block.slots.fullScreenToggled.on(
this._slots.fullScreenToggled.on(
() =>
setTimeout(() => {
this.requestUpdate();
@@ -114,3 +115,9 @@ export class EdgelessNavigatorBlackBackgroundWidget extends WidgetComponent<
@state()
private accessor show = false;
}
export const edgelessNavigatorBgWidget = WidgetViewExtension(
'affine:page',
EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET,
literal`${unsafeStatic(EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET)}`
);