import { useState, useEffect, FC, type MouseEvent, type DragEvent, type ReactNode, type CSSProperties, useCallback, } from 'react'; import { Virgo, PluginHooks, BlockDropPlacement, LINE_GAP, AsyncBlock, TAG_GAP, } from '@toeverything/framework/virgo'; import { Button } from '@toeverything/components/common'; import { styled } from '@toeverything/components/ui'; import { LeftMenu } from './LeftMenu'; import { distinctUntilChanged, Subject } from 'rxjs'; import { HandleChildIcon } from '@toeverything/components/icons'; import { MENU_WIDTH } from './menu-config'; const MENU_BUTTON_OFFSET = 4; export interface BlockDomInfo { block: AsyncBlock; rect: DOMRect; } export type LineInfoSubject = Subject< | { direction: BlockDropPlacement; blockInfo: BlockDomInfo; } | undefined >; export type LeftMenuProps = { editor: Virgo; hooks: PluginHooks; defaultVisible?: boolean; blockInfo: Subject; lineInfo: LineInfoSubject; }; type LineInfo = { direction: BlockDropPlacement; blockInfo: BlockDomInfo; }; function Line(props: { lineInfo: LineInfo; rootRect: DOMRect }) { const { lineInfo, rootRect } = props; if (!lineInfo || lineInfo.direction === BlockDropPlacement.none) { return null; } const { direction, blockInfo } = lineInfo; const lineStyle = { zIndex: 2, position: 'absolute' as const, background: '#502EC4', }; const intersectionRect = blockInfo.rect; const horizontalLineStyle = { ...lineStyle, width: intersectionRect.width, height: 2, left: intersectionRect.x - rootRect.x, }; const topLineStyle = { ...horizontalLineStyle, top: intersectionRect.top, }; const bottomLineStyle = { ...horizontalLineStyle, top: intersectionRect.bottom + 1 - rootRect.y - LINE_GAP + TAG_GAP, }; const verticalLineStyle = { ...lineStyle, width: 2, height: intersectionRect.height - LINE_GAP + TAG_GAP, top: intersectionRect.y - rootRect.y, }; const leftLineStyle = { ...verticalLineStyle, left: intersectionRect.x - 10 - rootRect.x, }; const rightLineStyle = { ...verticalLineStyle, left: intersectionRect.right + 10 - rootRect.x, }; const styleMap = { [BlockDropPlacement.left]: leftLineStyle, [BlockDropPlacement.right]: rightLineStyle, [BlockDropPlacement.top]: topLineStyle, [BlockDropPlacement.bottom]: bottomLineStyle, [BlockDropPlacement.outerLeft]: leftLineStyle, [BlockDropPlacement.outerRight]: rightLineStyle, }; return
; } function DragComponent(props: { children: ReactNode; style: CSSProperties; onDragStart: (event: DragEvent) => void; onDragEnd: (event: DragEvent) => void; }) { const { style, children, onDragStart, onDragEnd } = props; return ( event.stopPropagation()} onMouseDown={event => event.stopPropagation()} onDragStart={onDragStart} onDragEnd={onDragEnd} > {children} ); } export const LeftMenuDraggable: FC = props => { const { editor, blockInfo, defaultVisible, lineInfo } = props; const [visible, setVisible] = useState(defaultVisible); const [anchorEl, setAnchorEl] = useState(); const [rootRect, setRootRect] = useState(() => new DOMRect()); const [block, setBlock] = useState(); const [line, setLine] = useState(undefined); const handleDragStart = async (event: DragEvent) => { event.stopPropagation(); setVisible(false); editor.dragDropManager.isOnDrag = true; if (block == null) return; setRootRect(editor.container.getBoundingClientRect()); const dragImage = await editor.blockHelper.getBlockDragImg( block.block.id ); if (dragImage) { event.dataTransfer.setDragImage(dragImage, -50, -10); editor.dragDropManager.setDragBlockInfo(event, block.block.id); } }; const handleDragEnd = (event: DragEvent) => { event.preventDefault(); setLine(undefined); }; const onClick = (event: MouseEvent) => { if (block == null) return; const currentTarget = event.currentTarget; editor.selection.setSelectedNodesIds([block.block.id]); setVisible(true); setAnchorEl(currentTarget); }; const onClose = useCallback(() => setAnchorEl(undefined), [setAnchorEl]); useEffect(() => { const sub = blockInfo .pipe( distinctUntilChanged( (prev, curr) => prev?.block.id === curr?.block.id ) ) .subscribe(block => { setBlock(block); if (block != null) { setRootRect(editor.container.getBoundingClientRect()); setVisible(true); } }); return () => sub.unsubscribe(); }, [blockInfo, editor]); useEffect(() => { if (block?.block != null) { const unobserve = block.block.onUpdate(() => setBlock(undefined)); return unobserve; } return undefined; }, [block?.block]); useEffect(() => { const sub = lineInfo.subscribe(data => { if (data == null) { setLine(undefined); } else { const { direction, blockInfo } = data; setRootRect(editor.container.getBoundingClientRect()); setLine(prev => { if ( prev?.blockInfo.block.id !== blockInfo.block.id || prev?.direction !== direction ) { return { direction, blockInfo, }; } else { return prev; } }); } }); return () => sub.unsubscribe(); }, [editor, lineInfo]); return ( <> {block && ( { } )} ); }; const Draggable = styled(Button)({ cursor: 'grab', padding: '0', display: 'inlineFlex', alignItems: 'center', justifyContent: 'center', backgroundColor: 'transparent', width: '16px', height: '22px', '& svg': { fontSize: '20px', marginLeft: '-2px', }, ':hover': { backgroundColor: '#F5F7F8', borderRadius: '4px', }, }); const LigoLeftMenu = styled('div')({ backgroundColor: 'transparent', // marginRight: '4px', });