Files
AFFiNE/libs/components/editor-core/src/RenderRoot.tsx
2022-08-24 02:08:00 +08:00

290 lines
9.5 KiB
TypeScript

import { styled, usePatchNodes } from '@toeverything/components/ui';
import {
Protocol,
services,
type ReturnUnobserve,
} from '@toeverything/datasource/db-service';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { EditorProvider } from './Contexts';
import type { BlockEditor } from './editor';
import { useIsOnDrag } from './hooks';
import { addNewGroup, appendNewGroup } from './recast-block';
import { BlockRenderProvider, RenderBlock } from './render-block';
import { SelectionRect, SelectionRef } from './Selection';
interface RenderRootProps {
editor: BlockEditor;
editorElement: () => JSX.Element;
/**
* Scroll to the bottom of the article visually leave blank
*/
scrollBlank?: boolean;
}
const MAX_PAGE_WIDTH = 5000;
export const MIN_PAGE_WIDTH = 1480;
export const RenderRoot = ({ editor, editorElement }: RenderRootProps) => {
const selectionRef = useRef<SelectionRef>(null);
const triggeredBySelect = useRef(false);
const [pageWidth, setPageWidth] = useState<number>(MIN_PAGE_WIDTH);
const isOnDrag = useIsOnDrag(editor);
const { patch, has, patchedNodes } = usePatchNodes();
editor.setReactRenderRoot({ patch, has });
const rootId = editor.getRootBlockId();
const fetchPageBlockWidth = useCallback(async () => {
const dbPageBlock = await services.api.editorBlock.getBlock(
editor.workspace,
rootId
);
if (!dbPageBlock) return;
if (dbPageBlock.getDecoration('fullWidthChecked')) {
setPageWidth(MAX_PAGE_WIDTH);
} else {
setPageWidth(MIN_PAGE_WIDTH);
}
}, [editor.workspace, rootId]);
useEffect(() => {
fetchPageBlockWidth();
let unobserve: ReturnUnobserve | undefined = undefined;
const observe = async () => {
unobserve = await services.api.editorBlock.observe(
{
workspace: editor.workspace,
id: rootId,
},
fetchPageBlockWidth
);
};
observe();
return () => {
unobserve?.();
};
}, [rootId, editor, fetchPageBlockWidth]);
const onMouseMove = async (
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
selectionRef.current?.onMouseMove(event);
editor.getHooks().onRootNodeMouseMove(event);
};
const onMouseDown = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
triggeredBySelect.current = true;
selectionRef.current?.onMouseDown(event);
editor.getHooks().onRootNodeMouseDown(event);
};
const onMouseUp = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
selectionRef.current?.onMouseUp(event);
editor.getHooks().onRootNodeMouseUp(event);
};
const onMouseOut = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
editor.getHooks().onRootNodeMouseOut(event);
};
const onMouseLeave = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
editor.getHooks().onRootNodeMouseLeave(event);
};
const onContextmenu = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
selectionRef.current?.onContextmenu(event);
};
const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
// IMP move into keyboard managers?
editor.getHooks().onRootNodeKeyDown(event);
};
const onKeyUp: React.KeyboardEventHandler<HTMLDivElement> = event => {
// IMP move into keyboard managers?
editor.getHooks().onRootNodeKeyUp(event);
};
const onKeyDownCapture: React.KeyboardEventHandler<
HTMLDivElement
> = event => {
editor.getHooks().onRootNodeKeyDownCapture(event);
};
const onDragOver = (event: React.DragEvent<Element>) => {
event.dataTransfer.dropEffect = 'move';
event.preventDefault();
editor.dragDropManager.handlerEditorDragOver(event);
if (editor.dragDropManager.isEnabled()) {
editor.getHooks().onRootNodeDragOver(event);
}
};
const onDragLeave = (event: React.DragEvent<Element>) => {
if (editor.dragDropManager.isEnabled()) {
editor.getHooks().onRootNodeDragLeave(event);
}
};
const onDragOverCapture = (event: React.DragEvent<Element>) => {
event.preventDefault();
if (editor.dragDropManager.isEnabled()) {
editor.getHooks().onRootNodeDragOver(event);
}
};
const onDragEnd = (event: React.DragEvent<Element>) => {
editor.dragDropManager.handlerEditorDragEnd(event);
editor.getHooks().onRootNodeDragEnd(event);
};
const onDrop = (event: React.DragEvent<Element>) => {
event.preventDefault();
editor.dragDropManager.handlerEditorDrop(event);
editor.getHooks().onRootNodeDrop(event);
};
return (
<EditorProvider editor={editor} editorElement={editorElement}>
<BlockRenderProvider blockRender={RenderBlock}>
<Container
isEdgeless={editor.isEdgeless}
ref={ref => {
if (ref != null && ref !== editor.container) {
editor.container = ref;
editor.getHooks().render();
}
}}
onMouseMove={onMouseMove}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
onMouseOut={onMouseOut}
onContextMenu={onContextmenu}
onKeyDown={onKeyDown}
onKeyDownCapture={onKeyDownCapture}
onKeyUp={onKeyUp}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDragOverCapture={onDragOverCapture}
onDragEnd={onDragEnd}
onDrop={onDrop}
isOnDrag={isOnDrag}
>
<Content style={{ maxWidth: pageWidth + 'px' }}>
<RenderBlock blockId={editor.getRootBlockId()} />
</Content>
{/** TODO: remove selectionManager insert */}
{editor && (
<SelectionRect ref={selectionRef} editor={editor} />
)}
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
{patchedNodes}
</Container>
</BlockRenderProvider>
</EditorProvider>
);
};
// eslint-disable-next-line @typescript-eslint/naming-convention
function ScrollBlank({ editor }: { editor: BlockEditor }) {
const mouseMoved = useRef(false);
const onMouseDown = useCallback(() => (mouseMoved.current = false), []);
const onMouseMove = useCallback(() => (mouseMoved.current = true), []);
const onClick = useCallback(
async (e: React.MouseEvent) => {
if (mouseMoved.current) {
mouseMoved.current = false;
return;
}
const rootBlock = await editor.getBlockById(
editor.getRootBlockId()
);
if (!rootBlock) {
throw new Error('root block is not found');
}
const lastRootChildren = await rootBlock.lastChild();
// If last block is not a group
// create a group with a empty text
if (lastRootChildren == null) {
appendNewGroup(editor, rootBlock, true);
return;
}
if (
lastRootChildren.type !== Protocol.Block.Type.group ||
lastRootChildren.childrenIds.length > 1
) {
addNewGroup(editor, lastRootChildren, true);
return;
}
// If the **only** block in the group is text and is empty
// active the text block
const theGroupChildBlock = await lastRootChildren.firstChild();
if (
theGroupChildBlock &&
theGroupChildBlock.type === Protocol.Block.Type.text &&
theGroupChildBlock.blockProvider?.isEmpty()
) {
await editor.selectionManager.activeNodeByNodeId(
theGroupChildBlock.id
);
return;
}
// else create a new group
addNewGroup(editor, lastRootChildren, true);
},
[editor]
);
return (
<ScrollBlankContainer
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onClick={onClick}
/>
);
}
const PADDING_X = 150;
const Container = styled('div')(
({ isEdgeless, isOnDrag }: { isEdgeless: boolean; isOnDrag: boolean }) => ({
width: '100%',
padding: isEdgeless ? 0 : `72px ${PADDING_X}px 0 ${PADDING_X}px`,
minWidth: isEdgeless ? 'unset' : '940px',
position: 'relative',
...(isOnDrag && {
cursor: 'grabbing',
// expected css selector
// eslint-disable-next-line @typescript-eslint/naming-convention
'& *': { cursor: 'grabbing' },
}),
})
);
const Content = styled('div')({
width: '100%',
margin: '0 auto',
transitionDuration: '.2s',
transitionTimingFunction: 'ease-in',
});
const ScrollBlankContainer = styled('div')({
paddingBottom: '30vh',
margin: `0 -${PADDING_X}px`,
});