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

View File

@@ -1,4 +1,5 @@
import type { FrameBlockModel } from '@blocksuite/affine-model'; import type { FrameBlockModel } from '@blocksuite/affine-model';
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
import { SpecProvider } from '@blocksuite/affine-shared/utils'; import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { import {
BlockStdScope, BlockStdScope,
@@ -16,8 +17,6 @@ import { property, query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import debounce from 'lodash-es/debounce'; 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_WIDTH = 280;
const DEFAULT_PREVIEW_CONTAINER_HEIGHT = 166; const DEFAULT_PREVIEW_CONTAINER_HEIGHT = 166;
@@ -118,7 +117,8 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
static override key = 'frame-preview-watcher'; static override key = 'frame-preview-watcher';
override mounted() { override mounted() {
const { view } = this.std; const std = this.std;
const { view } = std;
view.viewUpdated.on(payload => { view.viewUpdated.on(payload => {
if ( if (
payload.type !== 'block' || payload.type !== 'block' ||
@@ -127,16 +127,17 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
) { ) {
return; return;
} }
const edgelessBlock = const viewport = std.get(GfxControllerIdentifier).viewport;
payload.view as EdgelessRootPreviewBlockComponent; viewport.sizeUpdated.once(() => {
edgelessBlock.editorViewportSelector = 'frame-preview-viewport';
edgelessBlock.service.viewport.sizeUpdated.once(() => {
refreshViewport(); refreshViewport();
}); });
}); });
} }
} }
this._previewSpec.extend([FramePreviewWatcher]); this._previewSpec.extend([
ViewportElementExtension('.frame-preview-viewport'),
FramePreviewWatcher,
]);
} }
private _refreshViewport() { private _refreshViewport() {

View File

@@ -20,6 +20,7 @@ import {
EditPropsStore, EditPropsStore,
FontLoaderService, FontLoaderService,
ThemeProvider, ThemeProvider,
ViewportElementProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import type { Viewport } from '@blocksuite/affine-shared/types'; import type { Viewport } from '@blocksuite/affine-shared/types';
import { import {
@@ -39,7 +40,6 @@ import {
type GfxViewportElement, type GfxViewportElement,
} from '@blocksuite/block-std/gfx'; } from '@blocksuite/block-std/gfx';
import { IS_WINDOWS } from '@blocksuite/global/env'; import { IS_WINDOWS } from '@blocksuite/global/env';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import { Bound, Point, Vec } from '@blocksuite/global/gfx'; import { Bound, Point, Vec } from '@blocksuite/global/gfx';
import { effect } from '@preact/signals-core'; import { effect } from '@preact/signals-core';
import { css, html } from 'lit'; import { css, html } from 'lit';
@@ -125,8 +125,6 @@ export class EdgelessRootBlockComponent extends BlockComponent<
private _resizeObserver: ResizeObserver | null = null; private _resizeObserver: ResizeObserver | null = null;
private _viewportElement: HTMLElement | null = null;
clipboardController = new EdgelessClipboardController(this); clipboardController = new EdgelessClipboardController(this);
keyboardManager: EdgelessPageKeyboardManager | null = null; keyboardManager: EdgelessPageKeyboardManager | null = null;
@@ -165,39 +163,11 @@ export class EdgelessRootBlockComponent extends BlockComponent<
* This refers to the wrapper element of the EditorHost. * This refers to the wrapper element of the EditorHost.
*/ */
get viewport(): Viewport { get viewport(): Viewport {
const { return this.std.get(ViewportElementProvider).viewport;
scrollLeft,
scrollTop,
scrollWidth,
scrollHeight,
clientWidth,
clientHeight,
} = this.viewportElement;
const { top, left } = this.viewportElement.getBoundingClientRect();
return {
top,
left,
scrollLeft,
scrollTop,
scrollWidth,
scrollHeight,
clientWidth,
clientHeight,
};
} }
get viewportElement(): HTMLElement { get viewportElement(): HTMLElement {
if (this._viewportElement) return this._viewportElement; return this.std.get(ViewportElementProvider).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;
} }
private _initFontLoader() { private _initFontLoader() {

View File

@@ -3,12 +3,12 @@ import {
type SurfaceBlockComponent, type SurfaceBlockComponent,
type SurfaceBlockModel, type SurfaceBlockModel,
} from '@blocksuite/affine-block-surface'; } from '@blocksuite/affine-block-surface';
import type { EdgelessPreviewer } from '@blocksuite/affine-block-surface-ref';
import type { RootBlockModel } from '@blocksuite/affine-model'; import type { RootBlockModel } from '@blocksuite/affine-model';
import { import {
EditorSettingProvider, EditorSettingProvider,
FontLoaderService, FontLoaderService,
ThemeProvider, ThemeProvider,
ViewportElementProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { requestThrottledConnectedFrame } from '@blocksuite/affine-shared/utils'; import { requestThrottledConnectedFrame } from '@blocksuite/affine-shared/utils';
import { import {
@@ -17,7 +17,6 @@ import {
SurfaceSelection, SurfaceSelection,
} from '@blocksuite/block-std'; } from '@blocksuite/block-std';
import type { GfxViewportElement } from '@blocksuite/block-std/gfx'; import type { GfxViewportElement } from '@blocksuite/block-std/gfx';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import { css, html } from 'lit'; import { css, html } from 'lit';
import { query, state } from 'lit/decorators.js'; import { query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.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 type { EdgelessRootService } from './edgeless-root-service.js';
import { isCanvasElement } from './utils/query.js'; import { isCanvasElement } from './utils/query.js';
export class EdgelessRootPreviewBlockComponent export class EdgelessRootPreviewBlockComponent extends BlockComponent<
extends BlockComponent< RootBlockModel,
RootBlockModel, EdgelessRootService,
EdgelessRootService, EdgelessRootBlockWidgetName
EdgelessRootBlockWidgetName > {
>
implements EdgelessPreviewer
{
static override styles = css` static override styles = css`
affine-edgeless-root-preview { affine-edgeless-root-preview {
pointer-events: none; pointer-events: none;
@@ -87,8 +83,6 @@ export class EdgelessRootPreviewBlockComponent
private _resizeObserver: ResizeObserver | null = null; private _resizeObserver: ResizeObserver | null = null;
private _viewportElement: HTMLElement | null = null;
get dispatcher() { get dispatcher() {
return this.service?.uiEventDispatcher; return this.service?.uiEventDispatcher;
} }
@@ -100,17 +94,7 @@ export class EdgelessRootPreviewBlockComponent
} }
get viewportElement(): HTMLElement { get viewportElement(): HTMLElement {
if (this._viewportElement) return this._viewportElement; return this.std.get(ViewportElementProvider).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;
} }
private _initFontLoader() { private _initFontLoader() {
@@ -159,23 +143,24 @@ export class EdgelessRootPreviewBlockComponent
} }
private _initResizeEffect() { private _initResizeEffect() {
if (!this._viewportElement) {
return;
}
const resizeObserver = new ResizeObserver((_: ResizeObserverEntry[]) => { const resizeObserver = new ResizeObserver((_: ResizeObserverEntry[]) => {
// FIXME: find a better way to get rid of empty check // FIXME: find a better way to get rid of empty check
if (!this.service || !this.service.selection || !this.service.viewport) { if (!this.service || !this.service.selection || !this.service.viewport) {
console.error('Service not ready'); console.error('Service is not ready');
return; return;
} }
this.service.selection.set(this.service.selection.surfaceSelections); this.service.selection.set(this.service.selection.surfaceSelections);
this.service.viewport.onResize(); this.service.viewport.onResize();
}); });
resizeObserver.observe(this.viewportElement); try {
this._resizeObserver?.disconnect(); resizeObserver.observe(this.viewportElement);
this._resizeObserver = resizeObserver; this._resizeObserver?.disconnect();
this._resizeObserver = resizeObserver;
} catch {
// viewport is not ready
console.error('Viewport is not ready');
}
} }
private _initSlotEffects() { private _initSlotEffects() {
@@ -265,18 +250,9 @@ export class EdgelessRootPreviewBlockComponent
`; `;
} }
override willUpdate(_changedProperties: Map<PropertyKey, unknown>): void {
if (_changedProperties.has('editorViewportSelector')) {
this._initResizeEffect();
}
}
@state() @state()
accessor overrideBackground: string | undefined = undefined; accessor overrideBackground: string | undefined = undefined;
@state()
accessor editorViewportSelector = '.affine-edgeless-viewport';
@query('gfx-viewport') @query('gfx-viewport')
accessor gfxViewportElm!: GfxViewportElement; accessor gfxViewportElm!: GfxViewportElement;

View File

@@ -1,3 +1,4 @@
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
import { autoConnectWidget } from '@blocksuite/affine-widget-edgeless-auto-connect'; import { autoConnectWidget } from '@blocksuite/affine-widget-edgeless-auto-connect';
import { frameTitleWidget } from '@blocksuite/affine-widget-frame-title'; import { frameTitleWidget } from '@blocksuite/affine-widget-frame-title';
import { edgelessRemoteSelectionWidget } from '@blocksuite/affine-widget-remote-selection'; import { edgelessRemoteSelectionWidget } from '@blocksuite/affine-widget-remote-selection';
@@ -72,6 +73,7 @@ const EdgelessCommonExtension: ExtensionType[] = [
CommonSpecs, CommonSpecs,
ToolController, ToolController,
EdgelessRootService, EdgelessRootService,
ViewportElementExtension('.affine-edgeless-viewport'),
].flat(); ].flat();
export const EdgelessRootBlockSpec: ExtensionType[] = [ export const EdgelessRootBlockSpec: ExtensionType[] = [

View File

@@ -8,7 +8,10 @@ import {
type RootBlockModel, type RootBlockModel,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text'; 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 type { Viewport } from '@blocksuite/affine-shared/types';
import { import {
focusTitle, focusTitle,
@@ -112,8 +115,6 @@ export class PageRootBlockComponent extends BlockComponent<
} }
`; `;
private _viewportElement: HTMLDivElement | null = null;
clipboardController = new PageClipboard(this); clipboardController = new PageClipboard(this);
/** /**
@@ -182,12 +183,8 @@ export class PageRootBlockComponent extends BlockComponent<
}; };
} }
get viewportElement(): HTMLDivElement | null { get viewportElement(): HTMLElement {
if (this._viewportElement) return this._viewportElement; return this.std.get(ViewportElementProvider).viewportElement;
this._viewportElement = this.host.closest<HTMLDivElement>(
'.affine-page-viewport'
);
return this._viewportElement;
} }
private _createDefaultNoteBlock() { private _createDefaultNoteBlock() {

View File

@@ -1,3 +1,4 @@
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
import { BlockViewExtension, WidgetViewExtension } from '@blocksuite/block-std'; import { BlockViewExtension, WidgetViewExtension } from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store'; import type { ExtensionType } from '@blocksuite/store';
import { literal, unsafeStatic } from 'lit/static-html.js'; import { literal, unsafeStatic } from 'lit/static-html.js';
@@ -23,6 +24,7 @@ const PageCommonExtension: ExtensionType[] = [
CommonSpecs, CommonSpecs,
PageRootService, PageRootService,
pageDraggingAreaWidget, pageDraggingAreaWidget,
ViewportElementExtension('.affine-page-viewport'),
].flat(); ].flat();
export const PageRootBlockSpec: ExtensionType[] = [ export const PageRootBlockSpec: ExtensionType[] = [

View File

@@ -2,5 +2,4 @@ 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 './types.js';
export * from './utils.js'; export * from './utils.js';

View File

@@ -15,6 +15,7 @@ import {
DocModeProvider, DocModeProvider,
EditPropsStore, EditPropsStore,
ThemeProvider, ThemeProvider,
ViewportElementExtension,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { import {
matchModels, matchModels,
@@ -238,7 +239,9 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
private _previewDoc: Store | null = null; 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; private _referencedModel: GfxModel | null = null;

View File

@@ -1,3 +0,0 @@
export interface EdgelessPreviewer {
editorViewportSelector: string;
}

View File

@@ -21,4 +21,5 @@ export * from './telemetry-service';
export * from './theme-service'; export * from './theme-service';
export * from './toolbar-service'; export * from './toolbar-service';
export * from './user-service'; export * from './user-service';
export * from './viewport-element-service';
export * from './virtual-keyboard-service'; export * from './virtual-keyboard-service';

View File

@@ -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,
};
},
};
});
},
};
};

View File

@@ -7,9 +7,9 @@ import {
GfxControllerIdentifier, GfxControllerIdentifier,
type GfxPrimitiveElementModel, type GfxPrimitiveElementModel,
} from '@blocksuite/affine/block-std/gfx'; } from '@blocksuite/affine/block-std/gfx';
import type { EdgelessRootPreviewBlockComponent } from '@blocksuite/affine/blocks/root';
import { EdgelessCRUDIdentifier } from '@blocksuite/affine/blocks/surface'; import { EdgelessCRUDIdentifier } from '@blocksuite/affine/blocks/surface';
import { Bound } from '@blocksuite/affine/global/gfx'; import { Bound } from '@blocksuite/affine/global/gfx';
import { ViewportElementExtension } from '@blocksuite/affine/shared/services';
import { SpecProvider } from '@blocksuite/affine/shared/utils'; import { SpecProvider } from '@blocksuite/affine/shared/utils';
import type { Block, Store } from '@blocksuite/affine/store'; import type { Block, Store } from '@blocksuite/affine/store';
import { useFramework } from '@toeverything/infra'; import { useFramework } from '@toeverything/infra';
@@ -76,7 +76,9 @@ export const EdgelessSnapshot = (props: Props) => {
const editorHost = new BlockStdScope({ const editorHost = new BlockStdScope({
store: doc, store: doc,
extensions: SpecProvider._.getSpec('preview:edgeless').value, extensions: SpecProvider._.getSpec('preview:edgeless').extend([
ViewportElementExtension('.ref-viewport'),
]).value,
}).render(); }).render();
docRef.current = doc; docRef.current = doc;
editorHostRef.current?.remove(); editorHostRef.current?.remove();
@@ -98,9 +100,7 @@ export const EdgelessSnapshot = (props: Props) => {
) { ) {
return; return;
} }
const component = payload.view as EdgelessRootPreviewBlockComponent;
doc.readonly = false; doc.readonly = false;
component.editorViewportSelector = 'ref-viewport';
const frame = getFrameBlock(doc); const frame = getFrameBlock(doc);
if (frame && docName !== 'frame') { if (frame && docName !== 'frame') {
// docName with value 'frame' shouldn't be deleted, it is a part of frame settings // docName with value 'frame' shouldn't be deleted, it is a part of frame settings