feat: add left tool when hover on block

This commit is contained in:
qinluhe 2023-03-24 13:31:37 +08:00
parent c6c97f7c83
commit 7781b912f0
11 changed files with 190 additions and 21 deletions

View File

@ -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;
}

View File

@ -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';

View File

@ -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 : <BlockSideTools blockEditor={blockEditor} container={container} />}
<BlockSelection onDragging={setDragging} blockEditor={blockEditor} container={container} />
</>
);
}

View File

@ -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}
</div>
</div>
{parentRef.current ? <BlockSelection blockEditor={blockEditor} container={parentRef.current} /> : null}
{parentRef.current ? <Overlay container={parentRef.current} blockEditor={blockEditor} /> : null}
</div>
);
}

View File

@ -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;

View File

@ -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<HTMLDivElement | null>(null);
const [isDragging, setDragging] = useState(false);
const pointRef = useRef<number[]>([]);
const startScrollTopRef = useRef<number>(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,
};
}

View File

@ -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 (
<div className='appflowy-block-selection-overlay z-1 pointer-events-none fixed inset-0 overflow-hidden'>
<div ref={ref} className='appflowy-block-selection-overlay z-1 pointer-events-none fixed inset-0 overflow-hidden'>
{isDragging ? <div className='z-99 absolute bg-[#00d5ff] opacity-25' style={style} /> : null}
</div>
);

View File

@ -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<string>();
const ref = useRef<HTMLDivElement | null>(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,
};
}

View File

@ -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 (
<Portal blockId={hoverBlock}>
<div
ref={ref}
style={{
opacity: 0,
}}
className='z-1 absolute left-[-50px] inline-flex h-[calc(1.5em_+_3px)] transition-opacity duration-500'
onMouseDown={(e) => {
// prevent toolbar from taking focus away from editor
e.preventDefault();
}}
>
<IconButton sx={sx}>
<AddIcon />
</IconButton>
<IconButton sx={sx}>
<DragIndicatorIcon />
</IconButton>
</div>
</Portal>
);
}

View File

@ -21,7 +21,7 @@ export default function TextBlock({
const { showGroups } = toolbarProps || toolbarDefaultProps;
return (
<div {...props} className={`${props.className} py-1`}>
<div {...props} className={`${props.className || ''} py-1`}>
<Slate editor={editor} onChange={onChange} value={value}>
{showGroups.length > 0 && <HoveringToolbar node={node} blockId={node.id} />}
<Editable

View File

@ -9,6 +9,21 @@ export function debounce(fn: (...args: any[]) => 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) {