mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: add left tool when hover on block
This commit is contained in:
parent
c6c97f7c83
commit
7781b912f0
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user