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(null); const triggeredBySelect = useRef(false); const [pageWidth, setPageWidth] = useState(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 ) => { selectionRef.current?.onMouseMove(event); editor.getHooks().onRootNodeMouseMove(event); }; const onMouseDown = ( event: React.MouseEvent ) => { triggeredBySelect.current = true; selectionRef.current?.onMouseDown(event); editor.getHooks().onRootNodeMouseDown(event); }; const onMouseUp = (event: React.MouseEvent) => { selectionRef.current?.onMouseUp(event); editor.getHooks().onRootNodeMouseUp(event); }; const onMouseOut = ( event: React.MouseEvent ) => { editor.getHooks().onRootNodeMouseOut(event); }; const onMouseLeave = ( event: React.MouseEvent ) => { editor.getHooks().onRootNodeMouseLeave(event); }; const onContextmenu = ( event: React.MouseEvent ) => { selectionRef.current?.onContextmenu(event); }; const onKeyDown: React.KeyboardEventHandler = event => { // IMP move into keyboard managers? editor.getHooks().onRootNodeKeyDown(event); }; const onKeyUp: React.KeyboardEventHandler = 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) => { event.dataTransfer.dropEffect = 'move'; event.preventDefault(); editor.dragDropManager.handlerEditorDragOver(event); if (editor.dragDropManager.isEnabled()) { editor.getHooks().onRootNodeDragOver(event); } }; const onDragLeave = (event: React.DragEvent) => { if (editor.dragDropManager.isEnabled()) { editor.getHooks().onRootNodeDragLeave(event); } }; const onDragOverCapture = (event: React.DragEvent) => { event.preventDefault(); if (editor.dragDropManager.isEnabled()) { editor.getHooks().onRootNodeDragOver(event); } }; const onDragEnd = (event: React.DragEvent) => { editor.dragDropManager.handlerEditorDragEnd(event); editor.getHooks().onRootNodeDragEnd(event); }; const onDrop = (event: React.DragEvent) => { event.preventDefault(); editor.dragDropManager.handlerEditorDrop(event); editor.getHooks().onRootNodeDrop(event); }; return ( { 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} > {/** TODO: remove selectionManager insert */} {editor && ( )} {editor.isEdgeless ? null : } {patchedNodes} ); }; // 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 ( ); } 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`, });