From 7781b912f061cb7ef7b24622279528a1b8da153b Mon Sep 17 00:00:00 2001 From: qinluhe Date: Fri, 24 Mar 2023 13:31:37 +0800 Subject: [PATCH] feat: add left tool when hover on block --- .../block_editor/view/block_position.ts | 24 +++++-- .../components/HoveringToolbar/index.tsx | 2 +- .../components/block/BlockList/Overlay.tsx | 14 ++++ .../components/block/BlockList/index.tsx | 4 +- .../BlockPortal/index.tsx} | 4 +- .../BlockSelection/BlockSelection.hooks.tsx | 30 +++++++-- .../components/block/BlockSelection/index.tsx | 15 ++++- .../BlockSideTools/BlockSideTools.hooks.tsx | 64 +++++++++++++++++++ .../components/block/BlockSideTools/index.tsx | 37 +++++++++++ .../components/block/TextBlock/index.tsx | 2 +- .../src/appflowy_app/utils/tool.ts | 15 +++++ 11 files changed, 190 insertions(+), 21 deletions(-) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/Overlay.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{HoveringToolbar/Portal.tsx => block/BlockPortal/index.tsx} (65%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/BlockSideTools.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/index.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts index 883c711132..ded70389c9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts @@ -8,7 +8,6 @@ export class BlockPositionManager { constructor(container: HTMLDivElement) { this.container = container; this.regionGrid = new RegionGrid(container.offsetHeight); - } isInViewport(nodeId: string) { @@ -21,12 +20,9 @@ export class BlockPositionManager { this.updateBlockPosition(blockId); this.viewportBlocks.add(blockId); } - return { unobserve: () => { - if (blockId) { - this.viewportBlocks.delete(blockId); - } + if (blockId) this.viewportBlocks.delete(blockId); }, } } @@ -66,6 +62,24 @@ export class BlockPositionManager { return this.regionGrid.getIntersectBlocks(startX, startY, endX, endY); } + getViewportBlockByPoint(x: number, y: number): BlockPosition | null { + let blockPosition: BlockPosition | null = null; + this.viewportBlocks.forEach(id => { + this.updateBlockPosition(id); + const block = this.blockPositions.get(id); + if (!block) return; + + if (block.x + block.width - 1 >= x && + block.y + block.height - 1 >= y && block.y <= y) { + if (!blockPosition || block.y > blockPosition.y) { + blockPosition = block; + } + } + }); + return blockPosition; + } + + destroy() { this.container = null; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx index dcd502905f..1210ceb1af 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx @@ -1,5 +1,5 @@ import FormatButton from './FormatButton'; -import Portal from './Portal'; +import Portal from '../block/BlockPortal'; import { TreeNode } from '$app/block_editor/view/tree_node'; import { useHoveringToolbar } from './index.hooks'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/Overlay.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/Overlay.tsx new file mode 100644 index 0000000000..17ce15c98e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/Overlay.tsx @@ -0,0 +1,14 @@ +import React, { useState } from 'react'; +import BlockSideTools from '../BlockSideTools'; +import BlockSelection from '../BlockSelection'; +import { BlockEditor } from '@/appflowy_app/block_editor'; + +export default function Overlay({ blockEditor, container }: { container: HTMLDivElement; blockEditor: BlockEditor }) { + const [isDragging, setDragging] = useState(false); + return ( + <> + {isDragging ? null : } + + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx index 9a8709ea64..0e5873ae44 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx @@ -4,7 +4,7 @@ import { withErrorBoundary } from 'react-error-boundary'; import ListFallbackComponent from './ListFallbackComponent'; import BlockListTitle from './BlockListTitle'; import BlockComponent from '../BlockComponent'; -import BlockSelection from '../BlockSelection'; +import Overlay from './Overlay'; function BlockList(props: BlockListProps) { const { root, rowVirtualizer, parentRef, blockEditor } = useBlockList(props); @@ -46,7 +46,7 @@ function BlockList(props: BlockListProps) { ) : null} - {parentRef.current ? : null} + {parentRef.current ? : null} ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockPortal/index.tsx similarity index 65% rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/block/BlockPortal/index.tsx index 0176c8f429..bdd969616d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockPortal/index.tsx @@ -1,9 +1,9 @@ import ReactDOM from 'react-dom'; -const Portal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => { +const BlockPortal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => { const root = document.querySelectorAll(`[data-block-id=${blockId}] > .block-overlay`)[0]; return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null; }; -export default Portal; +export default BlockPortal; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx index 00bc05f2d1..480841d5d5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx @@ -1,13 +1,26 @@ import { BlockEditor } from '@/appflowy_app/block_editor'; import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; -export function useBlockSelection({ container, blockEditor }: { container: HTMLDivElement; blockEditor: BlockEditor }) { +export function useBlockSelection({ + container, + blockEditor, + onDragging, +}: { + container: HTMLDivElement; + blockEditor: BlockEditor; + onDragging?: (_isDragging: boolean) => void; +}) { const blockPositionManager = blockEditor.renderTree.blockPositionManager; + const ref = useRef(null); const [isDragging, setDragging] = useState(false); const pointRef = useRef([]); const startScrollTopRef = useRef(0); + useEffect(() => { + onDragging?.(isDragging); + }, [isDragging]); + const [rect, setRect] = useState<{ startX: number; startY: number; @@ -89,6 +102,7 @@ export function useBlockSelection({ container, blockEditor }: { container: HTMLD (e: MouseEvent) => { if (!isDragging || !blockPositionManager) return; e.preventDefault(); + e.stopPropagation(); calcIntersectBlocks(e.clientX, e.clientY); const { top, bottom } = container.getBoundingClientRect(); @@ -119,19 +133,21 @@ export function useBlockSelection({ container, blockEditor }: { container: HTMLD ); useEffect(() => { - window.addEventListener('mousedown', handleDragStart); - window.addEventListener('mousemove', handleDraging); - window.addEventListener('mouseup', handleDragEnd); + if (!ref.current) return; + document.addEventListener('mousedown', handleDragStart); + document.addEventListener('mousemove', handleDraging); + document.addEventListener('mouseup', handleDragEnd); return () => { - window.removeEventListener('mousedown', handleDragStart); - window.removeEventListener('mousemove', handleDraging); - window.removeEventListener('mouseup', handleDragEnd); + document.removeEventListener('mousedown', handleDragStart); + document.removeEventListener('mousemove', handleDraging); + document.removeEventListener('mouseup', handleDragEnd); }; }, [handleDragStart, handleDragEnd, handleDraging]); return { isDragging, style, + ref, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx index 4ef554d489..67aa371748 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx @@ -2,14 +2,23 @@ import { useBlockSelection } from './BlockSelection.hooks'; import { BlockEditor } from '$app/block_editor'; import React from 'react'; -function BlockSelection({ container, blockEditor }: { container: HTMLDivElement; blockEditor: BlockEditor }) { - const { isDragging, style } = useBlockSelection({ +function BlockSelection({ + container, + blockEditor, + onDragging, +}: { + container: HTMLDivElement; + blockEditor: BlockEditor; + onDragging?: (_isDragging: boolean) => void; +}) { + const { isDragging, style, ref } = useBlockSelection({ container, blockEditor, + onDragging, }); return ( -
+
{isDragging ?
: null}
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/BlockSideTools.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/BlockSideTools.hooks.tsx new file mode 100644 index 0000000000..b9cdc1d5e1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/BlockSideTools.hooks.tsx @@ -0,0 +1,64 @@ +import { BlockEditor } from '@/appflowy_app/block_editor'; +import { BlockType } from '@/appflowy_app/interfaces'; +import { debounce } from '@/appflowy_app/utils/tool'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +export function useBlockSideTools({ blockEditor, container }: { container: HTMLDivElement; blockEditor: BlockEditor }) { + const [hoverBlock, setHoverBlock] = useState(); + const ref = useRef(null); + + const handleMouseMove = useCallback((e: MouseEvent) => { + const { clientX, clientY } = e; + const x = clientX; + const y = clientY + container.scrollTop; + const block = blockEditor.renderTree.blockPositionManager?.getViewportBlockByPoint(x, y); + + if (!block) { + setHoverBlock(''); + } else { + const node = blockEditor.renderTree.getTreeNode(block.id)!; + if ([BlockType.ColumnBlock].includes(node.type)) { + setHoverBlock(''); + } else { + setHoverBlock(block.id); + } + } + }, []); + + const debounceMove = useMemo(() => debounce(handleMouseMove, 30), [handleMouseMove]); + + useEffect(() => { + const el = ref.current; + if (!el) return; + if (!hoverBlock) { + el.style.opacity = '0'; + el.style.zIndex = '-1'; + } else { + el.style.opacity = '1'; + el.style.zIndex = '1'; + const node = blockEditor.renderTree.getTreeNode(hoverBlock); + el.style.top = '3px'; + if (node?.type === BlockType.HeadingBlock) { + if (node.data.level === 1) { + el.style.top = '8px'; + } else if (node.data.level === 2) { + el.style.top = '6px'; + } else { + el.style.top = '5px'; + } + } + } + }, [hoverBlock]); + + useEffect(() => { + container.addEventListener('mousemove', debounceMove); + return () => { + container.removeEventListener('mousemove', debounceMove); + }; + }, [debounceMove]); + + return { + hoverBlock, + ref, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/index.tsx new file mode 100644 index 0000000000..624d2a98ce --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useBlockSideTools } from './BlockSideTools.hooks'; +import { BlockEditor } from '@/appflowy_app/block_editor'; +import AddIcon from '@mui/icons-material/Add'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import Portal from '../BlockPortal'; +import { IconButton } from '@mui/material'; + +const sx = { height: 24, width: 24 }; + +export default function BlockSideTools(props: { container: HTMLDivElement; blockEditor: BlockEditor }) { + const { hoverBlock, ref } = useBlockSideTools(props); + + if (!hoverBlock) return null; + return ( + +
{ + // prevent toolbar from taking focus away from editor + e.preventDefault(); + }} + > + + + + + + +
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx index 906e9a4060..d024702852 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx @@ -21,7 +21,7 @@ export default function TextBlock({ const { showGroups } = toolbarProps || toolbarDefaultProps; return ( -
+
{showGroups.length > 0 && } void, delay: number) { } } +export function throttle(fn: (...args: any[]) => void, delay: number, immediate = true) { + let timeout: NodeJS.Timeout | null = null + return (...args: any[]) => { + if (!timeout) { + timeout = setTimeout(() => { + timeout = null + // eslint-disable-next-line prefer-spread + !immediate && fn.apply(undefined, args) + }, delay) + // eslint-disable-next-line prefer-spread + immediate && fn.apply(undefined, args) + } + } +} + export function get(obj: any, path: string[], defaultValue?: any) { let value = obj; for (const prop of path) {