From a6484018efe16fdc1082706934cf1848bb27a1a5 Mon Sep 17 00:00:00 2001 From: EYHN Date: Tue, 3 Sep 2024 06:37:58 +0000 Subject: [PATCH] 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$` --- .../common/infra/src/livedata/livedata.ts | 28 +++++ .../common/infra/src/utils/shallow-equal.ts | 34 ++++++ .../blocksuite-editor-container.tsx | 101 +++++++++++------- .../block-suite-editor/blocksuite-editor.tsx | 10 +- .../block-suite-editor/lit-adaper.tsx | 58 ++++------ .../src/components/page-detail-editor.tsx | 47 +++----- .../src/modules/editor/entities/editor.ts | 9 +- .../frontend/core/src/modules/editor/index.ts | 1 + .../src/modules/editor/services/editors.ts | 8 +- .../frontend/core/src/modules/editor/types.ts | 4 + .../view/doc-preview/doc-peek-view.tsx | 32 ++++-- .../core/src/modules/peek-view/view/utils.ts | 14 ++- .../detail-page/detail-page-wrapper.tsx | 21 +++- .../workspace/detail-page/detail-page.tsx | 4 +- 14 files changed, 240 insertions(+), 131 deletions(-) create mode 100644 packages/common/infra/src/utils/shallow-equal.ts create mode 100644 packages/frontend/core/src/modules/editor/types.ts diff --git a/packages/common/infra/src/livedata/livedata.ts b/packages/common/infra/src/livedata/livedata.ts index 058379940c..bc3c0dd024 100644 --- a/packages/common/infra/src/livedata/livedata.ts +++ b/packages/common/infra/src/livedata/livedata.ts @@ -23,6 +23,8 @@ import { throttleTime, } from 'rxjs'; +import { shallowEqual } from '../utils/shallow-equal'; + const logger = new DebugLogger('livedata'); /** @@ -334,6 +336,32 @@ export class LiveData return sub$; } + /** + * same as map, but do shallow equal check before emit + */ + selector(selector: (v: T) => R): LiveData { + const sub$ = LiveData.from( + new Observable(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) { return LiveData.from( this.pipe(distinctUntilChanged(comparator)), diff --git a/packages/common/infra/src/utils/shallow-equal.ts b/packages/common/infra/src/utils/shallow-equal.ts new file mode 100644 index 0000000000..b9d4a9d7a3 --- /dev/null +++ b/packages/common/infra/src/utils/shallow-equal.ts @@ -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; +} diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx index a637233b95..1474eb2174 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx @@ -1,7 +1,10 @@ +import type { EditorSelector } from '@affine/core/modules/editor'; import type { ReferenceInfo } from '@blocksuite/affine-model'; import { DocMode } from '@blocksuite/blocks'; +import type { InlineEditor } from '@blocksuite/inline/inline-editor'; import type { AffineEditorContainer, + DocTitle, EdgelessEditor, PageEditor, } from '@blocksuite/presets'; @@ -10,6 +13,7 @@ import clsx from 'clsx'; import type React from 'react'; import { forwardRef, + useCallback, useEffect, useLayoutEffect, useMemo, @@ -42,8 +46,7 @@ interface BlocksuiteEditorContainerProps { shared?: boolean; className?: string; style?: React.CSSProperties; - blockIds?: string[]; - elementIds?: string[]; + defaultEditorSelector?: EditorSelector; } // mimic the interface of the webcomponent and expose slots & host @@ -57,14 +60,24 @@ export const BlocksuiteEditorContainer = forwardRef< AffineEditorContainer, BlocksuiteEditorContainerProps >(function AffineEditorContainer( - { page, mode, className, style, shared, blockIds, elementIds }, + { page, mode, className, style, shared, defaultEditorSelector }, ref ) { - const scrolledRef = useRef(false); const rootRef = useRef(null); const docRef = useRef(null); + const docTitleRef = useRef(null); const edgelessRef = useRef(null); - const [anchor, setAnchor] = useState(null); + const [anchor] = useState(() => { + 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(() => { 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 useLayoutEffect(() => { requestAnimationFrame(() => { @@ -162,7 +167,7 @@ export const BlocksuiteEditorContainer = forwardRef< } return undefined; }, - }) as unknown as AffineEditorContainer; + }) as unknown as AffineEditorContainer & { origin: HTMLDivElement }; return proxy; }, [mode, page, slots]); @@ -177,31 +182,49 @@ export const BlocksuiteEditorContainer = forwardRef< } }, [affineEditorContainerProxy, ref]); - // `scrolledRef` should be updated if blockElement is changed useEffect(() => { - scrolledRef.current = false; - }, [anchor]); - - useEffect(() => { - if (!anchor) return; - - let canceled = false; - affineEditorContainerProxy.updateComplete - .then(() => { - if (!scrolledRef.current && !canceled) { - const std = affineEditorContainerProxy.host?.std; - if (std) { - scrollAnchoring(std, mode, anchor); + if (anchor) { + let canceled = false; + affineEditorContainerProxy.updateComplete + .then(() => { + if (!canceled) { + const std = affineEditorContainerProxy.host?.std; + if (std) { + scrollAnchoring(std, mode, anchor); + } } - scrolledRef.current = true; - } - }) - .catch(console.error); - return () => { - canceled = true; - }; + }) + .catch(console.error); + return () => { + 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]); + const handleClickPageModeBlank = useCallback(() => { + affineEditorContainerProxy.host?.std.command.exec( + 'appendParagraph' as never, + {} + ); + }, [affineEditorContainerProxy]); + return (
{mode === 'page' ? ( - + ) : ( () => void; style?: CSSProperties; className?: string; - blockIds?: string[]; - elementIds?: string[]; + defaultEditorSelector?: EditorSelector; }; function usePageRoot(page: Doc) { @@ -64,8 +64,7 @@ const BlockSuiteEditorImpl = forwardRef( onLoadEditor, shared, style, - blockIds, - elementIds, + defaultEditorSelector, }, ref ) { @@ -116,8 +115,7 @@ const BlockSuiteEditorImpl = forwardRef( ref={onRefChange} className={className} style={style} - blockIds={blockIds} - elementIds={elementIds} + defaultEditorSelector={defaultEditorSelector} /> ); } diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx index 0f64f34733..3045ebc189 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx @@ -6,7 +6,6 @@ import { import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; import { EditorSettingService } from '@affine/core/modules/editor-settting'; import { PeekViewService } from '@affine/core/modules/peek-view'; -import { WorkbenchService } from '@affine/core/modules/workbench'; import { DocMode } from '@blocksuite/blocks'; import { DocTitle, EdgelessEditor, PageEditor } from '@blocksuite/presets'; import type { Doc } from '@blocksuite/store'; @@ -24,7 +23,6 @@ import React, { useEffect, useMemo, useRef, - useState, } from 'react'; import { PagePropertiesTable } from '../../affine/page-properties'; @@ -146,18 +144,18 @@ const usePatchSpecs = (page: Doc, shared: boolean, mode: DocMode) => { export const BlocksuiteDocEditor = forwardRef< PageEditor, - BlocksuiteEditorProps ->(function BlocksuiteDocEditor({ page, shared }, ref) { - const titleRef = useRef(null); + BlocksuiteEditorProps & { + onClickBlank?: () => void; + titleRef?: React.Ref; + } +>(function BlocksuiteDocEditor( + { page, shared, onClickBlank, titleRef: externalTitleRef }, + ref +) { + const titleRef = useRef(null); const docRef = useRef(null); - const [docPage, setDocPage] = - useState(); 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 onDocRef = useCallback( @@ -174,22 +172,19 @@ export const BlocksuiteDocEditor = forwardRef< [ref] ); - useEffect(() => { - // auto focus the title - setTimeout(() => { - const docPage = docRef.current?.querySelector('affine-page-root'); - if (docPage) { - setDocPage(docPage); + const onTitleRef = useCallback( + (el: DocTitle) => { + titleRef.current = el; + if (externalTitleRef) { + if (typeof externalTitleRef === 'function') { + externalTitleRef(el); + } else { + (externalTitleRef as any).current = el; + } } - if (titleRef.current && !hash) { - const richText = titleRef.current.querySelector('rich-text'); - richText?.inlineEditor?.focusEnd(); - } else { - docPage?.focusFirstParagraph(); - } - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, + [externalTitleRef] + ); const [specs, portals] = usePatchSpecs(page, !!shared, DocMode.Page); @@ -199,7 +194,7 @@ export const BlocksuiteDocEditor = forwardRef< <>
{!isJournal ? ( - + ) : ( )} @@ -211,14 +206,7 @@ export const BlocksuiteDocEditor = forwardRef< specs={specs} hasViewport={false} /> - {docPage && !page.readonly ? ( -
{ - docPage.std.command.exec('appendParagraph' as never, {}); - }} - >
- ) : null} +
{!shared && settings.displayBiDirectionalLink ? ( ) : null} diff --git a/packages/frontend/core/src/components/page-detail-editor.tsx b/packages/frontend/core/src/components/page-detail-editor.tsx index fd152adbdc..9eddee14e6 100644 --- a/packages/frontend/core/src/components/page-detail-editor.tsx +++ b/packages/frontend/core/src/components/page-detail-editor.tsx @@ -1,22 +1,17 @@ import './page-detail-editor.css'; 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 { DisposableGroup } from '@blocksuite/global/utils'; import type { AffineEditorContainer } from '@blocksuite/presets'; import type { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store'; -import { - useLiveData, - useService, - useServiceOptional, -} from '@toeverything/infra'; +import { useLiveData, useService } from '@toeverything/infra'; import { cssVar } from '@toeverything/theme'; import clsx from 'clsx'; 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 { EditorSettingService, fontStyleOptions, @@ -40,35 +35,26 @@ export interface PageDetailEditorProps { docCollection: DocCollection; pageId: string; onLoad?: OnLoadEditor; + defaultEditorSelector?: EditorSelector; } const PageDetailEditorMain = memo(function PageDetailEditorMain({ page, onLoad, + defaultEditorSelector, }: 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 mode = useLiveData(editor.mode$); const isSharedMode = editor.isSharedMode; 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 fontStyle = fontStyleOptions.find( @@ -122,8 +108,7 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({ mode={mode} page={page} shared={isSharedMode} - blockIds={params?.blockIds} - elementIds={params?.elementIds} + defaultEditorSelector={defaultEditorSelector} onLoadEditor={onLoadEditor} /> ); @@ -135,9 +120,5 @@ export const PageDetailEditor = (props: PageDetailEditorProps) => { if (!page) { return null; } - return ( - - - - ); + return ; }; diff --git a/packages/frontend/core/src/modules/editor/entities/editor.ts b/packages/frontend/core/src/modules/editor/entities/editor.ts index 8521848cb8..d4081e2dee 100644 --- a/packages/frontend/core/src/modules/editor/entities/editor.ts +++ b/packages/frontend/core/src/modules/editor/entities/editor.ts @@ -4,13 +4,20 @@ import type { DocService, WorkspaceService } from '@toeverything/infra'; import { Entity, LiveData } from '@toeverything/infra'; 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, { editor: this as Editor, }); readonly mode$ = new LiveData(this.props.defaultMode); + readonly selector$ = new LiveData( + this.props.defaultEditorSelector + ); readonly doc = this.docService.doc; readonly isSharedMode = this.workspaceService.workspace.openOptions.isSharedMode; diff --git a/packages/frontend/core/src/modules/editor/index.ts b/packages/frontend/core/src/modules/editor/index.ts index 3ee383b183..7b2eb8ee3a 100644 --- a/packages/frontend/core/src/modules/editor/index.ts +++ b/packages/frontend/core/src/modules/editor/index.ts @@ -15,6 +15,7 @@ export { Editor } from './entities/editor'; export { EditorScope } from './scopes/editor'; export { EditorService } from './services/editor'; export { EditorsService } from './services/editors'; +export type { EditorSelector } from './types'; export function configureEditorModule(framework: Framework) { framework diff --git a/packages/frontend/core/src/modules/editor/services/editors.ts b/packages/frontend/core/src/modules/editor/services/editors.ts index c60dca7853..189df432c9 100644 --- a/packages/frontend/core/src/modules/editor/services/editors.ts +++ b/packages/frontend/core/src/modules/editor/services/editors.ts @@ -2,9 +2,13 @@ import type { DocMode } from '@blocksuite/blocks'; import { Service } from '@toeverything/infra'; import { Editor } from '../entities/editor'; +import type { EditorSelector } from '../types'; export class EditorsService extends Service { - createEditor(defaultMode: DocMode) { - return this.framework.createEntity(Editor, { defaultMode }); + createEditor(defaultMode: DocMode, defaultEditorSelector?: EditorSelector) { + return this.framework.createEntity(Editor, { + defaultMode, + defaultEditorSelector, + }); } } diff --git a/packages/frontend/core/src/modules/editor/types.ts b/packages/frontend/core/src/modules/editor/types.ts new file mode 100644 index 0000000000..b08e002323 --- /dev/null +++ b/packages/frontend/core/src/modules/editor/types.ts @@ -0,0 +1,4 @@ +export type EditorSelector = { + blockIds?: string[]; + elementIds?: string[]; +}; diff --git a/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx b/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx index 589c494f7d..3d1e62c76a 100644 --- a/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx +++ b/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx @@ -10,7 +10,12 @@ import { DebugLogger } from '@affine/debug'; import { DocMode, type EdgelessRootService } from '@blocksuite/blocks'; import { Bound, DisposableGroup } from '@blocksuite/global/utils'; 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 { useCallback, useEffect, useState } from 'react'; @@ -70,10 +75,14 @@ export function DocPeekPreview({ mode?: DocMode; 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 workbench = useService(WorkbenchService).workbench; const peekView = useService(PeekViewService).peekView; + const defaultEditorSelector = useLiveData(editor?.selector$); const [editorElement, setEditorElement] = useState(null); @@ -162,7 +171,7 @@ export function DocPeekPreview({ }, [docId, peekView, workbench]); // if sync engine has been synced and the page is null, show 404 page. - if (!doc || !resolvedMode) { + if (!doc || !resolvedMode || !editor) { return loading || !resolvedMode ? ( ) : ( @@ -177,14 +186,15 @@ export function DocPeekPreview({ className={clsx('affine-page-viewport', styles.affineDocViewport)} > - + + + { +export const useEditor = ( + pageId: string, + preferMode?: DocMode, + preferSelector?: EditorSelector +) => { const currentWorkspace = useService(WorkspaceService).workspace; const docsService = useService(DocsService); const docRecordList = docsService.list; const docListReady = useLiveData(docRecordList.isReady$); const docRecord = docRecordList.doc$(pageId).value; const preferModeRef = useRef(preferMode); + const preferSelectorRef = useRef(preferSelector); const [doc, setDoc] = useState(null); const [editor, setEditor] = useState(null); @@ -38,7 +43,10 @@ export const useEditor = (pageId: string, preferMode?: DocMode) => { } const editor = doc.scope .get(EditorsService) - .createEditor(preferModeRef.current || doc.primaryMode$.value); + .createEditor( + preferModeRef.current || doc.primaryMode$.value, + preferSelectorRef.current + ); setEditor(editor); return () => { editor.dispose(); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page-wrapper.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-wrapper.tsx index 537102194c..4cdd86e9fd 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page-wrapper.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page-wrapper.tsx @@ -31,7 +31,17 @@ const useLoadDoc = (pageId: string) => { const queryString = 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 queryStringMode = @@ -41,6 +51,10 @@ const useLoadDoc = (pageId: string) => { // We only read the querystring mode when entering, so use useState here. const [initialQueryStringMode] = useState(() => queryStringMode); + const [initialQueryStringSelector] = useState(() => ({ + blockIds: queryString.blockIds, + elementIds: queryString.elementIds, + })); const [doc, setDoc] = useState(null); const [editor, setEditor] = useState(null); @@ -64,13 +78,14 @@ const useLoadDoc = (pageId: string) => { const editor = doc.scope .get(EditorsService) .createEditor( - initialQueryStringMode || doc.getPrimaryMode() || ('page' as DocMode) + initialQueryStringMode || doc.getPrimaryMode() || ('page' as DocMode), + initialQueryStringSelector ); setEditor(editor); return () => { editor.dispose(); }; - }, [doc, initialQueryStringMode]); + }, [doc, initialQueryStringMode, initialQueryStringSelector]); // update editor mode to queryString useEffect(() => { diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index d176294a06..181f7794e2 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -32,7 +32,7 @@ import { WorkspaceService, } from '@toeverything/infra'; 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 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 { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper(); const editorContainer = useLiveData(editor.editorContainer$); + const [defaultSelector] = useState(() => editor.selector$.value); const isSideBarOpen = useLiveData(workbench.sidebarOpen$); const { appSettings } = useAppSettingHelper(); @@ -287,6 +288,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { pageId={doc.id} onLoad={onLoad} docCollection={docCollection} + defaultEditorSelector={defaultSelector} />