refactor: replace editor container with editor host (#10328)

### TL;DR
Refactored editor access to use `EditorHost` instead of `AffineEditorContainer` and updated mode access through `DocModeProvider`.

### What changed?
- Changed editor property types from `AffineEditorContainer` to `EditorHost` across multiple components
- Updated mode access to use `DocModeProvider` service instead of direct editor mode access
- Modified editor references to use `editor.host` where appropriate
- Updated scroll and highlight utilities to work with `EditorHost`

### How to test?
1. Open a document in both page and edgeless modes
2. Verify outline panel functionality works as expected
3. Test outline viewer navigation and highlighting
4. Confirm mobile outline menu operates correctly
5. Check that frame panel and TOC features work in all modes

### Why make this change?
This change improves architectural consistency by using `EditorHost` directly and accessing mode through the proper service provider. This makes the code more maintainable and follows better dependency practices by using the correct abstraction levels.
This commit is contained in:
Saul-Mirone
2025-02-20 14:20:32 +00:00
parent efe36161e8
commit 5ac15f12e6
16 changed files with 80 additions and 71 deletions

View File

@@ -22,7 +22,7 @@ export class CustomOutlinePanel extends WithDisposable(ShadowlessElement) {
private _renderPanel() { private _renderPanel() {
return html`<affine-outline-panel return html`<affine-outline-panel
.editor=${this.editor} .editor=${this.editor.host}
.fitPadding=${[50, 360, 50, 50]} .fitPadding=${[50, 360, 50, 50]}
></affine-outline-panel>`; ></affine-outline-panel>`;
} }

View File

@@ -17,7 +17,7 @@ export class CustomOutlineViewer extends WithDisposable(LitElement) {
private _renderViewer() { private _renderViewer() {
return html`<affine-outline-viewer return html`<affine-outline-viewer
.editor=${this.editor} .editor=${this.editor.host}
.toggleOutlinePanel=${this.toggleOutlinePanel} .toggleOutlinePanel=${this.toggleOutlinePanel}
></affine-outline-viewer>`; ></affine-outline-viewer>`;
} }

View File

@@ -2,6 +2,7 @@ import { effects } from '@blocksuite/affine-block-note/effects';
import { ShadowlessElement, SurfaceSelection } from '@blocksuite/block-std'; import { ShadowlessElement, SurfaceSelection } from '@blocksuite/block-std';
import { import {
changeNoteDisplayMode, changeNoteDisplayMode,
DocModeProvider,
matchModels, matchModels,
NoteBlockModel, NoteBlockModel,
NoteDisplayMode, NoteDisplayMode,
@@ -224,7 +225,9 @@ export class OutlinePanelBody extends SignalWatcher(
private _watchSelectedNotes() { private _watchSelectedNotes() {
return effect(() => { return effect(() => {
const { std, doc, mode } = this.editor; const { std, doc } = this.editor;
const docModeService = this.editor.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
if (mode !== 'edgeless') return; if (mode !== 'edgeless') return;
const currSelectedNotes = std.selection const currSelectedNotes = std.selection

View File

@@ -1,3 +1,4 @@
import type { EditorHost } from '@blocksuite/block-std';
import type { ParagraphBlockModel, Signal } from '@blocksuite/blocks'; import type { ParagraphBlockModel, Signal } from '@blocksuite/blocks';
import { import {
AttachmentIcon, AttachmentIcon,
@@ -22,8 +23,6 @@ import {
import { createContext } from '@lit/context'; import { createContext } from '@lit/context';
import type { TemplateResult } from 'lit'; import type { TemplateResult } from 'lit';
import type { AffineEditorContainer } from '../../editors/editor-container.js';
const _16px = { width: '16px', height: '16px' }; const _16px = { width: '16px', height: '16px' };
const paragraphIconMap: Record< const paragraphIconMap: Record<
@@ -85,7 +84,7 @@ export const headingKeys = new Set(
export const outlineSettingsKey = 'outlinePanelSettings'; export const outlineSettingsKey = 'outlinePanelSettings';
export type TocContext = { export type TocContext = {
editor$: Signal<AffineEditorContainer>; editor$: Signal<EditorHost>;
enableSorting$: Signal<boolean>; enableSorting$: Signal<boolean>;
showIcons$: Signal<boolean>; showIcons$: Signal<boolean>;
fitPadding$: Signal<number[]>; fitPadding$: Signal<number[]>;

View File

@@ -1,6 +1,11 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { PropTypes, requiredProperties } from '@blocksuite/block-std';
import { import {
type EditorHost,
PropTypes,
requiredProperties,
} from '@blocksuite/block-std';
import {
DocModeProvider,
matchModels, matchModels,
NoteDisplayMode, NoteDisplayMode,
ParagraphBlockModel, ParagraphBlockModel,
@@ -14,7 +19,6 @@ import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import type { AffineEditorContainer } from '../../editors/editor-container.js';
import { getHeadingBlocksFromDoc } from './utils/query.js'; import { getHeadingBlocksFromDoc } from './utils/query.js';
import { import {
observeActiveHeadingDuringScroll, observeActiveHeadingDuringScroll,
@@ -162,8 +166,9 @@ export class MobileOutlineMenu extends SignalWatcher(
}; };
override render() { override render() {
if (this.editor.doc.root === null || this.editor.mode === 'edgeless') const docModeService = this.editor.std.get(DocModeProvider);
return nothing; const mode = docModeService.getEditorMode();
if (this.editor.doc.root === null || mode === 'edgeless') return nothing;
const headingBlocks = getHeadingBlocksFromDoc( const headingBlocks = getHeadingBlocksFromDoc(
this.editor.doc, this.editor.doc,
@@ -182,7 +187,7 @@ export class MobileOutlineMenu extends SignalWatcher(
} }
@property({ attribute: false }) @property({ attribute: false })
accessor editor!: AffineEditorContainer; accessor editor!: EditorHost;
} }
declare global { declare global {

View File

@@ -1,15 +1,16 @@
import { import {
type EditorHost,
PropTypes, PropTypes,
requiredProperties, requiredProperties,
ShadowlessElement, ShadowlessElement,
} from '@blocksuite/block-std'; } from '@blocksuite/block-std';
import { DocModeProvider } from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { provide } from '@lit/context'; import { provide } from '@lit/context';
import { effect, signal } from '@preact/signals-core'; import { effect, signal } from '@preact/signals-core';
import { html, type PropertyValues } from 'lit'; import { html, type PropertyValues } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import type { AffineEditorContainer } from '../../editors/editor-container.js';
import { outlineSettingsKey, type TocContext, tocContext } from './config.js'; import { outlineSettingsKey, type TocContext, tocContext } from './config.js';
import * as styles from './outline-panel.css'; import * as styles from './outline-panel.css';
@@ -21,6 +22,12 @@ export const AFFINE_OUTLINE_PANEL = 'affine-outline-panel';
export class OutlinePanel extends SignalWatcher( export class OutlinePanel extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
) { ) {
private _getEditorMode(host: EditorHost) {
const docModeService = host.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode;
}
private _setContext() { private _setContext() {
this._context = { this._context = {
editor$: signal(this.editor), editor$: signal(this.editor),
@@ -39,7 +46,7 @@ export class OutlinePanel extends SignalWatcher(
} }
const editor = this._context.editor$.value; const editor = this._context.editor$.value;
if (editor.mode === 'edgeless') { if (this._getEditorMode(editor) === 'edgeless') {
this._context.enableSorting$.value = true; this._context.enableSorting$.value = true;
} else if (settings) { } else if (settings) {
this._context.enableSorting$.value = settings.enableSorting; this._context.enableSorting$.value = settings.enableSorting;
@@ -51,7 +58,8 @@ export class OutlinePanel extends SignalWatcher(
private _watchSettingsChange() { private _watchSettingsChange() {
this.disposables.add( this.disposables.add(
effect(() => { effect(() => {
if (this._context.editor$.value.mode === 'edgeless') return; if (this._getEditorMode(this._context.editor$.value) === 'edgeless')
return;
const showPreviewIcon = this._context.showIcons$.value; const showPreviewIcon = this._context.showIcons$.value;
const enableNotesSorting = this._context.enableSorting$.value; const enableNotesSorting = this._context.enableSorting$.value;
@@ -84,7 +92,7 @@ export class OutlinePanel extends SignalWatcher(
} }
override render() { override render() {
if (!this.editor.host) return; if (!this.editor) return;
return html` return html`
<affine-outline-panel-header></affine-outline-panel-header> <affine-outline-panel-header></affine-outline-panel-header>
@@ -97,7 +105,7 @@ export class OutlinePanel extends SignalWatcher(
private accessor _context!: TocContext; private accessor _context!: TocContext;
@property({ attribute: false }) @property({ attribute: false })
accessor editor!: AffineEditorContainer; accessor editor!: EditorHost;
@property({ attribute: false }) @property({ attribute: false })
accessor fitPadding!: number[]; accessor fitPadding!: number[];

View File

@@ -1,9 +1,14 @@
import { import {
type EditorHost,
PropTypes, PropTypes,
requiredProperties, requiredProperties,
ShadowlessElement, ShadowlessElement,
} from '@blocksuite/block-std'; } from '@blocksuite/block-std';
import { NoteDisplayMode, scrollbarStyle } from '@blocksuite/blocks'; import {
DocModeProvider,
NoteDisplayMode,
scrollbarStyle,
} from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { TocIcon } from '@blocksuite/icons/lit'; import { TocIcon } from '@blocksuite/icons/lit';
import { provide } from '@lit/context'; import { provide } from '@lit/context';
@@ -13,7 +18,6 @@ import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import type { AffineEditorContainer } from '../../editors/editor-container.js';
import { type TocContext, tocContext } from './config.js'; import { type TocContext, tocContext } from './config.js';
import { getHeadingBlocksFromDoc } from './utils/query.js'; import { getHeadingBlocksFromDoc } from './utils/query.js';
import { import {
@@ -219,8 +223,9 @@ export class OutlineViewer extends SignalWatcher(
} }
override render() { override render() {
if (this.editor.doc.root === null || this.editor.mode === 'edgeless') const docModeService = this.editor.std.get(DocModeProvider);
return nothing; const mode = docModeService.getEditorMode();
if (this.editor.doc.root === null || mode === 'edgeless') return nothing;
const headingBlocks = getHeadingBlocksFromDoc( const headingBlocks = getHeadingBlocksFromDoc(
this.editor.doc, this.editor.doc,
@@ -308,7 +313,7 @@ export class OutlineViewer extends SignalWatcher(
private accessor _showViewer: boolean = false; private accessor _showViewer: boolean = false;
@property({ attribute: false }) @property({ attribute: false })
accessor editor!: AffineEditorContainer; accessor editor!: EditorHost;
@property({ attribute: false }) @property({ attribute: false })
accessor toggleOutlinePanel: (() => void) | null = null; accessor toggleOutlinePanel: (() => void) | null = null;

View File

@@ -1,13 +1,17 @@
import type { EditorHost } from '@blocksuite/block-std'; import type { EditorHost } from '@blocksuite/block-std';
import { getDocTitleByEditorHost, NoteDisplayMode } from '@blocksuite/blocks'; import {
DocModeProvider,
getDocTitleByEditorHost,
NoteDisplayMode,
} from '@blocksuite/blocks';
import { clamp, DisposableGroup } from '@blocksuite/global/utils'; import { clamp, DisposableGroup } from '@blocksuite/global/utils';
import type { AffineEditorContainer } from '../../../editors/editor-container.js';
import { getHeadingBlocksFromDoc } from './query.js'; import { getHeadingBlocksFromDoc } from './query.js';
export function scrollToBlock(editor: AffineEditorContainer, blockId: string) { export function scrollToBlock(host: EditorHost, blockId: string) {
const { host, mode } = editor; const docModeService = host.std.get(DocModeProvider);
if (mode === 'edgeless' || !host) return; const mode = docModeService.getEditorMode();
if (mode === 'edgeless') return;
if (editor.doc.root?.id === blockId) { if (editor.doc.root?.id === blockId) {
const docTitle = getDocTitleByEditorHost(host); const docTitle = getDocTitleByEditorHost(host);
@@ -49,12 +53,11 @@ export function isBlockBeforeViewportCenter(
} }
export const observeActiveHeadingDuringScroll = ( export const observeActiveHeadingDuringScroll = (
getEditor: () => AffineEditorContainer, // workaround for editor changed getEditor: () => EditorHost, // workaround for editor changed
update: (activeHeading: string | null) => void update: (activeHeading: string | null) => void
) => { ) => {
const handler = () => { const handler = () => {
const { host } = getEditor(); const host = getEditor();
if (!host) return;
const headings = getHeadingBlocksFromDoc( const headings = getHeadingBlocksFromDoc(
host.doc, host.doc,
@@ -81,13 +84,10 @@ export const observeActiveHeadingDuringScroll = (
let highlightMask: HTMLDivElement | null = null; let highlightMask: HTMLDivElement | null = null;
let highlightTimeoutId: ReturnType<typeof setTimeout> | null = null; let highlightTimeoutId: ReturnType<typeof setTimeout> | null = null;
function highlightBlock(editor: AffineEditorContainer, blockId: string) { function highlightBlock(host: EditorHost, blockId: string) {
const emptyClear = () => {}; const emptyClear = () => {};
const { host } = editor; if (host.doc.root?.id === blockId) return emptyClear;
if (!host) return emptyClear;
if (editor.doc.root?.id === blockId) return emptyClear;
const rootComponent = host.querySelector('affine-page-root'); const rootComponent = host.querySelector('affine-page-root');
if (!rootComponent) return emptyClear; if (!rootComponent) return emptyClear;
@@ -152,11 +152,11 @@ function highlightBlock(editor: AffineEditorContainer, blockId: string) {
// this function is useful when the scroll need smooth animation // this function is useful when the scroll need smooth animation
let highlightIntervalId: ReturnType<typeof setInterval> | null = null; let highlightIntervalId: ReturnType<typeof setInterval> | null = null;
export async function scrollToBlockWithHighlight( export async function scrollToBlockWithHighlight(
editor: AffineEditorContainer, host: EditorHost,
blockId: string, blockId: string,
timeout = 3000 timeout = 3000
) { ) {
scrollToBlock(editor, blockId); scrollToBlock(host, blockId);
let timeCount = 0; let timeCount = 0;
@@ -173,10 +173,9 @@ export async function scrollToBlockWithHighlight(
return; return;
} }
const { host } = editor; const block = host.view.getBlock(blockId);
const block = host?.view.getBlock(blockId);
if (!host || !block || timeCount > timeout) { if (!block || timeCount > timeout) {
clearInterval(highlightIntervalId); clearInterval(highlightIntervalId);
resolve(() => {}); resolve(() => {});
return; return;
@@ -194,7 +193,7 @@ export async function scrollToBlockWithHighlight(
clearInterval(highlightIntervalId); clearInterval(highlightIntervalId);
// highlight block // highlight block
resolve(highlightBlock(editor, blockId)); resolve(highlightBlock(host, blockId));
}, 100); }, 100);
}); });
} }

View File

@@ -1,4 +1,4 @@
import type { AffineEditorContainer } from '@blocksuite/affine/presets'; import type { EditorHost } from '@blocksuite/affine/block-std';
import { OutlineViewer } from '@blocksuite/affine/presets'; import { OutlineViewer } from '@blocksuite/affine/presets';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
@@ -9,7 +9,7 @@ export const EditorOutlineViewer = ({
show, show,
openOutlinePanel, openOutlinePanel,
}: { }: {
editor: AffineEditorContainer | null; editor: EditorHost | null;
show: boolean; show: boolean;
openOutlinePanel?: () => void; openOutlinePanel?: () => void;
}) => { }) => {

View File

@@ -310,7 +310,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
/> />
</Scrollable.Root> </Scrollable.Root>
<EditorOutlineViewer <EditorOutlineViewer
editor={editorContainer} editor={editorContainer?.host ?? null}
show={mode === 'page' && !isSideBarOpen} show={mode === 'page' && !isSideBarOpen}
openOutlinePanel={openOutlinePanel} openOutlinePanel={openOutlinePanel}
/> />
@@ -350,7 +350,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
<ViewSidebarTab tabId="outline" icon={<TocIcon />}> <ViewSidebarTab tabId="outline" icon={<TocIcon />}>
<Scrollable.Root className={styles.sidebarScrollArea}> <Scrollable.Root className={styles.sidebarScrollArea}>
<Scrollable.Viewport> <Scrollable.Viewport>
<EditorOutlinePanel editor={editorContainer} /> <EditorOutlinePanel editor={editorContainer?.host ?? null} />
</Scrollable.Viewport> </Scrollable.Viewport>
<Scrollable.Scrollbar /> <Scrollable.Scrollbar />
</Scrollable.Root> </Scrollable.Root>
@@ -359,7 +359,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
<ViewSidebarTab tabId="frame" icon={<FrameIcon />}> <ViewSidebarTab tabId="frame" icon={<FrameIcon />}>
<Scrollable.Root className={styles.sidebarScrollArea}> <Scrollable.Root className={styles.sidebarScrollArea}>
<Scrollable.Viewport> <Scrollable.Viewport>
<EditorFramePanel editor={editorContainer} /> <EditorFramePanel editor={editorContainer?.host ?? null} />
</Scrollable.Viewport> </Scrollable.Viewport>
<Scrollable.Scrollbar /> <Scrollable.Scrollbar />
</Scrollable.Root> </Scrollable.Root>

View File

@@ -1,34 +1,30 @@
import type { EditorHost } from '@blocksuite/affine/block-std';
import { FramePanel } from '@blocksuite/affine/blocks'; import { FramePanel } from '@blocksuite/affine/blocks';
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import * as styles from './frame.css'; import * as styles from './frame.css';
// A wrapper for FramePanel // A wrapper for FramePanel
export const EditorFramePanel = ({ export const EditorFramePanel = ({ editor }: { editor: EditorHost | null }) => {
editor,
}: {
editor: AffineEditorContainer | null;
}) => {
const framePanelRef = useRef<FramePanel | null>(null); const framePanelRef = useRef<FramePanel | null>(null);
const onRefChange = useCallback( const onRefChange = useCallback(
(container: HTMLDivElement | null) => { (container: HTMLDivElement | null) => {
if (editor?.host && container && container.children.length === 0) { if (editor && container && container.children.length === 0) {
framePanelRef.current = new FramePanel(); framePanelRef.current = new FramePanel();
framePanelRef.current.host = editor.host; framePanelRef.current.host = editor;
framePanelRef.current.fitPadding = [20, 20, 20, 20]; framePanelRef.current.fitPadding = [20, 20, 20, 20];
container.append(framePanelRef.current); container.append(framePanelRef.current);
} }
}, },
[editor?.host] [editor]
); );
useEffect(() => { useEffect(() => {
if (editor?.host && framePanelRef.current) { if (editor && framePanelRef.current) {
framePanelRef.current.host = editor.host; framePanelRef.current.host = editor;
} }
}, [editor?.host]); }, [editor]);
return <div className={styles.root} ref={onRefChange} />; return <div className={styles.root} ref={onRefChange} />;
}; };

View File

@@ -1,4 +1,4 @@
import type { AffineEditorContainer } from '@blocksuite/affine/presets'; import type { EditorHost } from '@blocksuite/affine/block-std';
import { OutlinePanel } from '@blocksuite/affine/presets'; import { OutlinePanel } from '@blocksuite/affine/presets';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
@@ -8,7 +8,7 @@ import * as styles from './outline.css';
export const EditorOutlinePanel = ({ export const EditorOutlinePanel = ({
editor, editor,
}: { }: {
editor: AffineEditorContainer | null; editor: EditorHost | null;
}) => { }) => {
const outlinePanelRef = useRef<OutlinePanel | null>(null); const outlinePanelRef = useRef<OutlinePanel | null>(null);

View File

@@ -268,7 +268,7 @@ const SharePageInner = ({
<Scrollable.Scrollbar /> <Scrollable.Scrollbar />
</Scrollable.Root> </Scrollable.Root>
<EditorOutlineViewer <EditorOutlineViewer
editor={editorContainer} editor={editorContainer?.host ?? null}
show={publishMode === 'page'} show={publishMode === 'page'}
/> />
{!BUILD_CONFIG.isElectron && <SharePageFooter />} {!BUILD_CONFIG.isElectron && <SharePageFooter />}

View File

@@ -1,14 +1,8 @@
import { import type { EditorHost } from '@blocksuite/affine/block-std';
type AffineEditorContainer, import { MobileOutlineMenu } from '@blocksuite/affine/presets';
MobileOutlineMenu,
} from '@blocksuite/affine/presets';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
export const MobileTocMenu = ({ export const MobileTocMenu = ({ editor }: { editor: EditorHost | null }) => {
editor,
}: {
editor: AffineEditorContainer | null;
}) => {
const outlineMenuRef = useRef<MobileOutlineMenu | null>(null); const outlineMenuRef = useRef<MobileOutlineMenu | null>(null);
const onRefChange = useCallback((container: HTMLDivElement | null) => { const onRefChange = useCallback((container: HTMLDivElement | null) => {
if (container) { if (container) {

View File

@@ -127,7 +127,7 @@ export const PageHeaderMenuButton = () => {
title={t['com.affine.header.menu.toc']()} title={t['com.affine.header.menu.toc']()}
items={ items={
<div className={styles.outlinePanel}> <div className={styles.outlinePanel}>
<MobileTocMenu editor={editorContainer} /> <MobileTocMenu editor={editorContainer?.host ?? null} />
</div> </div>
} }
> >

View File

@@ -179,7 +179,7 @@ function DocPeekPreviewEditor({
</Scrollable.Root> </Scrollable.Root>
{!BUILD_CONFIG.isMobileEdition && !BUILD_CONFIG.isMobileWeb ? ( {!BUILD_CONFIG.isMobileEdition && !BUILD_CONFIG.isMobileWeb ? (
<EditorOutlineViewer <EditorOutlineViewer
editor={editorElement} editor={editorElement?.host ?? null}
show={mode === 'page'} show={mode === 'page'}
openOutlinePanel={openOutlinePanel} openOutlinePanel={openOutlinePanel}
/> />