refactor(core): refactor editor query string selector (#8058)
The editor selector is the information for locating a block, which can automatically focus on a certain content when a user opens a document.
```
export type EditorSelector = {
blockIds?: string[];
elementIds?: string[];
};
```
The selector can be set from multiple places, such as passing it in the center peek parameter, or passing it in the query part of the URL.
This pr decoupled the selector from the query string and now available at `editorService.editor.selector$`
This commit is contained in:
@@ -23,6 +23,8 @@ import {
|
|||||||
throttleTime,
|
throttleTime,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { shallowEqual } from '../utils/shallow-equal';
|
||||||
|
|
||||||
const logger = new DebugLogger('livedata');
|
const logger = new DebugLogger('livedata');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -334,6 +336,32 @@ export class LiveData<T = unknown>
|
|||||||
return sub$;
|
return sub$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* same as map, but do shallow equal check before emit
|
||||||
|
*/
|
||||||
|
selector<R>(selector: (v: T) => R): LiveData<R> {
|
||||||
|
const sub$ = LiveData.from(
|
||||||
|
new Observable<R>(subscriber => {
|
||||||
|
let last: any = undefined;
|
||||||
|
return this.subscribe({
|
||||||
|
next: v => {
|
||||||
|
const data = selector(v);
|
||||||
|
if (!shallowEqual(last, data)) {
|
||||||
|
subscriber.next(data);
|
||||||
|
}
|
||||||
|
last = data;
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
sub$.complete();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
undefined as R // is safe
|
||||||
|
);
|
||||||
|
|
||||||
|
return sub$;
|
||||||
|
}
|
||||||
|
|
||||||
distinctUntilChanged(comparator?: (previous: T, current: T) => boolean) {
|
distinctUntilChanged(comparator?: (previous: T, current: T) => boolean) {
|
||||||
return LiveData.from(
|
return LiveData.from(
|
||||||
this.pipe(distinctUntilChanged(comparator)),
|
this.pipe(distinctUntilChanged(comparator)),
|
||||||
|
|||||||
34
packages/common/infra/src/utils/shallow-equal.ts
Normal file
34
packages/common/infra/src/utils/shallow-equal.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// credit: https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/shallowEqual.js
|
||||||
|
export function shallowEqual(objA: any, objB: any) {
|
||||||
|
if (Object.is(objA, objB)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof objA !== 'object' ||
|
||||||
|
objA === null ||
|
||||||
|
typeof objB !== 'object' ||
|
||||||
|
objB === null
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysA = Object.keys(objA);
|
||||||
|
const keysB = Object.keys(objB);
|
||||||
|
|
||||||
|
if (keysA.length !== keysB.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for A's keys different from B.
|
||||||
|
for (const key of keysA) {
|
||||||
|
if (
|
||||||
|
!Object.prototype.hasOwnProperty.call(objB, key) ||
|
||||||
|
!Object.is(objA[key], objB[key])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import type { EditorSelector } from '@affine/core/modules/editor';
|
||||||
import type { ReferenceInfo } from '@blocksuite/affine-model';
|
import type { ReferenceInfo } from '@blocksuite/affine-model';
|
||||||
import { DocMode } from '@blocksuite/blocks';
|
import { DocMode } from '@blocksuite/blocks';
|
||||||
|
import type { InlineEditor } from '@blocksuite/inline/inline-editor';
|
||||||
import type {
|
import type {
|
||||||
AffineEditorContainer,
|
AffineEditorContainer,
|
||||||
|
DocTitle,
|
||||||
EdgelessEditor,
|
EdgelessEditor,
|
||||||
PageEditor,
|
PageEditor,
|
||||||
} from '@blocksuite/presets';
|
} from '@blocksuite/presets';
|
||||||
@@ -10,6 +13,7 @@ import clsx from 'clsx';
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -42,8 +46,7 @@ interface BlocksuiteEditorContainerProps {
|
|||||||
shared?: boolean;
|
shared?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
blockIds?: string[];
|
defaultEditorSelector?: EditorSelector;
|
||||||
elementIds?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mimic the interface of the webcomponent and expose slots & host
|
// mimic the interface of the webcomponent and expose slots & host
|
||||||
@@ -57,14 +60,24 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
AffineEditorContainer,
|
AffineEditorContainer,
|
||||||
BlocksuiteEditorContainerProps
|
BlocksuiteEditorContainerProps
|
||||||
>(function AffineEditorContainer(
|
>(function AffineEditorContainer(
|
||||||
{ page, mode, className, style, shared, blockIds, elementIds },
|
{ page, mode, className, style, shared, defaultEditorSelector },
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const scrolledRef = useRef(false);
|
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
const docRef = useRef<PageEditor>(null);
|
const docRef = useRef<PageEditor>(null);
|
||||||
|
const docTitleRef = useRef<DocTitle>(null);
|
||||||
const edgelessRef = useRef<EdgelessEditor>(null);
|
const edgelessRef = useRef<EdgelessEditor>(null);
|
||||||
const [anchor, setAnchor] = useState<string | null>(null);
|
const [anchor] = useState<string | null>(() => {
|
||||||
|
if (
|
||||||
|
mode === DocMode.Edgeless &&
|
||||||
|
defaultEditorSelector?.elementIds?.length
|
||||||
|
) {
|
||||||
|
return defaultEditorSelector.elementIds[0];
|
||||||
|
} else if (defaultEditorSelector?.blockIds?.length) {
|
||||||
|
return defaultEditorSelector.blockIds[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const slots: BlocksuiteEditorContainerRef['slots'] = useMemo(() => {
|
const slots: BlocksuiteEditorContainerRef['slots'] = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@@ -75,14 +88,6 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mode === DocMode.Edgeless && elementIds?.length) {
|
|
||||||
setAnchor(elementIds[0]);
|
|
||||||
} else if (blockIds?.length) {
|
|
||||||
setAnchor(blockIds[0]);
|
|
||||||
}
|
|
||||||
}, [blockIds, elementIds, mode]);
|
|
||||||
|
|
||||||
// forward the slot to the webcomponent
|
// forward the slot to the webcomponent
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -162,7 +167,7 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
}) as unknown as AffineEditorContainer;
|
}) as unknown as AffineEditorContainer & { origin: HTMLDivElement };
|
||||||
|
|
||||||
return proxy;
|
return proxy;
|
||||||
}, [mode, page, slots]);
|
}, [mode, page, slots]);
|
||||||
@@ -177,31 +182,49 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
}
|
}
|
||||||
}, [affineEditorContainerProxy, ref]);
|
}, [affineEditorContainerProxy, ref]);
|
||||||
|
|
||||||
// `scrolledRef` should be updated if blockElement is changed
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrolledRef.current = false;
|
if (anchor) {
|
||||||
}, [anchor]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!anchor) return;
|
|
||||||
|
|
||||||
let canceled = false;
|
let canceled = false;
|
||||||
affineEditorContainerProxy.updateComplete
|
affineEditorContainerProxy.updateComplete
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!scrolledRef.current && !canceled) {
|
if (!canceled) {
|
||||||
const std = affineEditorContainerProxy.host?.std;
|
const std = affineEditorContainerProxy.host?.std;
|
||||||
if (std) {
|
if (std) {
|
||||||
scrollAnchoring(std, mode, anchor);
|
scrollAnchoring(std, mode, anchor);
|
||||||
}
|
}
|
||||||
scrolledRef.current = true;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
return () => {
|
return () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
// if no anchor, focus the title
|
||||||
|
let canceled = false;
|
||||||
|
|
||||||
|
affineEditorContainerProxy.updateComplete
|
||||||
|
.then(() => {
|
||||||
|
if (!canceled) {
|
||||||
|
const title = docTitleRef.current?.querySelector<
|
||||||
|
HTMLElement & { inlineEditor: InlineEditor }
|
||||||
|
>('rich-text');
|
||||||
|
title?.inlineEditor.focusEnd();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
return () => {
|
||||||
|
canceled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
}, [anchor, affineEditorContainerProxy, mode]);
|
}, [anchor, affineEditorContainerProxy, mode]);
|
||||||
|
|
||||||
|
const handleClickPageModeBlank = useCallback(() => {
|
||||||
|
affineEditorContainerProxy.host?.std.command.exec(
|
||||||
|
'appendParagraph' as never,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}, [affineEditorContainerProxy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid={`editor-${page.id}`}
|
data-testid={`editor-${page.id}`}
|
||||||
@@ -215,7 +238,13 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
>
|
>
|
||||||
{mode === 'page' ? (
|
{mode === 'page' ? (
|
||||||
<BlocksuiteDocEditor shared={shared} page={page} ref={docRef} />
|
<BlocksuiteDocEditor
|
||||||
|
shared={shared}
|
||||||
|
page={page}
|
||||||
|
ref={docRef}
|
||||||
|
titleRef={docTitleRef}
|
||||||
|
onClickBlank={handleClickPageModeBlank}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<BlocksuiteEdgelessEditor
|
<BlocksuiteEdgelessEditor
|
||||||
shared={shared}
|
shared={shared}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
||||||
|
import type { EditorSelector } from '@affine/core/modules/editor';
|
||||||
import type { DocMode } from '@blocksuite/blocks';
|
import type { DocMode } from '@blocksuite/blocks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
@@ -29,8 +30,7 @@ export type EditorProps = {
|
|||||||
onLoadEditor?: (editor: AffineEditorContainer) => () => void;
|
onLoadEditor?: (editor: AffineEditorContainer) => () => void;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
blockIds?: string[];
|
defaultEditorSelector?: EditorSelector;
|
||||||
elementIds?: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function usePageRoot(page: Doc) {
|
function usePageRoot(page: Doc) {
|
||||||
@@ -64,8 +64,7 @@ const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
|
|||||||
onLoadEditor,
|
onLoadEditor,
|
||||||
shared,
|
shared,
|
||||||
style,
|
style,
|
||||||
blockIds,
|
defaultEditorSelector,
|
||||||
elementIds,
|
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
@@ -116,8 +115,7 @@ const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
|
|||||||
ref={onRefChange}
|
ref={onRefChange}
|
||||||
className={className}
|
className={className}
|
||||||
style={style}
|
style={style}
|
||||||
blockIds={blockIds}
|
defaultEditorSelector={defaultEditorSelector}
|
||||||
elementIds={elementIds}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||||
import { EditorSettingService } from '@affine/core/modules/editor-settting';
|
import { EditorSettingService } from '@affine/core/modules/editor-settting';
|
||||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
|
||||||
import { DocMode } from '@blocksuite/blocks';
|
import { DocMode } from '@blocksuite/blocks';
|
||||||
import { DocTitle, EdgelessEditor, PageEditor } from '@blocksuite/presets';
|
import { DocTitle, EdgelessEditor, PageEditor } from '@blocksuite/presets';
|
||||||
import type { Doc } from '@blocksuite/store';
|
import type { Doc } from '@blocksuite/store';
|
||||||
@@ -24,7 +23,6 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { PagePropertiesTable } from '../../affine/page-properties';
|
import { PagePropertiesTable } from '../../affine/page-properties';
|
||||||
@@ -146,18 +144,18 @@ const usePatchSpecs = (page: Doc, shared: boolean, mode: DocMode) => {
|
|||||||
|
|
||||||
export const BlocksuiteDocEditor = forwardRef<
|
export const BlocksuiteDocEditor = forwardRef<
|
||||||
PageEditor,
|
PageEditor,
|
||||||
BlocksuiteEditorProps
|
BlocksuiteEditorProps & {
|
||||||
>(function BlocksuiteDocEditor({ page, shared }, ref) {
|
onClickBlank?: () => void;
|
||||||
const titleRef = useRef<DocTitle>(null);
|
titleRef?: React.Ref<DocTitle>;
|
||||||
|
}
|
||||||
|
>(function BlocksuiteDocEditor(
|
||||||
|
{ page, shared, onClickBlank, titleRef: externalTitleRef },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const titleRef = useRef<DocTitle | null>(null);
|
||||||
const docRef = useRef<PageEditor | null>(null);
|
const docRef = useRef<PageEditor | null>(null);
|
||||||
const [docPage, setDocPage] =
|
|
||||||
useState<HTMLElementTagNameMap['affine-page-root']>();
|
|
||||||
const { isJournal } = useJournalInfoHelper(page.collection, page.id);
|
const { isJournal } = useJournalInfoHelper(page.collection, page.id);
|
||||||
|
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
|
||||||
const activeView = useLiveData(workbench.activeView$);
|
|
||||||
const hash = useLiveData(activeView.location$).hash;
|
|
||||||
|
|
||||||
const editorSettingService = useService(EditorSettingService);
|
const editorSettingService = useService(EditorSettingService);
|
||||||
|
|
||||||
const onDocRef = useCallback(
|
const onDocRef = useCallback(
|
||||||
@@ -174,22 +172,19 @@ export const BlocksuiteDocEditor = forwardRef<
|
|||||||
[ref]
|
[ref]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const onTitleRef = useCallback(
|
||||||
// auto focus the title
|
(el: DocTitle) => {
|
||||||
setTimeout(() => {
|
titleRef.current = el;
|
||||||
const docPage = docRef.current?.querySelector('affine-page-root');
|
if (externalTitleRef) {
|
||||||
if (docPage) {
|
if (typeof externalTitleRef === 'function') {
|
||||||
setDocPage(docPage);
|
externalTitleRef(el);
|
||||||
}
|
|
||||||
if (titleRef.current && !hash) {
|
|
||||||
const richText = titleRef.current.querySelector('rich-text');
|
|
||||||
richText?.inlineEditor?.focusEnd();
|
|
||||||
} else {
|
} else {
|
||||||
docPage?.focusFirstParagraph();
|
(externalTitleRef as any).current = el;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
},
|
||||||
}, []);
|
[externalTitleRef]
|
||||||
|
);
|
||||||
|
|
||||||
const [specs, portals] = usePatchSpecs(page, !!shared, DocMode.Page);
|
const [specs, portals] = usePatchSpecs(page, !!shared, DocMode.Page);
|
||||||
|
|
||||||
@@ -199,7 +194,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
|||||||
<>
|
<>
|
||||||
<div className={styles.affineDocViewport} style={{ height: '100%' }}>
|
<div className={styles.affineDocViewport} style={{ height: '100%' }}>
|
||||||
{!isJournal ? (
|
{!isJournal ? (
|
||||||
<adapted.DocTitle doc={page} ref={titleRef} />
|
<adapted.DocTitle doc={page} ref={onTitleRef} />
|
||||||
) : (
|
) : (
|
||||||
<BlocksuiteEditorJournalDocTitle page={page} />
|
<BlocksuiteEditorJournalDocTitle page={page} />
|
||||||
)}
|
)}
|
||||||
@@ -211,14 +206,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
|||||||
specs={specs}
|
specs={specs}
|
||||||
hasViewport={false}
|
hasViewport={false}
|
||||||
/>
|
/>
|
||||||
{docPage && !page.readonly ? (
|
<div className={styles.docEditorGap} onClick={onClickBlank}></div>
|
||||||
<div
|
|
||||||
className={styles.docEditorGap}
|
|
||||||
onClick={() => {
|
|
||||||
docPage.std.command.exec('appendParagraph' as never, {});
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
) : null}
|
|
||||||
{!shared && settings.displayBiDirectionalLink ? (
|
{!shared && settings.displayBiDirectionalLink ? (
|
||||||
<BiDirectionalLinkPanel />
|
<BiDirectionalLinkPanel />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
import './page-detail-editor.css';
|
import './page-detail-editor.css';
|
||||||
|
|
||||||
import { useDocCollectionPage } from '@affine/core/hooks/use-block-suite-workspace-page';
|
import { useDocCollectionPage } from '@affine/core/hooks/use-block-suite-workspace-page';
|
||||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
|
||||||
import type { DocMode } from '@blocksuite/blocks';
|
import type { DocMode } from '@blocksuite/blocks';
|
||||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import type { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store';
|
import type { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store';
|
||||||
import {
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
useLiveData,
|
|
||||||
useService,
|
|
||||||
useServiceOptional,
|
|
||||||
} from '@toeverything/infra';
|
|
||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo, Suspense, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { EditorService } from '../modules/editor';
|
import { type EditorSelector, EditorService } from '../modules/editor';
|
||||||
import {
|
import {
|
||||||
EditorSettingService,
|
EditorSettingService,
|
||||||
fontStyleOptions,
|
fontStyleOptions,
|
||||||
@@ -40,35 +35,26 @@ export interface PageDetailEditorProps {
|
|||||||
docCollection: DocCollection;
|
docCollection: DocCollection;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
onLoad?: OnLoadEditor;
|
onLoad?: OnLoadEditor;
|
||||||
|
defaultEditorSelector?: EditorSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
||||||
page,
|
page,
|
||||||
onLoad,
|
onLoad,
|
||||||
|
defaultEditorSelector,
|
||||||
}: PageDetailEditorProps & { page: BlockSuiteDoc }) {
|
}: PageDetailEditorProps & { page: BlockSuiteDoc }) {
|
||||||
const viewService = useServiceOptional(ViewService);
|
|
||||||
const params = useLiveData(
|
|
||||||
viewService?.view.queryString$<{
|
|
||||||
mode?: string;
|
|
||||||
blockIds?: string[];
|
|
||||||
elementIds?: string[];
|
|
||||||
}>({
|
|
||||||
// Cannot handle single id situation correctly: `blockIds=xxx`
|
|
||||||
arrayFormat: 'none',
|
|
||||||
types: {
|
|
||||||
mode: 'string',
|
|
||||||
blockIds: value => (value.length ? value.split(',') : []),
|
|
||||||
elementIds: value => (value.length ? value.split(',') : []),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const editor = useService(EditorService).editor;
|
const editor = useService(EditorService).editor;
|
||||||
const mode = useLiveData(editor.mode$);
|
const mode = useLiveData(editor.mode$);
|
||||||
|
|
||||||
const isSharedMode = editor.isSharedMode;
|
const isSharedMode = editor.isSharedMode;
|
||||||
const editorSetting = useService(EditorSettingService).editorSetting;
|
const editorSetting = useService(EditorSettingService).editorSetting;
|
||||||
const settings = useLiveData(editorSetting.settings$);
|
const settings = useLiveData(
|
||||||
|
editorSetting.settings$.selector(s => ({
|
||||||
|
fontFamily: s.fontFamily,
|
||||||
|
customFontFamily: s.customFontFamily,
|
||||||
|
fullWidthLayout: s.fullWidthLayout,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
const fontStyle = fontStyleOptions.find(
|
const fontStyle = fontStyleOptions.find(
|
||||||
@@ -122,8 +108,7 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
|||||||
mode={mode}
|
mode={mode}
|
||||||
page={page}
|
page={page}
|
||||||
shared={isSharedMode}
|
shared={isSharedMode}
|
||||||
blockIds={params?.blockIds}
|
defaultEditorSelector={defaultEditorSelector}
|
||||||
elementIds={params?.elementIds}
|
|
||||||
onLoadEditor={onLoadEditor}
|
onLoadEditor={onLoadEditor}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -135,9 +120,5 @@ export const PageDetailEditor = (props: PageDetailEditorProps) => {
|
|||||||
if (!page) {
|
if (!page) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return <PageDetailEditorMain {...props} page={page} />;
|
||||||
<Suspense>
|
|
||||||
<PageDetailEditorMain {...props} page={page} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,13 +4,20 @@ import type { DocService, WorkspaceService } from '@toeverything/infra';
|
|||||||
import { Entity, LiveData } from '@toeverything/infra';
|
import { Entity, LiveData } from '@toeverything/infra';
|
||||||
|
|
||||||
import { EditorScope } from '../scopes/editor';
|
import { EditorScope } from '../scopes/editor';
|
||||||
|
import type { EditorSelector } from '../types';
|
||||||
|
|
||||||
export class Editor extends Entity<{ defaultMode: DocMode }> {
|
export class Editor extends Entity<{
|
||||||
|
defaultMode: DocMode;
|
||||||
|
defaultEditorSelector?: EditorSelector;
|
||||||
|
}> {
|
||||||
readonly scope = this.framework.createScope(EditorScope, {
|
readonly scope = this.framework.createScope(EditorScope, {
|
||||||
editor: this as Editor,
|
editor: this as Editor,
|
||||||
});
|
});
|
||||||
|
|
||||||
readonly mode$ = new LiveData(this.props.defaultMode);
|
readonly mode$ = new LiveData(this.props.defaultMode);
|
||||||
|
readonly selector$ = new LiveData<EditorSelector | undefined>(
|
||||||
|
this.props.defaultEditorSelector
|
||||||
|
);
|
||||||
readonly doc = this.docService.doc;
|
readonly doc = this.docService.doc;
|
||||||
readonly isSharedMode =
|
readonly isSharedMode =
|
||||||
this.workspaceService.workspace.openOptions.isSharedMode;
|
this.workspaceService.workspace.openOptions.isSharedMode;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export { Editor } from './entities/editor';
|
|||||||
export { EditorScope } from './scopes/editor';
|
export { EditorScope } from './scopes/editor';
|
||||||
export { EditorService } from './services/editor';
|
export { EditorService } from './services/editor';
|
||||||
export { EditorsService } from './services/editors';
|
export { EditorsService } from './services/editors';
|
||||||
|
export type { EditorSelector } from './types';
|
||||||
|
|
||||||
export function configureEditorModule(framework: Framework) {
|
export function configureEditorModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import type { DocMode } from '@blocksuite/blocks';
|
|||||||
import { Service } from '@toeverything/infra';
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
import { Editor } from '../entities/editor';
|
import { Editor } from '../entities/editor';
|
||||||
|
import type { EditorSelector } from '../types';
|
||||||
|
|
||||||
export class EditorsService extends Service {
|
export class EditorsService extends Service {
|
||||||
createEditor(defaultMode: DocMode) {
|
createEditor(defaultMode: DocMode, defaultEditorSelector?: EditorSelector) {
|
||||||
return this.framework.createEntity(Editor, { defaultMode });
|
return this.framework.createEntity(Editor, {
|
||||||
|
defaultMode,
|
||||||
|
defaultEditorSelector,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
packages/frontend/core/src/modules/editor/types.ts
Normal file
4
packages/frontend/core/src/modules/editor/types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type EditorSelector = {
|
||||||
|
blockIds?: string[];
|
||||||
|
elementIds?: string[];
|
||||||
|
};
|
||||||
@@ -10,7 +10,12 @@ import { DebugLogger } from '@affine/debug';
|
|||||||
import { DocMode, type EdgelessRootService } from '@blocksuite/blocks';
|
import { DocMode, type EdgelessRootService } from '@blocksuite/blocks';
|
||||||
import { Bound, DisposableGroup } from '@blocksuite/global/utils';
|
import { Bound, DisposableGroup } from '@blocksuite/global/utils';
|
||||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import { DocsService, FrameworkScope, useService } from '@toeverything/infra';
|
import {
|
||||||
|
DocsService,
|
||||||
|
FrameworkScope,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -70,10 +75,14 @@ export function DocPeekPreview({
|
|||||||
mode?: DocMode;
|
mode?: DocMode;
|
||||||
xywh?: `[${number},${number},${number},${number}]`;
|
xywh?: `[${number},${number},${number},${number}]`;
|
||||||
}) {
|
}) {
|
||||||
const { doc, workspace, loading } = useEditor(docId, mode);
|
const { doc, workspace, editor, loading } = useEditor(docId, mode, {
|
||||||
|
blockIds,
|
||||||
|
elementIds,
|
||||||
|
});
|
||||||
const { jumpToTag } = useNavigateHelper();
|
const { jumpToTag } = useNavigateHelper();
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const peekView = useService(PeekViewService).peekView;
|
const peekView = useService(PeekViewService).peekView;
|
||||||
|
const defaultEditorSelector = useLiveData(editor?.selector$);
|
||||||
const [editorElement, setEditorElement] =
|
const [editorElement, setEditorElement] =
|
||||||
useState<AffineEditorContainer | null>(null);
|
useState<AffineEditorContainer | null>(null);
|
||||||
|
|
||||||
@@ -162,7 +171,7 @@ export function DocPeekPreview({
|
|||||||
}, [docId, peekView, workbench]);
|
}, [docId, peekView, workbench]);
|
||||||
|
|
||||||
// if sync engine has been synced and the page is null, show 404 page.
|
// if sync engine has been synced and the page is null, show 404 page.
|
||||||
if (!doc || !resolvedMode) {
|
if (!doc || !resolvedMode || !editor) {
|
||||||
return loading || !resolvedMode ? (
|
return loading || !resolvedMode ? (
|
||||||
<PageDetailSkeleton key="current-page-is-null" />
|
<PageDetailSkeleton key="current-page-is-null" />
|
||||||
) : (
|
) : (
|
||||||
@@ -177,15 +186,16 @@ export function DocPeekPreview({
|
|||||||
className={clsx('affine-page-viewport', styles.affineDocViewport)}
|
className={clsx('affine-page-viewport', styles.affineDocViewport)}
|
||||||
>
|
>
|
||||||
<FrameworkScope scope={doc.scope}>
|
<FrameworkScope scope={doc.scope}>
|
||||||
|
<FrameworkScope scope={editor.scope}>
|
||||||
<BlockSuiteEditor
|
<BlockSuiteEditor
|
||||||
ref={onRef}
|
ref={onRef}
|
||||||
className={styles.editor}
|
className={styles.editor}
|
||||||
mode={resolvedMode}
|
mode={resolvedMode}
|
||||||
blockIds={blockIds}
|
defaultEditorSelector={defaultEditorSelector}
|
||||||
elementIds={elementIds}
|
|
||||||
page={doc.blockSuiteDoc}
|
page={doc.blockSuiteDoc}
|
||||||
/>
|
/>
|
||||||
</FrameworkScope>
|
</FrameworkScope>
|
||||||
|
</FrameworkScope>
|
||||||
<EditorOutlineViewer
|
<EditorOutlineViewer
|
||||||
editor={editorElement}
|
editor={editorElement}
|
||||||
show={resolvedMode === 'page'}
|
show={resolvedMode === 'page'}
|
||||||
|
|||||||
@@ -8,15 +8,20 @@ import {
|
|||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { type Editor, EditorsService } from '../../editor';
|
import { type Editor, type EditorSelector, EditorsService } from '../../editor';
|
||||||
|
|
||||||
export const useEditor = (pageId: string, preferMode?: DocMode) => {
|
export const useEditor = (
|
||||||
|
pageId: string,
|
||||||
|
preferMode?: DocMode,
|
||||||
|
preferSelector?: EditorSelector
|
||||||
|
) => {
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
const docRecordList = docsService.list;
|
const docRecordList = docsService.list;
|
||||||
const docListReady = useLiveData(docRecordList.isReady$);
|
const docListReady = useLiveData(docRecordList.isReady$);
|
||||||
const docRecord = docRecordList.doc$(pageId).value;
|
const docRecord = docRecordList.doc$(pageId).value;
|
||||||
const preferModeRef = useRef(preferMode);
|
const preferModeRef = useRef(preferMode);
|
||||||
|
const preferSelectorRef = useRef(preferSelector);
|
||||||
|
|
||||||
const [doc, setDoc] = useState<Doc | null>(null);
|
const [doc, setDoc] = useState<Doc | null>(null);
|
||||||
const [editor, setEditor] = useState<Editor | null>(null);
|
const [editor, setEditor] = useState<Editor | null>(null);
|
||||||
@@ -38,7 +43,10 @@ export const useEditor = (pageId: string, preferMode?: DocMode) => {
|
|||||||
}
|
}
|
||||||
const editor = doc.scope
|
const editor = doc.scope
|
||||||
.get(EditorsService)
|
.get(EditorsService)
|
||||||
.createEditor(preferModeRef.current || doc.primaryMode$.value);
|
.createEditor(
|
||||||
|
preferModeRef.current || doc.primaryMode$.value,
|
||||||
|
preferSelectorRef.current
|
||||||
|
);
|
||||||
setEditor(editor);
|
setEditor(editor);
|
||||||
return () => {
|
return () => {
|
||||||
editor.dispose();
|
editor.dispose();
|
||||||
|
|||||||
@@ -31,7 +31,17 @@ const useLoadDoc = (pageId: string) => {
|
|||||||
const queryString = useLiveData(
|
const queryString = useLiveData(
|
||||||
viewService.view.queryString$<{
|
viewService.view.queryString$<{
|
||||||
mode?: string;
|
mode?: string;
|
||||||
}>()
|
blockIds?: string[];
|
||||||
|
elementIds?: string[];
|
||||||
|
}>({
|
||||||
|
// Cannot handle single id situation correctly: `blockIds=xxx`
|
||||||
|
arrayFormat: 'none',
|
||||||
|
types: {
|
||||||
|
mode: 'string',
|
||||||
|
blockIds: value => (value.length ? value.split(',') : []),
|
||||||
|
elementIds: value => (value.length ? value.split(',') : []),
|
||||||
|
},
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryStringMode =
|
const queryStringMode =
|
||||||
@@ -41,6 +51,10 @@ const useLoadDoc = (pageId: string) => {
|
|||||||
|
|
||||||
// We only read the querystring mode when entering, so use useState here.
|
// We only read the querystring mode when entering, so use useState here.
|
||||||
const [initialQueryStringMode] = useState(() => queryStringMode);
|
const [initialQueryStringMode] = useState(() => queryStringMode);
|
||||||
|
const [initialQueryStringSelector] = useState(() => ({
|
||||||
|
blockIds: queryString.blockIds,
|
||||||
|
elementIds: queryString.elementIds,
|
||||||
|
}));
|
||||||
|
|
||||||
const [doc, setDoc] = useState<Doc | null>(null);
|
const [doc, setDoc] = useState<Doc | null>(null);
|
||||||
const [editor, setEditor] = useState<Editor | null>(null);
|
const [editor, setEditor] = useState<Editor | null>(null);
|
||||||
@@ -64,13 +78,14 @@ const useLoadDoc = (pageId: string) => {
|
|||||||
const editor = doc.scope
|
const editor = doc.scope
|
||||||
.get(EditorsService)
|
.get(EditorsService)
|
||||||
.createEditor(
|
.createEditor(
|
||||||
initialQueryStringMode || doc.getPrimaryMode() || ('page' as DocMode)
|
initialQueryStringMode || doc.getPrimaryMode() || ('page' as DocMode),
|
||||||
|
initialQueryStringSelector
|
||||||
);
|
);
|
||||||
setEditor(editor);
|
setEditor(editor);
|
||||||
return () => {
|
return () => {
|
||||||
editor.dispose();
|
editor.dispose();
|
||||||
};
|
};
|
||||||
}, [doc, initialQueryStringMode]);
|
}, [doc, initialQueryStringMode, initialQueryStringSelector]);
|
||||||
|
|
||||||
// update editor mode to queryString
|
// update editor mode to queryString
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import type { Map as YMap } from 'yjs';
|
import type { Map as YMap } from 'yjs';
|
||||||
|
|
||||||
@@ -91,6 +91,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash));
|
const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash));
|
||||||
const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
|
const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
|
||||||
const editorContainer = useLiveData(editor.editorContainer$);
|
const editorContainer = useLiveData(editor.editorContainer$);
|
||||||
|
const [defaultSelector] = useState(() => editor.selector$.value);
|
||||||
|
|
||||||
const isSideBarOpen = useLiveData(workbench.sidebarOpen$);
|
const isSideBarOpen = useLiveData(workbench.sidebarOpen$);
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
@@ -287,6 +288,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
pageId={doc.id}
|
pageId={doc.id}
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
docCollection={docCollection}
|
docCollection={docCollection}
|
||||||
|
defaultEditorSelector={defaultSelector}
|
||||||
/>
|
/>
|
||||||
</Scrollable.Viewport>
|
</Scrollable.Viewport>
|
||||||
<Scrollable.Scrollbar
|
<Scrollable.Scrollbar
|
||||||
|
|||||||
Reference in New Issue
Block a user