+
);
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) {