290 lines
9.2 KiB
TypeScript
290 lines
9.2 KiB
TypeScript
import { styled, usePatchNodes } from '@toeverything/components/ui';
|
|
import {
|
|
Protocol,
|
|
services,
|
|
type ReturnUnobserve,
|
|
} from '@toeverything/datasource/db-service';
|
|
import type { PropsWithChildren } from 'react';
|
|
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 { 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,
|
|
children,
|
|
}: PropsWithChildren<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 value={{ editor, editorElement }}>
|
|
<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' }}>
|
|
{children}
|
|
</Content>
|
|
{/** TODO: remove selectionManager insert */}
|
|
{editor && <SelectionRect ref={selectionRef} editor={editor} />}
|
|
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
|
{patchedNodes}
|
|
</Container>
|
|
</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 (
|
|
<ScrollBlankContainter
|
|
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 ScrollBlankContainter = styled('div')({
|
|
paddingBottom: '30vh',
|
|
margin: `0 -${PADDING_X}px`,
|
|
});
|