feat(editor): add viewport element service (#10727)

This commit is contained in:
Saul-Mirone
2025-03-10 04:26:18 +00:00
parent 7ab3b695dc
commit 4dd5f2ffb0
12 changed files with 107 additions and 99 deletions
@@ -1,4 +1,5 @@
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import {
BlockStdScope,
@@ -16,8 +17,6 @@ import { property, query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import debounce from 'lodash-es/debounce';
import type { EdgelessRootPreviewBlockComponent } from '../../edgeless-root-preview-block.js';
const DEFAULT_PREVIEW_CONTAINER_WIDTH = 280;
const DEFAULT_PREVIEW_CONTAINER_HEIGHT = 166;
@@ -118,7 +117,8 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
static override key = 'frame-preview-watcher';
override mounted() {
const { view } = this.std;
const std = this.std;
const { view } = std;
view.viewUpdated.on(payload => {
if (
payload.type !== 'block' ||
@@ -127,16 +127,17 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
) {
return;
}
const edgelessBlock =
payload.view as EdgelessRootPreviewBlockComponent;
edgelessBlock.editorViewportSelector = 'frame-preview-viewport';
edgelessBlock.service.viewport.sizeUpdated.once(() => {
const viewport = std.get(GfxControllerIdentifier).viewport;
viewport.sizeUpdated.once(() => {
refreshViewport();
});
});
}
}
this._previewSpec.extend([FramePreviewWatcher]);
this._previewSpec.extend([
ViewportElementExtension('.frame-preview-viewport'),
FramePreviewWatcher,
]);
}
private _refreshViewport() {
@@ -20,6 +20,7 @@ import {
EditPropsStore,
FontLoaderService,
ThemeProvider,
ViewportElementProvider,
} from '@blocksuite/affine-shared/services';
import type { Viewport } from '@blocksuite/affine-shared/types';
import {
@@ -39,7 +40,6 @@ import {
type GfxViewportElement,
} from '@blocksuite/block-std/gfx';
import { IS_WINDOWS } from '@blocksuite/global/env';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import { Bound, Point, Vec } from '@blocksuite/global/gfx';
import { effect } from '@preact/signals-core';
import { css, html } from 'lit';
@@ -125,8 +125,6 @@ export class EdgelessRootBlockComponent extends BlockComponent<
private _resizeObserver: ResizeObserver | null = null;
private _viewportElement: HTMLElement | null = null;
clipboardController = new EdgelessClipboardController(this);
keyboardManager: EdgelessPageKeyboardManager | null = null;
@@ -165,39 +163,11 @@ export class EdgelessRootBlockComponent extends BlockComponent<
* This refers to the wrapper element of the EditorHost.
*/
get viewport(): Viewport {
const {
scrollLeft,
scrollTop,
scrollWidth,
scrollHeight,
clientWidth,
clientHeight,
} = this.viewportElement;
const { top, left } = this.viewportElement.getBoundingClientRect();
return {
top,
left,
scrollLeft,
scrollTop,
scrollWidth,
scrollHeight,
clientWidth,
clientHeight,
};
return this.std.get(ViewportElementProvider).viewport;
}
get viewportElement(): HTMLElement {
if (this._viewportElement) return this._viewportElement;
this._viewportElement = this.host.closest(
'.affine-edgeless-viewport'
) as HTMLElement | null;
if (!this._viewportElement) {
throw new BlockSuiteError(
BlockSuiteError.ErrorCode.ValueNotExists,
'EdgelessRootBlockComponent.viewportElement: viewport element is not found'
);
}
return this._viewportElement;
return this.std.get(ViewportElementProvider).viewportElement;
}
private _initFontLoader() {
@@ -3,12 +3,12 @@ import {
type SurfaceBlockComponent,
type SurfaceBlockModel,
} from '@blocksuite/affine-block-surface';
import type { EdgelessPreviewer } from '@blocksuite/affine-block-surface-ref';
import type { RootBlockModel } from '@blocksuite/affine-model';
import {
EditorSettingProvider,
FontLoaderService,
ThemeProvider,
ViewportElementProvider,
} from '@blocksuite/affine-shared/services';
import { requestThrottledConnectedFrame } from '@blocksuite/affine-shared/utils';
import {
@@ -17,7 +17,6 @@ import {
SurfaceSelection,
} from '@blocksuite/block-std';
import type { GfxViewportElement } from '@blocksuite/block-std/gfx';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import { css, html } from 'lit';
import { query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
@@ -26,14 +25,11 @@ import type { EdgelessRootBlockWidgetName } from '../types.js';
import type { EdgelessRootService } from './edgeless-root-service.js';
import { isCanvasElement } from './utils/query.js';
export class EdgelessRootPreviewBlockComponent
extends BlockComponent<
RootBlockModel,
EdgelessRootService,
EdgelessRootBlockWidgetName
>
implements EdgelessPreviewer
{
export class EdgelessRootPreviewBlockComponent extends BlockComponent<
RootBlockModel,
EdgelessRootService,
EdgelessRootBlockWidgetName
> {
static override styles = css`
affine-edgeless-root-preview {
pointer-events: none;
@@ -87,8 +83,6 @@ export class EdgelessRootPreviewBlockComponent
private _resizeObserver: ResizeObserver | null = null;
private _viewportElement: HTMLElement | null = null;
get dispatcher() {
return this.service?.uiEventDispatcher;
}
@@ -100,17 +94,7 @@ export class EdgelessRootPreviewBlockComponent
}
get viewportElement(): HTMLElement {
if (this._viewportElement) return this._viewportElement;
this._viewportElement = this.host.closest(
this.editorViewportSelector
) as HTMLElement | null;
if (!this._viewportElement) {
throw new BlockSuiteError(
BlockSuiteError.ErrorCode.ValueNotExists,
'EdgelessRootPreviewBlockComponent.viewportElement: viewport element is not found'
);
}
return this._viewportElement;
return this.std.get(ViewportElementProvider).viewportElement;
}
private _initFontLoader() {
@@ -159,23 +143,24 @@ export class EdgelessRootPreviewBlockComponent
}
private _initResizeEffect() {
if (!this._viewportElement) {
return;
}
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 not ready');
console.error('Service is not ready');
return;
}
this.service.selection.set(this.service.selection.surfaceSelections);
this.service.viewport.onResize();
});
resizeObserver.observe(this.viewportElement);
this._resizeObserver?.disconnect();
this._resizeObserver = resizeObserver;
try {
resizeObserver.observe(this.viewportElement);
this._resizeObserver?.disconnect();
this._resizeObserver = resizeObserver;
} catch {
// viewport is not ready
console.error('Viewport is not ready');
}
}
private _initSlotEffects() {
@@ -265,18 +250,9 @@ export class EdgelessRootPreviewBlockComponent
`;
}
override willUpdate(_changedProperties: Map<PropertyKey, unknown>): void {
if (_changedProperties.has('editorViewportSelector')) {
this._initResizeEffect();
}
}
@state()
accessor overrideBackground: string | undefined = undefined;
@state()
accessor editorViewportSelector = '.affine-edgeless-viewport';
@query('gfx-viewport')
accessor gfxViewportElm!: GfxViewportElement;
@@ -1,3 +1,4 @@
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
import { autoConnectWidget } from '@blocksuite/affine-widget-edgeless-auto-connect';
import { frameTitleWidget } from '@blocksuite/affine-widget-frame-title';
import { edgelessRemoteSelectionWidget } from '@blocksuite/affine-widget-remote-selection';
@@ -72,6 +73,7 @@ const EdgelessCommonExtension: ExtensionType[] = [
CommonSpecs,
ToolController,
EdgelessRootService,
ViewportElementExtension('.affine-edgeless-viewport'),
].flat();
export const EdgelessRootBlockSpec: ExtensionType[] = [
@@ -8,7 +8,10 @@ import {
type RootBlockModel,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { PageViewportService } from '@blocksuite/affine-shared/services';
import {
PageViewportService,
ViewportElementProvider,
} from '@blocksuite/affine-shared/services';
import type { Viewport } from '@blocksuite/affine-shared/types';
import {
focusTitle,
@@ -112,8 +115,6 @@ export class PageRootBlockComponent extends BlockComponent<
}
`;
private _viewportElement: HTMLDivElement | null = null;
clipboardController = new PageClipboard(this);
/**
@@ -182,12 +183,8 @@ export class PageRootBlockComponent extends BlockComponent<
};
}
get viewportElement(): HTMLDivElement | null {
if (this._viewportElement) return this._viewportElement;
this._viewportElement = this.host.closest<HTMLDivElement>(
'.affine-page-viewport'
);
return this._viewportElement;
get viewportElement(): HTMLElement {
return this.std.get(ViewportElementProvider).viewportElement;
}
private _createDefaultNoteBlock() {
@@ -1,3 +1,4 @@
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
import { BlockViewExtension, WidgetViewExtension } from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import { literal, unsafeStatic } from 'lit/static-html.js';
@@ -23,6 +24,7 @@ const PageCommonExtension: ExtensionType[] = [
CommonSpecs,
PageRootService,
pageDraggingAreaWidget,
ViewportElementExtension('.affine-page-viewport'),
].flat();
export const PageRootBlockSpec: ExtensionType[] = [
@@ -2,5 +2,4 @@ export * from './commands.js';
export * from './surface-ref-block.js';
export * from './surface-ref-block-edgeless.js';
export * from './surface-ref-spec.js';
export * from './types.js';
export * from './utils.js';
@@ -15,6 +15,7 @@ import {
DocModeProvider,
EditPropsStore,
ThemeProvider,
ViewportElementExtension,
} from '@blocksuite/affine-shared/services';
import {
matchModels,
@@ -238,7 +239,9 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
private _previewDoc: Store | null = null;
private readonly _previewSpec = SpecProvider._.getSpec('preview:edgeless');
private readonly _previewSpec = SpecProvider._.getSpec(
'preview:edgeless'
).extend([ViewportElementExtension('.ref-viewport')]);
private _referencedModel: GfxModel | null = null;
@@ -1,3 +0,0 @@
export interface EdgelessPreviewer {
editorViewportSelector: string;
}
@@ -21,4 +21,5 @@ export * from './telemetry-service';
export * from './theme-service';
export * from './toolbar-service';
export * from './user-service';
export * from './viewport-element-service';
export * from './virtual-keyboard-service';
@@ -0,0 +1,60 @@
import { createIdentifier } from '@blocksuite/global/di';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import type { ExtensionType } from '@blocksuite/store';
import type { Viewport } from '../types';
export interface ViewportElementService {
get viewportElement(): HTMLElement;
get viewport(): Viewport;
}
export const ViewportElementProvider = createIdentifier<ViewportElementService>(
'ViewportElementProvider'
);
export const ViewportElementExtension = (selector: string): ExtensionType => {
return {
setup: di => {
di.override(ViewportElementProvider, () => {
const getViewportElement = (): HTMLElement => {
const viewportElement = document.querySelector<HTMLElement>(selector);
if (!viewportElement) {
throw new BlockSuiteError(
BlockSuiteError.ErrorCode.ValueNotExists,
`ViewportElementProvider: viewport element is not found`
);
}
return viewportElement;
};
return {
get viewportElement() {
return getViewportElement();
},
get viewport() {
const viewportElement = getViewportElement();
const {
scrollLeft,
scrollTop,
scrollWidth,
scrollHeight,
clientWidth,
clientHeight,
} = viewportElement;
const { top, left } = viewportElement.getBoundingClientRect();
return {
top,
left,
scrollLeft,
scrollTop,
scrollWidth,
scrollHeight,
clientWidth,
clientHeight,
};
},
};
});
},
};
};
@@ -7,9 +7,9 @@ import {
GfxControllerIdentifier,
type GfxPrimitiveElementModel,
} from '@blocksuite/affine/block-std/gfx';
import type { EdgelessRootPreviewBlockComponent } from '@blocksuite/affine/blocks/root';
import { EdgelessCRUDIdentifier } from '@blocksuite/affine/blocks/surface';
import { Bound } from '@blocksuite/affine/global/gfx';
import { ViewportElementExtension } from '@blocksuite/affine/shared/services';
import { SpecProvider } from '@blocksuite/affine/shared/utils';
import type { Block, Store } from '@blocksuite/affine/store';
import { useFramework } from '@toeverything/infra';
@@ -76,7 +76,9 @@ export const EdgelessSnapshot = (props: Props) => {
const editorHost = new BlockStdScope({
store: doc,
extensions: SpecProvider._.getSpec('preview:edgeless').value,
extensions: SpecProvider._.getSpec('preview:edgeless').extend([
ViewportElementExtension('.ref-viewport'),
]).value,
}).render();
docRef.current = doc;
editorHostRef.current?.remove();
@@ -98,9 +100,7 @@ export const EdgelessSnapshot = (props: Props) => {
) {
return;
}
const component = payload.view as EdgelessRootPreviewBlockComponent;
doc.readonly = false;
component.editorViewportSelector = 'ref-viewport';
const frame = getFrameBlock(doc);
if (frame && docName !== 'frame') {
// docName with value 'frame' shouldn't be deleted, it is a part of frame settings