From a38c213744342b64f0a1c5d70ef05458f32519d4 Mon Sep 17 00:00:00 2001 From: qinluhe Date: Thu, 23 Mar 2023 15:09:23 +0800 Subject: [PATCH 1/7] fix: Optimize the re-render node when the selection changes --- .../block_editor/blocks/text_block/index.ts | 21 +++++++++++- .../block_editor/core/operation.ts | 14 ++++++++ .../block_editor/view/block_position.ts | 32 +++++++++---------- .../appflowy_app/block_editor/view/tree.ts | 30 +++++++++-------- .../block_editor/view/tree_node.ts | 18 +++++++++++ .../block/BlockList/BlockList.hooks.tsx | 3 +- .../components/block/TextBlock/index.hooks.ts | 3 ++ 7 files changed, 89 insertions(+), 32 deletions(-) diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts index de42c3c373..29e468c78b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts @@ -2,10 +2,11 @@ import { BaseEditor, BaseSelection, Descendant } from "slate"; import { TreeNode } from '$app/block_editor/view/tree_node'; import { Operation } from "$app/block_editor/core/operation"; import { TextBlockSelectionManager } from './text_selection'; +import { BlockType } from "@/appflowy_app/interfaces"; export class TextBlockManager { public selectionManager: TextBlockSelectionManager; - constructor(private operation: Operation) { + constructor(private rootId: string, private operation: Operation) { this.selectionManager = new TextBlockSelectionManager(); } @@ -18,6 +19,24 @@ export class TextBlockManager { this.operation.updateNode(node.id, path, data); } + deleteNode(node: TreeNode) { + if (node.type !== BlockType.TextBlock) { + this.operation.updateNode(node.id, ['type'], BlockType.TextBlock); + return; + } + if (node.parent!.id !== this.rootId) { + const newParent = node.parent!.parent!; + const newPrev = node.parent; + this.operation.moveNode(node.id, newParent.id, newPrev?.id || ''); + } + if (!node.prevLine) return; + this.operation.updateNode(node.prevLine.id, ['data', 'content'], [ + ...node.prevLine.data.content, + ...node.data.content, + ]); + this.operation.deleteNode(node.id); + } + splitNode(node: TreeNode, editor: BaseEditor) { const focus = editor.selection?.focus; const path = focus?.path || [0, editor.children.length - 1]; diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts index 38f3a3fb76..1755e05f8a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts @@ -5,6 +5,7 @@ import { Block } from './block'; export class Operation { private sync: BlockEditorSync; + constructor(private blockChain: BlockChain) { this.sync = new BlockEditorSync(); } @@ -66,6 +67,19 @@ export class Operation { }); this.sync.sendOps([op]); } + + moveNode(blockId: string, newParentId: string, newPrevId: string) { + const op = this.getMoveOp(blockId, newParentId, newPrevId); + this.blockChain.move(blockId, newParentId, newPrevId); + this.sync.sendOps([op]); + } + + deleteNode(blockId: string) { + const op = this.getRemoveOp(blockId); + this.blockChain.remove(blockId); + this.sync.sendOps([op]); + } + private getUpdateNodeOp(blockId: string, path: string[], value: T): { type: 'update', data: UpdateOpData 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 a2841d8a3b..883c711132 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 @@ -3,30 +3,31 @@ export class BlockPositionManager { private regionGrid: RegionGrid; private viewportBlocks: Set = new Set(); private blockPositions: Map = new Map(); - private observer: IntersectionObserver; private container: HTMLDivElement | null = null; constructor(container: HTMLDivElement) { this.container = container; this.regionGrid = new RegionGrid(container.offsetHeight); - this.observer = new IntersectionObserver((entries) => { - for (const entry of entries) { - const blockId = entry.target.getAttribute('data-block-id'); - if (!blockId) return; - if (entry.isIntersecting) { - this.updateBlockPosition(blockId); - this.viewportBlocks.add(blockId); - } else { - this.viewportBlocks.delete(blockId); - } - } - }, { root: container }); + + } + + isInViewport(nodeId: string) { + return this.viewportBlocks.has(nodeId); } observeBlock(node: HTMLDivElement) { - this.observer.observe(node); + const blockId = node.getAttribute('data-block-id'); + if (blockId) { + this.updateBlockPosition(blockId); + this.viewportBlocks.add(blockId); + } + return { - unobserve: () => this.observer.unobserve(node), + unobserve: () => { + if (blockId) { + this.viewportBlocks.delete(blockId); + } + }, } } @@ -67,7 +68,6 @@ export class BlockPositionManager { destroy() { this.container = null; - this.observer.disconnect(); } } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts index 4eb136ff09..f416bc80ae 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts @@ -133,21 +133,25 @@ export class RenderTree { updateSelections(selections: string[]) { const newSelections = filterSelections(selections, this.map); - let isDiff = false; - if (newSelections.length !== this.selections.size) { - isDiff = true; - } - const selectedBlocksSet = new Set(newSelections); - if (Array.from(this.selections).some((id) => !selectedBlocksSet.has(id))) { - isDiff = true; - } - if (isDiff) { - const shouldUpdateIds = new Set([...this.selections, ...newSelections]); - this.selections = selectedBlocksSet; - shouldUpdateIds.forEach((id) => this.forceUpdate(id)); - } + const updateNotSelected: string[] = []; + const updateSelected: string[] = []; + Array.from(this.selections).forEach((id) => { + if (!selectedBlocksSet.has(id)) { + updateNotSelected.push(id); + } + }); + newSelections.forEach(id => { + if (!this.selections.has(id)) { + updateSelected.push(id); + } + }); + + this.selections = selectedBlocksSet; + [...updateNotSelected, ...updateSelected].forEach((id) => { + this.forceUpdate(id); + }); } isSelected(nodeId: string) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts index 9ed78bd4b4..19120e08c1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts @@ -52,6 +52,24 @@ export class TreeNode { this.children.push(node); } + get lastChild() { + return this.children[this.children.length - 1]; + } + + get prevLine(): TreeNode | null { + if (!this.parent) return null; + const index = this.parent?.children.findIndex(item => item.id === this.id); + if (index === 0) { + return this.parent; + } + const prev = this.parent.children[index - 1]; + let line = prev; + while(line.lastChild) { + line = line.lastChild; + } + return line; + } + get block() { return this._block; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx index 0d673a47e8..e55bb3f7d7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx @@ -21,7 +21,6 @@ export function useBlockList({ blockId, blockEditor }: BlockListProps) { const rowVirtualizer = useVirtualizer({ count: root?.children.length || 0, getScrollElement: () => parentRef.current, - overscan: 5, estimateSize: () => { return defaultSize; }, @@ -71,7 +70,7 @@ export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: Fa export function withTextBlockManager(Component: (props: BlockListProps) => React.ReactElement) { return (props: BlockListProps) => { - const textBlockManager = new TextBlockManager(props.blockEditor.operation); + const textBlockManager = new TextBlockManager(props.blockId, props.blockEditor.operation); useEffect(() => { return () => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts index a776ae8be4..59ac3c934d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts @@ -49,6 +49,9 @@ export function useTextBlock({ return; } + case 'Backspace': { + console.log(editor.selection) + } } triggerHotkey(event, editor); From c6c97f7c839c7de022401e4cd4951d3db318305b Mon Sep 17 00:00:00 2001 From: qinluhe Date: Thu, 23 Mar 2023 16:13:03 +0800 Subject: [PATCH 2/7] feat: the feature of delete block --- .../block_editor/blocks/text_block/index.ts | 39 ++++++++++++++-- .../block_editor/core/block_chain.ts | 3 +- .../appflowy_app/block_editor/view/tree.ts | 46 ++++++++++--------- .../block_editor/view/tree_node.ts | 1 + .../components/block/TextBlock/index.hooks.ts | 23 ++++++++-- 5 files changed, 84 insertions(+), 28 deletions(-) diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts index 29e468c78b..13a5ac74f8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts @@ -1,15 +1,26 @@ -import { BaseEditor, BaseSelection, Descendant } from "slate"; +import { BaseEditor, BaseSelection, Descendant, Editor, Transforms } from "slate"; import { TreeNode } from '$app/block_editor/view/tree_node'; import { Operation } from "$app/block_editor/core/operation"; import { TextBlockSelectionManager } from './text_selection'; import { BlockType } from "@/appflowy_app/interfaces"; +import { ReactEditor } from "slate-react"; export class TextBlockManager { public selectionManager: TextBlockSelectionManager; + private editorMap: Map = new Map(); + constructor(private rootId: string, private operation: Operation) { this.selectionManager = new TextBlockSelectionManager(); } + register(id: string, editor: BaseEditor & ReactEditor) { + this.editorMap.set(id, editor); + } + + unregister(id: string) { + this.editorMap.delete(id); + } + setSelection(node: TreeNode, selection: BaseSelection) { // console.log(node.id, selection); this.selectionManager.setSelection(node.id, selection) @@ -22,19 +33,41 @@ export class TextBlockManager { deleteNode(node: TreeNode) { if (node.type !== BlockType.TextBlock) { this.operation.updateNode(node.id, ['type'], BlockType.TextBlock); + this.operation.updateNode(node.id, ['data'], { content: node.data.content }); return; } - if (node.parent!.id !== this.rootId) { + + if (!node.block.next && node.parent!.id !== this.rootId) { const newParent = node.parent!.parent!; const newPrev = node.parent; this.operation.moveNode(node.id, newParent.id, newPrev?.id || ''); + return; } if (!node.prevLine) return; + + const retainData = node.prevLine.data.content; + const editor = this.editorMap.get(node.prevLine.id); + if (editor) { + const index = retainData.length - 1; + const anchor = { + path: [0, index], + offset: retainData[index].text.length, + }; + const selection = { + anchor, + focus: {...anchor} + }; + ReactEditor.focus(editor); + Transforms.select(editor, selection); + } + this.operation.updateNode(node.prevLine.id, ['data', 'content'], [ - ...node.prevLine.data.content, + ...retainData, ...node.data.content, ]); + this.operation.deleteNode(node.id); + } splitNode(node: TreeNode, editor: BaseEditor) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts index 877f3592df..0dbf0ee2b9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts @@ -118,9 +118,10 @@ export class BlockChain { remove(blockId: string) { const block = this.getBlock(blockId); if (!block) return; + const oldParentId = block.parent?.id; block.remove(); this.map.delete(block.id); - this.onBlockChange('delete', { block }); + this.onBlockChange('remove', { oldParentId }); return block; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts index f416bc80ae..150990e23c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts @@ -87,27 +87,28 @@ export class RenderTree { if (!block) return null; const node = this.createNode(block); if (!node) return null; - - if (shouldUpdateChildren) { - const children: TreeNode[] = []; - let childBlock = block.firstChild; - - while(childBlock) { - const child = this.createNode(childBlock); - child.update(childBlock, child.children); - children.push(child); - childBlock = childBlock.next; - } - - node.update(block, children); - node?.reRender(); - node?.children.forEach(child => { - child.reRender(); - }) - } else { - node.update(block, node.children); - node?.reRender(); + if (!shouldUpdateChildren) { + node.update(node.block, node.children); + node.reRender(); + return; } + + + const children: TreeNode[] = []; + let childBlock = block.firstChild; + + while (childBlock) { + const child = this.createNode(childBlock); + child.update(childBlock, child.children); + children.push(child); + childBlock = childBlock.next; + } + + node.update(block, children); + node.reRender(); + node.children.forEach(child => { + child.reRender(); + }); } onBlockChange(command: string, data: BlockChangeProps) { @@ -119,6 +120,9 @@ export class RenderTree { case 'update': this.forceUpdate(block!.id); break; + case 'remove': + if (oldParentId) this.forceUpdate(oldParentId, true); + break; case 'move': if (oldParentId) this.forceUpdate(oldParentId, true); if (block?.parent) this.forceUpdate(block.parent.id, true); @@ -127,7 +131,7 @@ export class RenderTree { default: break; } - + } updateSelections(selections: string[]) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts index 19120e08c1..32cba14edf 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts @@ -36,6 +36,7 @@ export class TreeNode { } update(block: Block, children: TreeNode[]) { + this.type = block.type; this.data = block.data; this.children = []; children.forEach(child => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts index 59ac3c934d..0275be16f5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts @@ -1,7 +1,7 @@ import { TreeNode } from "@/appflowy_app/block_editor/view/tree_node"; import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey"; -import { useCallback, useContext, useLayoutEffect, useState } from "react"; -import { Transforms, createEditor, Descendant } from 'slate'; +import { useCallback, useContext, useEffect, useLayoutEffect, useState } from "react"; +import { Transforms, createEditor, Descendant, Range } from 'slate'; import { ReactEditor, withReact } from 'slate-react'; import { TextBlockContext } from '$app/utils/slate/context'; @@ -50,7 +50,14 @@ export function useTextBlock({ return; } case 'Backspace': { - console.log(editor.selection) + if (!editor.selection) return; + const { anchor } = editor.selection; + const isCollapase = Range.isCollapsed(editor.selection); + if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') { + event.stopPropagation(); + event.preventDefault(); + textBlockManager?.deleteNode(node); + } } } @@ -64,7 +71,17 @@ export function useTextBlock({ editor.children = value; Transforms.collapse(editor); + useEffect(() => { + textBlockManager?.register(node.id, editor); + + return () => { + textBlockManager?.unregister(node.id); + } + }, [ editor ]) + + useLayoutEffect(() => { + let timer: NodeJS.Timeout; if (focusId === node.id && selection) { ReactEditor.focus(editor); From 7781b912f061cb7ef7b24622279528a1b8da153b Mon Sep 17 00:00:00 2001 From: qinluhe Date: Fri, 24 Mar 2023 13:31:37 +0800 Subject: [PATCH 3/7] 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) { From df66521f13a8e3951634825eb23fb6b7825d4b88 Mon Sep 17 00:00:00 2001 From: qinluhe Date: Sat, 25 Mar 2023 17:38:58 +0800 Subject: [PATCH 4/7] refactor: document data and update --- frontend/appflowy_tauri/package.json | 8 +- frontend/appflowy_tauri/pnpm-lock.yaml | 85 +- .../HoveringToolbar/FormatButton.tsx | 0 .../HoveringToolbar/FormatIcon.tsx | 0 .../HoveringToolbar/index.hooks.ts | 0 .../{ => block}/HoveringToolbar/index.tsx | 2 +- .../components/block/TextBlock/index.tsx | 2 +- .../components/document/BlockPortal/index.tsx | 9 + .../DocumentTitle/DocumentTitle.hooks.ts | 7 + .../document/DocumentTitle/index.tsx | 14 + .../document/HoveringToolbar/FormatButton.tsx | 32 + .../document/HoveringToolbar/FormatIcon.tsx | 20 + .../document/HoveringToolbar/index.hooks.ts | 34 + .../document/HoveringToolbar/index.tsx | 30 + .../components/document/Node/Node.hooks.ts | 11 + .../components/document/Node/index.tsx | 37 + .../components/document/Root/Root.hooks.tsx | 16 + .../components/document/Root/Tree.hooks.tsx | 63 ++ .../components/document/Root/index.tsx | 32 + .../document/TextBlock/BindYjs.hooks.ts | 62 ++ .../components/document/TextBlock/Leaf.tsx | 41 + .../document/TextBlock/TextBlock.hooks.ts | 75 ++ .../components/document/TextBlock/index.tsx | 40 + .../VirtualizerList/VirtualizerList.hooks.tsx | 21 + .../document/VirtualizerList/index.tsx | 52 ++ .../ErrorBoundaryFallbackComponent.tsx | 12 + .../document/_shared/SubscribeNode.hooks.ts | 16 + .../NavigationPanel/FolderItem.hooks.ts | 5 + .../src/appflowy_app/interfaces/document.ts | 31 + .../effects/document/document_controller.ts | 120 +++ .../stores/reducers/document/slice.ts | 63 ++ .../src/appflowy_app/stores/store.ts | 2 + .../src/appflowy_app/utils/block.ts | 9 + .../src/appflowy_app/utils/tool.ts | 37 + .../appflowy_app/views/DocumentPage.hooks.ts | 790 +----------------- .../src/appflowy_app/views/DocumentPage.tsx | 22 +- 36 files changed, 997 insertions(+), 803 deletions(-) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => block}/HoveringToolbar/FormatButton.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => block}/HoveringToolbar/FormatIcon.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => block}/HoveringToolbar/index.hooks.ts (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/{ => block}/HoveringToolbar/index.tsx (95%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index 8215381041..9cac4d87c2 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -20,6 +20,7 @@ "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.12", "@reduxjs/toolkit": "^1.9.2", + "@slate-yjs/core": "^0.3.1", "@tanstack/react-virtual": "3.0.0-beta.54", "@tauri-apps/api": "^1.2.0", "events": "^3.3.0", @@ -42,8 +43,9 @@ "slate": "^0.91.4", "slate-react": "^0.91.9", "ts-results": "^3.3.0", - "ulid": "^2.3.0", - "utf8": "^3.0.0" + "utf8": "^3.0.0", + "yjs": "^13.5.51", + "y-indexeddb": "^9.0.9" }, "devDependencies": { "@tauri-apps/cli": "^1.2.2", @@ -53,6 +55,7 @@ "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@types/utf8": "^3.0.1", + "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitejs/plugin-react": "^3.0.0", @@ -64,6 +67,7 @@ "prettier-plugin-tailwindcss": "^0.2.2", "tailwindcss": "^3.2.7", "typescript": "^4.6.4", + "uuid": "^9.0.0", "vite": "^4.0.0" } } diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml index 4402ceca71..426fb22859 100644 --- a/frontend/appflowy_tauri/pnpm-lock.yaml +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -6,6 +6,7 @@ specifiers: '@mui/icons-material': ^5.11.11 '@mui/material': ^5.11.12 '@reduxjs/toolkit': ^1.9.2 + '@slate-yjs/core': ^0.3.1 '@tanstack/react-virtual': 3.0.0-beta.54 '@tauri-apps/api': ^1.2.0 '@tauri-apps/cli': ^1.2.2 @@ -15,6 +16,7 @@ specifiers: '@types/react': ^18.0.15 '@types/react-dom': ^18.0.6 '@types/utf8': ^3.0.1 + '@types/uuid': ^9.0.1 '@typescript-eslint/eslint-plugin': ^5.51.0 '@typescript-eslint/parser': ^5.51.0 '@vitejs/plugin-react': ^3.0.0 @@ -31,6 +33,7 @@ specifiers: postcss: ^8.4.21 prettier: 2.8.4 prettier-plugin-tailwindcss: ^0.2.2 + protoc-gen-ts: ^0.8.5 react: ^18.2.0 react-dom: ^18.2.0 react-error-boundary: ^3.1.4 @@ -45,9 +48,11 @@ specifiers: tailwindcss: ^3.2.7 ts-results: ^3.3.0 typescript: ^4.6.4 - ulid: ^2.3.0 utf8: ^3.0.0 + uuid: ^9.0.0 vite: ^4.0.0 + y-indexeddb: ^9.0.9 + yjs: ^13.5.51 dependencies: '@emotion/react': 11.10.6_pmekkgnqduwlme35zpnqhenc34 @@ -55,6 +60,7 @@ dependencies: '@mui/icons-material': 5.11.11_ao76n7r2cajsoyr3cbwrn7geoi '@mui/material': 5.11.12_xqeqsl5kvjjtyxwyi3jhw3yuli '@reduxjs/toolkit': 1.9.3_k4ae6lp43ej6mezo3ztvx6pykq + '@slate-yjs/core': 0.3.1_slate@0.91.4+yjs@13.5.51 '@tanstack/react-virtual': 3.0.0-beta.54_react@18.2.0 '@tauri-apps/api': 1.2.0 events: 3.3.0 @@ -64,6 +70,7 @@ dependencies: is-hotkey: 0.2.0 jest: 29.5.0_@types+node@18.14.6 nanoid: 4.0.1 + protoc-gen-ts: 0.8.6_ss7alqtodw6rv4lluxhr36xjoa react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-error-boundary: 3.1.4_react@18.2.0 @@ -76,8 +83,9 @@ dependencies: slate: 0.91.4 slate-react: 0.91.9_6tgy34rvmll7duwkm4ydcekf3u ts-results: 3.3.0 - ulid: 2.3.0 utf8: 3.0.0 + y-indexeddb: 9.0.9_yjs@13.5.51 + yjs: 13.5.51 devDependencies: '@tauri-apps/cli': 1.2.3 @@ -87,6 +95,7 @@ devDependencies: '@types/react': 18.0.28 '@types/react-dom': 18.0.11 '@types/utf8': 3.0.1 + '@types/uuid': 9.0.1 '@typescript-eslint/eslint-plugin': 5.54.0_6mj2wypvdnknez7kws2nfdgupi '@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu '@vitejs/plugin-react': 3.1.0_vite@4.1.4 @@ -98,6 +107,7 @@ devDependencies: prettier-plugin-tailwindcss: 0.2.4_prettier@2.8.4 tailwindcss: 3.2.7_postcss@8.4.21 typescript: 4.9.5 + uuid: 9.0.0 vite: 4.1.4_@types+node@18.14.6 packages: @@ -1308,6 +1318,17 @@ packages: '@sinonjs/commons': 2.0.0 dev: false + /@slate-yjs/core/0.3.1_slate@0.91.4+yjs@13.5.51: + resolution: {integrity: sha512-8nvS9m5FhMNONgydAfzwDCUhuoWbgzx5Bvw1/foSe+JO331UOT1xAKbUX5FzGCOunUcbRjMPXSdNyiPc0dodJg==} + peerDependencies: + slate: '>=0.70.0' + yjs: ^13.5.29 + dependencies: + slate: 0.91.4 + y-protocols: 1.0.5 + yjs: 13.5.51 + dev: false + /@tanstack/react-virtual/3.0.0-beta.54_react@18.2.0: resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==} peerDependencies: @@ -1553,6 +1574,10 @@ packages: resolution: {integrity: sha512-1EkWuw7rT3BMz2HpmcEOr/HL61mWNA6Ulr/KdbXR9AI0A55wD4Qfv8hizd8Q1DnknSIzzDvQmvvY/guvX7jjZA==} dev: true + /@types/uuid/9.0.1: + resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==} + dev: true + /@types/yargs-parser/21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: false @@ -3050,6 +3075,10 @@ packages: /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isomorphic.js/0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + dev: false + /istanbul-lib-coverage/3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} @@ -3575,6 +3604,14 @@ packages: type-check: 0.4.0 dev: true + /lib0/0.2.73: + resolution: {integrity: sha512-aJJIElCLWnHMcYZPtsM07QoSfHwpxCy4VUzBYGXFYEmh/h2QS5uZNbCCfL0CqnkOE30b7Tp9DVfjXag+3qzZjQ==} + engines: {node: '>=14'} + hasBin: true + dependencies: + isomorphic.js: 0.2.5 + dev: false + /lilconfig/2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -4055,6 +4092,17 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 + /protoc-gen-ts/0.8.6_ss7alqtodw6rv4lluxhr36xjoa: + resolution: {integrity: sha512-66oeorGy4QBvYjQGd/gaeOYyFqKyRmRgTpofmnw8buMG0P7A0jQjoKSvKJz5h5tNUaVkIzvGBUTRVGakrhhwpA==} + hasBin: true + peerDependencies: + google-protobuf: ^3.13.0 + typescript: 4.x.x + dependencies: + google-protobuf: 3.21.2 + typescript: 4.9.5 + dev: false + /punycode/2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -4678,12 +4726,6 @@ packages: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} hasBin: true - dev: true - - /ulid/2.3.0: - resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} - hasBin: true - dev: false /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -4726,6 +4768,11 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /uuid/9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + dev: true + /v8-to-istanbul/9.1.0: resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} @@ -4839,6 +4886,21 @@ packages: engines: {node: '>=0.4'} dev: true + /y-indexeddb/9.0.9_yjs@13.5.51: + resolution: {integrity: sha512-GcJbiJa2eD5hankj46Hea9z4hbDnDjvh1fT62E5SpZRsv8GcEemw34l1hwI2eknGcv5Ih9JfusT37JLx9q3LFg==} + peerDependencies: + yjs: ^13.0.0 + dependencies: + lib0: 0.2.73 + yjs: 13.5.51 + dev: false + + /y-protocols/1.0.5: + resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==} + dependencies: + lib0: 0.2.73 + dev: false + /y18n/5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4872,6 +4934,13 @@ packages: yargs-parser: 21.1.1 dev: false + /yjs/13.5.51: + resolution: {integrity: sha512-F1Nb3z3TdandD80IAeQqgqy/2n9AhDLcXoBhZvCUX1dNVe0ef7fIwi6MjSYaGAYF2Ev8VcLcsGnmuGGOl7AWbw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + dependencies: + lib0: 0.2.73 + dev: false + /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatButton.tsx similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatButton.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatIcon.tsx similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatIcon.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.hooks.ts similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts rename to frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.hooks.ts diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx similarity index 95% rename from frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx index 1210ceb1af..0d02fbf665 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx @@ -1,5 +1,5 @@ import FormatButton from './FormatButton'; -import Portal from '../block/BlockPortal'; +import Portal from '../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/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx index d024702852..11b43bf2b9 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 @@ -1,7 +1,7 @@ import BlockComponent from '../BlockComponent'; import { Slate, Editable } from 'slate-react'; import Leaf from './Leaf'; -import HoveringToolbar from '$app/components/HoveringToolbar'; +import HoveringToolbar from '@/appflowy_app/components/block/HoveringToolbar'; import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; import { useTextBlock } from './index.hooks'; import { BlockCommonProps, TextBlockToolbarProps } from '@/appflowy_app/interfaces'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx new file mode 100644 index 0000000000..bdd969616d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx @@ -0,0 +1,9 @@ +import ReactDOM from 'react-dom'; + +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 BlockPortal; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts new file mode 100644 index 0000000000..6c5e80c721 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts @@ -0,0 +1,7 @@ +import { useSubscribeNode } from '../_shared/SubscribeNode.hooks'; +export function useDocumentTitle(id: string) { + const { node } = useSubscribeNode(id); + return { + node + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx new file mode 100644 index 0000000000..9923e5cf76 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { useDocumentTitle } from './DocumentTitle.hooks'; +import TextBlock from '../TextBlock'; + +export default function DocumentTitle({ id }: { id: string }) { + const { node } = useDocumentTitle(id); + if (!node) return null; + + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx new file mode 100644 index 0000000000..1409680f24 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx @@ -0,0 +1,32 @@ +import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; + +import { command } from '$app/constants/toolbar'; +import FormatIcon from './FormatIcon'; +import { BaseEditor } from 'slate'; + +const FormatButton = ({ editor, format, icon }: { editor: BaseEditor; format: string; icon: string }) => { + return ( + + {command[format].title} + {command[format].key} +
+ } + placement='top-start' + > + toggleFormat(editor, format)} + > + + + + ); +}; + +export default FormatButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx new file mode 100644 index 0000000000..371ec6585c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material'; +import { iconSize } from '$app/constants/toolbar'; + +export default function FormatIcon({ icon }: { icon: string }) { + switch (icon) { + case 'bold': + return ; + case 'underlined': + return ; + case 'italic': + return ; + case 'code': + return ; + case 'strikethrough': + return ; + default: + return null; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts new file mode 100644 index 0000000000..bddc3be4f7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts @@ -0,0 +1,34 @@ +import { useEffect, useRef } from 'react'; +import { useFocused, useSlate } from 'slate-react'; +import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar'; + + +export function useHoveringToolbar(id: string) { + const editor = useSlate(); + const inFocus = useFocused(); + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const nodeRect = document.querySelector(`[data-block-id=${id}]`)?.getBoundingClientRect(); + + if (!nodeRect) return; + const position = calcToolbarPosition(editor, el, nodeRect); + + if (!position) { + el.style.opacity = '0'; + el.style.zIndex = '-1'; + } else { + el.style.opacity = '1'; + el.style.zIndex = '1'; + el.style.top = position.top; + el.style.left = position.left; + } + }); + return { + ref, + inFocus, + editor + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx new file mode 100644 index 0000000000..a35588033c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx @@ -0,0 +1,30 @@ +import FormatButton from './FormatButton'; +import Portal from '../BlockPortal'; +import { useHoveringToolbar } from './index.hooks'; + +const HoveringToolbar = ({ id }: { id: string }) => { + const { inFocus, ref, editor } = useHoveringToolbar(id); + if (!inFocus) return null; + + return ( + +
{ + // prevent toolbar from taking focus away from editor + e.preventDefault(); + }} + > + {['bold', 'italic', 'underlined', 'strikethrough', 'code'].map((format) => ( + + ))} +
+
+ ); +}; + +export default HoveringToolbar; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts new file mode 100644 index 0000000000..5517908214 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts @@ -0,0 +1,11 @@ + +import { useSubscribeNode } from '../_shared/SubscribeNode.hooks'; + +export function useNode(id: string) { + const { node, childIds } = useSubscribeNode(id); + + return { + node, + childIds, + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx new file mode 100644 index 0000000000..b53530d8f1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx @@ -0,0 +1,37 @@ +import React, { useCallback } from 'react'; +import { useNode } from './Node.hooks'; +import { withErrorBoundary } from 'react-error-boundary'; +import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import TextBlock from '../TextBlock'; + +function NodeComponent({ id }: { id: string }) { + const { node, childIds } = useNode(id); + + const renderBlock = useCallback((props: { node: Node; childIds?: string[] }) => { + switch (props.node.type) { + case 'text': + return ; + default: + break; + } + }, []); + + if (!node) return null; + + return ( +
+ {renderBlock({ + node, + childIds, + })} +
+
+ ); +} + +const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, { + FallbackComponent: ErrorBoundaryFallbackComponent, +}); + +export default React.memo(NodeWithErrorBoundary); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx new file mode 100644 index 0000000000..faf8df0897 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx @@ -0,0 +1,16 @@ +import { DocumentData } from '$app/interfaces/document'; +import { useSubscribeNode } from '../_shared/SubscribeNode.hooks'; +import { useParseTree } from './Tree.hooks'; + +export function useRoot({ documentData }: { documentData: DocumentData }) { + const { rootId } = documentData; + + useParseTree(documentData); + + const { node: rootNode, childIds: rootChildIds } = useSubscribeNode(rootId); + + return { + node: rootNode, + childIds: rootChildIds, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx new file mode 100644 index 0000000000..143c1d7ac5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx @@ -0,0 +1,63 @@ +import { useEffect } from 'react'; +import { DocumentData, NestedBlock } from '$app/interfaces/document'; +import { useAppDispatch } from '@/appflowy_app/stores/store'; +import { documentActions, Node } from '$app/stores/reducers/document/slice'; + +export function useParseTree(documentData: DocumentData) { + const dispatch = useAppDispatch(); + const { blocks, ytexts, yarrays, rootId } = documentData; + const flattenNestedBlocks = ( + block: NestedBlock + ): (Node & { + children: string[]; + })[] => { + const node: Node & { + children: string[]; + } = { + id: block.id, + delta: ytexts[block.data.text], + data: block.data, + type: block.type, + parent: block.parent, + children: yarrays[block.children], + }; + + const nodes = [node]; + node.children.forEach((child) => { + const childBlock = blocks[child]; + nodes.push(...flattenNestedBlocks(childBlock)); + }); + return nodes; + }; + + const initializeNodeHierarchy = (parentId: string, children: string[]) => { + children.forEach((childId) => { + dispatch(documentActions.addChild({ parentId, childId })); + const child = blocks[childId]; + initializeNodeHierarchy(childId, yarrays[child.children]); + }); + }; + + useEffect(() => { + const root = documentData.blocks[rootId]; + + const initialNodes = flattenNestedBlocks(root); + + initialNodes.forEach((node) => { + const _node = { + id: node.id, + parent: node.parent, + data: node.data, + type: node.type, + delta: node.delta, + }; + dispatch(documentActions.addNode(_node)); + }); + + initializeNodeHierarchy(rootId, yarrays[root.children]); + + return () => { + dispatch(documentActions.clear()); + }; + }, [documentData]); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx new file mode 100644 index 0000000000..3e89c1b31e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx @@ -0,0 +1,32 @@ +import { DocumentData } from '@/appflowy_app/interfaces/document'; +import React, { useCallback } from 'react'; +import { useRoot } from './Root.hooks'; +import Node from '../Node'; +import { withErrorBoundary } from 'react-error-boundary'; +import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent'; +import VirtualizerList from '../VirtualizerList'; +import { Skeleton } from '@mui/material'; + +function Root({ documentData }: { documentData: DocumentData }) { + const { node, childIds } = useRoot({ documentData }); + + const renderNode = useCallback((nodeId: string) => { + return ; + }, []); + + if (!node || !childIds) { + return ; + } + + return ( +
+ +
+ ); +} + +const RootWithErrorBoundary = withErrorBoundary(Root, { + FallbackComponent: ErrorBoundaryFallbackComponent, +}); + +export default React.memo(RootWithErrorBoundary); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts new file mode 100644 index 0000000000..8e0d31d6b3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts @@ -0,0 +1,62 @@ + + +import { useEffect, useMemo, useRef } from "react"; +import { createEditor } from "slate"; +import { withReact } from "slate-react"; + +import * as Y from 'yjs'; +import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core'; +import { Delta } from '@slate-yjs/core/dist/model/types'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; + +const initialValue = [{ + type: 'paragraph', + children: [{ text: '' }], +}]; + +export function useBindYjs(delta: TextDelta[], update: (_delta: Delta) => void) { + const yTextRef = useRef(); + // Create a yjs document and get the shared type + const sharedType = useMemo(() => { + const ydoc = new Y.Doc() + const _sharedType = ydoc.get('content', Y.XmlText) as Y.XmlText; + + const insertDelta = slateNodesToInsertDelta(initialValue); + // Load the initial value into the yjs document + _sharedType.applyDelta(insertDelta); + + const yText = insertDelta[0].insert as Y.XmlText; + yTextRef.current = yText; + + return _sharedType; + }, []); + + const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []); + + useEffect(() => { + YjsEditor.connect(editor); + return () => { + yTextRef.current = undefined; + YjsEditor.disconnect(editor); + } + }, [editor]); + + useEffect(() => { + const yText = yTextRef.current; + if (!yText) return; + + const textEventHandler = (event: Y.YTextEvent) => { + console.log(event.delta, event.target.toDelta()); + update(event.delta as Delta); + } + yText.applyDelta(delta); + yText.observe(textEventHandler); + + return () => { + yText.unobserve(textEventHandler); + } + }, [delta]) + + + return { editor } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx new file mode 100644 index 0000000000..aa5dcd1efa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx @@ -0,0 +1,41 @@ +import { BaseText } from 'slate'; +import { RenderLeafProps } from 'slate-react'; + +const Leaf = ({ + attributes, + children, + leaf, +}: RenderLeafProps & { + leaf: BaseText & { + bold?: boolean; + code?: boolean; + italic?: boolean; + underlined?: boolean; + strikethrough?: boolean; + }; +}) => { + let newChildren = children; + if (leaf.bold) { + newChildren = {children}; + } + + if (leaf.code) { + newChildren = {newChildren}; + } + + if (leaf.italic) { + newChildren = {newChildren}; + } + + if (leaf.underlined) { + newChildren = {newChildren}; + } + + return ( + + {newChildren} + + ); +}; + +export default Leaf; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts new file mode 100644 index 0000000000..6d896c2182 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts @@ -0,0 +1,75 @@ +import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey"; +import { useCallback, useContext, useState } from "react"; +import { Descendant, Range } from "slate"; +import { useBindYjs } from "./BindYjs.hooks"; +import { YDocControllerContext } from '../../../stores/effects/document/document_controller'; +import { Delta } from "@slate-yjs/core/dist/model/types"; +import { TextDelta } from '../../../interfaces/document'; + +function useController(textId: string) { + const docController = useContext(YDocControllerContext); + + const update = useCallback( + (delta: Delta) => { + docController?.yTextApply(textId, delta) + }, + [textId], + ) + + return { + update + } +} + +export function useTextBlock(text: string, delta: TextDelta[]) { + const { update } = useController(text); + const { editor } = useBindYjs(delta, update); + const [value, setValue] = useState([]); + + const onChange = useCallback( + (e: Descendant[]) => { + setValue(e); + }, + [editor], + ); + + const onKeyDownCapture = (event: React.KeyboardEvent) => { + switch (event.key) { + case 'Enter': { + event.stopPropagation(); + event.preventDefault(); + + return; + } + case 'Backspace': { + if (!editor.selection) return; + const { anchor } = editor.selection; + const isCollapase = Range.isCollapsed(editor.selection); + if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') { + event.stopPropagation(); + event.preventDefault(); + return; + } + } + } + triggerHotkey(event, editor); + } + + const onDOMBeforeInput = useCallback((e: InputEvent) => { + // COMPAT: in Apple, `compositionend` is dispatched after the + // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese. + // Here, prevent the beforeInput event and wait for the compositionend event to take effect + if (e.inputType === 'insertFromComposition') { + e.preventDefault(); + } + + }, []); + + return { + onChange, + onKeyDownCapture, + onDOMBeforeInput, + editor, + value + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx new file mode 100644 index 0000000000..e721337145 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx @@ -0,0 +1,40 @@ +import { Slate, Editable } from 'slate-react'; +import Leaf from './Leaf'; +import { useTextBlock } from './TextBlock.hooks'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import NodeComponent from '../Node'; +import HoveringToolbar from '../HoveringToolbar'; + +export default function TextBlock({ + node, + childIds, + placeholder, + ...props +}: { + node: Node; + childIds?: string[]; + placeholder?: string; +} & React.HTMLAttributes) { + const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, node.delta); + + return ( +
+ + + } + placeholder={placeholder || 'Please enter some text...'} + /> + + {childIds && childIds.length > 0 ? ( +
+ {childIds.map((item) => ( + + ))} +
+ ) : null} +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx new file mode 100644 index 0000000000..c0e543bf5f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx @@ -0,0 +1,21 @@ +import { useVirtualizer } from '@tanstack/react-virtual'; +import { useRef } from 'react'; + +const defaultSize = 60; + +export function useVirtualizerList(count: number) { + const parentRef = useRef(null); + + const rowVirtualizer = useVirtualizer({ + count, + getScrollElement: () => parentRef.current, + estimateSize: () => { + return defaultSize; + }, + }); + + return { + rowVirtualizer, + parentRef, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx new file mode 100644 index 0000000000..7d84f19450 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { useVirtualizerList } from './VirtualizerList.hooks'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import DocumentTitle from '../DocumentTitle'; + +export default function VirtualizerList({ + childIds, + node, + renderNode, +}: { + childIds: string[]; + node: Node; + renderNode: (nodeId: string) => JSX.Element; +}) { + const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length); + + const virtualItems = rowVirtualizer.getVirtualItems(); + + return ( +
+
+ {node && childIds && virtualItems.length ? ( +
+ {virtualItems.map((virtualRow) => { + const id = childIds[virtualRow.index]; + return ( +
+ {virtualRow.index === 0 ? : null} + {renderNode(id)} +
+ ); + })} +
+ ) : null} +
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx new file mode 100644 index 0000000000..fc6851734c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx @@ -0,0 +1,12 @@ +import { Alert } from '@mui/material'; +import { FallbackProps } from 'react-error-boundary'; + +export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: FallbackProps) { + return ( + +

Something went wrong:

+
{error.message}
+ +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts new file mode 100644 index 0000000000..cf0530f1ac --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts @@ -0,0 +1,16 @@ +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import { useAppSelector } from '@/appflowy_app/stores/store'; +import { useMemo } from 'react'; + +export function useSubscribeNode(id: string) { + const node = useAppSelector(state => state.document.nodes[id]); + const childIds = useAppSelector(state => state.document.children[id]); + + const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.type]); + const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]); + + return { + node: memoizedNode, + childIds: memoizedChildIds + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts index 3c38ab70cd..b03ecd865d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts @@ -9,6 +9,7 @@ import { useError } from '../../error/Error.hooks'; import { AppObserver } from '../../../stores/effects/folder/app/app_observer'; import { useNavigate } from 'react-router-dom'; import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants'; +import { YDocController } from '$app/stores/effects/document/document_controller'; export const useFolderEvents = (folder: IFolder, pages: IPage[]) => { const appDispatch = useAppDispatch(); @@ -132,6 +133,10 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => { layoutType: ViewLayoutTypePB.Document, }); + // temp: let me try it by yjs + const ydocController = new YDocController(newView.id); + await ydocController.createDocument(); + appDispatch( pagesActions.addPage({ folderId: folder.id, diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts new file mode 100644 index 0000000000..0cfefe0d75 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts @@ -0,0 +1,31 @@ +// eslint-disable-next-line no-shadow +export enum BlockType { + PageBlock = 'page', + HeadingBlock = 'heading', + ListBlock = 'list', + TextBlock = 'text', + CodeBlock = 'code', + EmbedBlock = 'embed', + QuoteBlock = 'quote', + DividerBlock = 'divider', + MediaBlock = 'media', + TableBlock = 'table', + ColumnBlock = 'column' +} +export interface NestedBlock { + id: string; + type: BlockType; + data: Record; + parent: string | null; + children: string; +} +export interface TextDelta { + insert: string; + attributes?: Record; +} +export interface DocumentData { + rootId: string; + blocks: Record; + ytexts: Record; + yarrays: Record; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts new file mode 100644 index 0000000000..67bd8d01d0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts @@ -0,0 +1,120 @@ +import * as Y from 'yjs'; +import { IndexeddbPersistence } from 'y-indexeddb'; +import { v4 } from 'uuid'; +import { DocumentData } from '@/appflowy_app/interfaces/document'; +import { createContext } from 'react'; + +export type DeltaAttributes = { + retain: number; + attributes: Record; +}; + +export type DeltaRetain = { retain: number }; +export type DeltaDelete = { delete: number }; +export type DeltaInsert = { + insert: string | Y.XmlText; + attributes?: Record; +}; + +export type InsertDelta = Array; +export type Delta = Array< + DeltaRetain | DeltaDelete | DeltaInsert | DeltaAttributes +>; + +export const YDocControllerContext = createContext(null); + +export class YDocController { + private _ydoc: Y.Doc; + private readonly provider: IndexeddbPersistence; + + constructor(private id: string) { + this._ydoc = new Y.Doc(); + this.provider = new IndexeddbPersistence(`document-${this.id}`, this._ydoc); + } + + + createDocument = async () => { + await this.provider.whenSynced; + const ydoc = this._ydoc; + const blocks = ydoc.getMap('blocks'); + const rootNode = ydoc.getArray("root"); + + // create page block for root node + const rootId = v4(); + rootNode.push([rootId]) + const rootChildrenId = v4(); + const rootChildren = ydoc.getArray(rootChildrenId); + const rootTitleId = v4(); + const yTitle = ydoc.getText(rootTitleId); + yTitle.insert(0, ""); + const root = { + id: rootId, + type: 'page', + data: { + text: rootTitleId + }, + parent: null, + children: rootChildrenId + }; + blocks.set(root.id, root); + + // create text block for first line + const textId = v4(); + const yTextId = v4(); + const ytext = ydoc.getText(yTextId); + ytext.insert(0, ""); + const textChildrenId = v4(); + ydoc.getArray(textChildrenId); + const text = { + id: textId, + type: 'text', + data: { + text: yTextId, + }, + parent: rootId, + children: textChildrenId, + } + + // add text block to root children + rootChildren.push([textId]); + blocks.set(text.id, text); + } + + open = async (): Promise => { + await this.provider.whenSynced; + const ydoc = this._ydoc; + ydoc.on('updateV2', (update) => { + console.log('======', update); + }) + const blocks = ydoc.getMap('blocks'); + const obj: DocumentData = { + rootId: ydoc.getArray('root').toArray()[0] || '', + blocks: blocks.toJSON(), + ytexts: {}, + yarrays: {} + }; + Object.keys(obj.blocks).forEach(key => { + const value = obj.blocks[key]; + if (value.children) { + Object.assign(obj.yarrays, { + [value.children]: ydoc.getArray(value.children).toArray() + }); + } + if (value.data.text) { + Object.assign(obj.ytexts, { + [value.data.text]: ydoc.getText(value.data.text).toDelta() + }) + } + }); + return obj; + } + + + yTextApply = (yTextId: string, delta: Delta) => { + console.log("====", yTextId, delta); + const ydoc = this._ydoc; + const ytext = ydoc.getText(yTextId); + ytext.applyDelta(delta); + } + +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts new file mode 100644 index 0000000000..eb98209a07 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts @@ -0,0 +1,63 @@ +import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document"; +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; + +export interface Node { + id: string; + parent: string | null; + type: BlockType; + selected?: boolean; + delta: TextDelta[]; + data: { + text?: string; + }; +} + +export type NodeState = { + nodes: Record; + children: Record; +}; +const initialState: NodeState = { + nodes: {}, + children: {}, +}; + +export const documentSlice = createSlice({ + name: 'document', + initialState: initialState, + reducers: { + clear: (state, action: PayloadAction) => { + return initialState; + }, + addNode: (state, action: PayloadAction) => { + state.nodes[action.payload.id] = action.payload; + }, + addChild: (state, action: PayloadAction<{ parentId: string, childId: string }>) => { + const children = state.children[action.payload.parentId]; + if (children) { + children.push(action.payload.childId); + } else { + state.children[action.payload.parentId] = [action.payload.childId] + } + }, + + updateNode: (state, action: PayloadAction<{id: string; parent?: string; type?: BlockType; data?: any }>) => { + state.nodes[action.payload.id] = { + ...state.nodes[action.payload.id], + ...action.payload + } + }, + + removeNode: (state, action: PayloadAction) => { + const parentId = state.nodes[action.payload].parent; + delete state.nodes[action.payload]; + if (parentId) { + const index = state.children[parentId].indexOf(action.payload); + if (index > -1) { + state.children[parentId].splice(index, 1); + } + } + }, + }, +}); + +export const documentActions = documentSlice.actions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts index 1f96485938..9bd9c15909 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts @@ -14,6 +14,7 @@ import { currentUserSlice } from './reducers/current-user/slice'; import { gridSlice } from './reducers/grid/slice'; import { workspaceSlice } from './reducers/workspace/slice'; import { databaseSlice } from './reducers/database/slice'; +import { documentSlice } from './reducers/document/slice'; import { boardSlice } from './reducers/board/slice'; import { errorSlice } from './reducers/error/slice'; import { activePageIdSlice } from './reducers/activePageId/slice'; @@ -32,6 +33,7 @@ const store = configureStore({ [gridSlice.name]: gridSlice.reducer, [databaseSlice.name]: databaseSlice.reducer, [boardSlice.name]: boardSlice.reducer, + [documentSlice.name]: documentSlice.reducer, [workspaceSlice.name]: workspaceSlice.reducer, [errorSlice.name]: errorSlice.reducer, }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts index c40e840036..57d9b53822 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts @@ -2,6 +2,7 @@ import { createContext } from 'react'; import { ulid } from "ulid"; import { BlockEditor } from '../block_editor/index'; +import { BlockType } from '../interfaces'; export const BlockContext = createContext<{ id?: string; @@ -23,3 +24,11 @@ export function calculateViewportBlockMaxCount() { } +export interface NestedNode { + id: string; + children: string; + parent: string | null; + type: BlockType; + data: any; +} + diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts index e46f5e6179..6bf8d0ebde 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts @@ -49,3 +49,40 @@ export function set(obj: any, path: string[], value: any): void { } } } + +export function isEqual(value1: T, value2: T): boolean { + if (typeof value1 !== 'object' || value1 === null || typeof value2 !== 'object' || value2 === null) { + return value1 === value2; + } + + + if (Array.isArray(value1)) { + if (!Array.isArray(value2) || value1.length !== value2.length) { + return false; + } + + for (let i = 0; i < value1.length; i++) { + if (!isEqual(value1[i], value2[i])) { + return false; + } + } + + return true; + } + + const keys1 = Object.keys(value1); + const keys2 = Object.keys(value2); + + if (keys1.length !== keys2.length) { + return false; + } + + for (const key of keys1) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + if (!isEqual(value1[key], value2[key])) { + return false; + } + } + return true; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts index 1dfe73fd85..c91ba322d4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts @@ -4,796 +4,30 @@ import { DocumentVersionPB, OpenDocumentPayloadPB, } from '../../services/backend/events/flowy-document'; -import { BlockInterface, BlockType } from '../interfaces'; import { useParams } from 'react-router-dom'; -import { BlockEditor } from '../block_editor'; +import { DocumentData } from '../interfaces/document'; +import { YDocController } from '$app/stores/effects/document/document_controller'; -const loadBlockData = async (id: string): Promise> => { - return { - [id]: { - id: id, - type: BlockType.PageBlock, - data: { content: [{ text: 'Document Title' }] }, - next: null, - firstChild: "L1-1", - }, - "L1-1": { - id: "L1-1", - type: BlockType.HeadingBlock, - data: { level: 1, content: [{ text: 'Heading 1' }] }, - next: "L1-2", - firstChild: null, - }, - "L1-2": { - id: "L1-2", - type: BlockType.HeadingBlock, - data: { level: 2, content: [{ text: 'Heading 2' }] }, - next: "L1-3", - firstChild: null, - }, - "L1-3": { - id: "L1-3", - type: BlockType.HeadingBlock, - data: { level: 3, content: [{ text: 'Heading 3' }] }, - next: "L1-4", - firstChild: null, - }, - "L1-4": { - id: "L1-4", - type: BlockType.TextBlock, - data: { content: [ - { - text: - 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', - }, - { text: 'bold', bold: true }, - { text: ', ' }, - { text: 'italic', italic: true }, - { text: ', or anything else you might want to do!' }, - ] }, - next: "L1-5", - firstChild: null, - }, - "L1-5": { - id: "L1-5", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - { text: 'select any piece of text and the menu will appear', bold: true }, - { text: '.' }, - ] }, - next: "L1-6", - firstChild: "L1-5-1", - }, - "L1-5-1": { - id: "L1-5-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: "L1-5-2", - firstChild: null, - }, - "L1-5-2": { - id: "L1-5-2", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L1-6": { - id: "L1-6", - type: BlockType.ListBlock, - data: { type: 'bulleted', content: [ - { - text: - "Since it's rich text, you can do things like turn a selection of text ", - }, - { text: 'bold', bold: true }, - { - text: - ', or add a semantically rendered block quote in the middle of the page, like this:', - }, - ] }, - next: "L1-7", - firstChild: "L1-6-1", - }, - "L1-6-1": { - id: "L1-6-1", - type: BlockType.ListBlock, - data: { type: 'numbered', content: [ - { - text: - "Since it's rich text, you can do things like turn a selection of text ", - }, - - ] }, - - next: "L1-6-2", - firstChild: null, - }, - "L1-6-2": { - id: "L1-6-2", - type: BlockType.ListBlock, - data: { type: 'numbered', content: [ - { - text: - "Since it's rich text, you can do things like turn a selection of text ", - }, - - ] }, - - next: "L1-6-3", - firstChild: null, - }, - "L1-6-3": { - id: "L1-6-3", - type: BlockType.TextBlock, - data: { content: [{ text: 'A wise quote.' }] }, - next: null, - firstChild: null, - }, - - "L1-7": { - id: "L1-7", - type: BlockType.ListBlock, - data: { type: 'column' }, - - next: "L1-8", - firstChild: "L1-7-1", - }, - "L1-7-1": { - id: "L1-7-1", - type: BlockType.ColumnBlock, - data: { ratio: '0.33' }, - next: "L1-7-2", - firstChild: "L1-7-1-1", - }, - "L1-7-1-1": { - id: "L1-7-1-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L1-7-2": { - id: "L1-7-2", - type: BlockType.ColumnBlock, - data: { ratio: '0.33' }, - next: "L1-7-3", - firstChild: "L1-7-2-1", - }, - "L1-7-2-1": { - id: "L1-7-2-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: "L1-7-2-2", - firstChild: null, - }, - "L1-7-2-2": { - id: "L1-7-2-2", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L1-7-3": { - id: "L1-7-3", - type: BlockType.ColumnBlock, - data: { ratio: '0.33' }, - next: null, - firstChild: "L1-7-3-1", - }, - "L1-7-3-1": { - id: "L1-7-3-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L1-8": { - id: "L1-8", - type: BlockType.HeadingBlock, - data: { level: 1, content: [{ text: 'Heading 1' }] }, - next: "L1-9", - firstChild: null, - }, - "L1-9": { - id: "L1-9", - type: BlockType.HeadingBlock, - data: { level: 2, content: [{ text: 'Heading 2' }] }, - next: "L1-10", - firstChild: null, - }, - "L1-10": { - id: "L1-10", - type: BlockType.HeadingBlock, - data: { level: 3, content: [{ text: 'Heading 3' }] }, - next: "L1-11", - firstChild: null, - }, - "L1-11": { - id: "L1-11", - type: BlockType.TextBlock, - data: { content: [ - { - text: - 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', - }, - { text: 'bold', bold: true }, - { text: ', ' }, - { text: 'italic', italic: true }, - { text: ', or anything else you might want to do!' }, - ] }, - next: "L1-12", - firstChild: null, - }, - "L1-12": { - id: "L1-12", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - { text: 'select any piece of text and the menu will appear', bold: true }, - { text: '.' }, - ] }, - next: "L2-1", - firstChild: "L1-12-1", - }, - "L1-12-1": { - id: "L1-12-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: "L1-12-2", - firstChild: null, - }, - "L1-12-2": { - id: "L1-12-2", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L2-1": { - id: "L2-1", - type: BlockType.HeadingBlock, - data: { level: 1, content: [{ text: 'Heading 1' }] }, - next: "L2-2", - firstChild: null, - }, - "L2-2": { - id: "L2-2", - type: BlockType.HeadingBlock, - data: { level: 2, content: [{ text: 'Heading 2' }] }, - next: "L2-3", - firstChild: null, - }, - "L2-3": { - id: "L2-3", - type: BlockType.HeadingBlock, - data: { level: 3, content: [{ text: 'Heading 3' }] }, - next: "L2-4", - firstChild: null, - }, - "L2-4": { - id: "L2-4", - type: BlockType.TextBlock, - data: { content: [ - { - text: - 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', - }, - { text: 'bold', bold: true }, - { text: ', ' }, - { text: 'italic', italic: true }, - { text: ', or anything else you might want to do!' }, - ] }, - next: "L2-5", - firstChild: null, - }, - "L2-5": { - id: "L2-5", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - { text: 'select any piece of text and the menu will appear', bold: true }, - { text: '.' }, - ] }, - next: "L2-6", - firstChild: "L2-5-1", - }, - "L2-5-1": { - id: "L2-5-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: "L2-5-2", - firstChild: null, - }, - "L2-5-2": { - id: "L2-5-2", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L2-6": { - id: "L2-6", - type: BlockType.ListBlock, - data: { type: 'bulleted', content: [ - { - text: - "Since it's rich text, you can do things like turn a selection of text ", - }, - { text: 'bold', bold: true }, - { - text: - ', or add a semantically rendered block quote in the middle of the page, like this:', - }, - ] }, - next: "L2-7", - firstChild: "L2-6-1", - }, - "L2-6-1": { - id: "L2-6-1", - type: BlockType.ListBlock, - data: { type: 'numbered', content: [ - { - text: - "Since it's rich text, you can do things like turn a selection of text ", - }, - - ] }, - - next: "L2-6-2", - firstChild: null, - }, - "L2-6-2": { - id: "L2-6-2", - type: BlockType.ListBlock, - data: { type: 'numbered', content: [ - { - text: - "Since it's rich text, you can do things like turn a selection of text ", - }, - - ] }, - - next: "L2-6-3", - firstChild: null, - }, - - "L2-6-3": { - id: "L2-6-3", - type: BlockType.TextBlock, - data: { content: [{ text: 'A wise quote.' }] }, - next: null, - firstChild: null, - }, - - "L2-7": { - id: "L2-7", - type: BlockType.ListBlock, - data: { type: 'column' }, - - next: "L2-8", - firstChild: "L2-7-1", - }, - "L2-7-1": { - id: "L2-7-1", - type: BlockType.ColumnBlock, - data: { ratio: '0.33' }, - next: "L2-7-2", - firstChild: "L2-7-1-1", - }, - "L2-7-1-1": { - id: "L2-7-1-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L2-7-2": { - id: "L2-7-2", - type: BlockType.ColumnBlock, - data: { ratio: '0.33' }, - next: "L2-7-3", - firstChild: "L2-7-2-1", - }, - "L2-7-2-1": { - id: "L2-7-2-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: "L2-7-2-2", - firstChild: null, - }, - "L2-7-2-2": { - id: "L2-7-2-2", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L2-7-3": { - id: "L2-7-3", - type: BlockType.ColumnBlock, - data: { ratio: '0.33' }, - next: null, - firstChild: "L2-7-3-1", - }, - "L2-7-3-1": { - id: "L2-7-3-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L2-8": { - id: "L2-8", - type: BlockType.HeadingBlock, - data: { level: 1, content: [{ text: 'Heading 1' }] }, - next: "L2-9", - firstChild: null, - }, - "L2-9": { - id: "L2-9", - type: BlockType.HeadingBlock, - data: { level: 2, content: [{ text: 'Heading 2' }] }, - next: "L2-10", - firstChild: null, - }, - "L2-10": { - id: "L2-10", - type: BlockType.HeadingBlock, - data: { level: 3, content: [{ text: 'Heading 3' }] }, - next: "L2-11", - firstChild: null, - }, - "L2-11": { - id: "L2-11", - type: BlockType.TextBlock, - data: { content: [ - { - text: - 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', - }, - { text: 'bold', bold: true }, - { text: ', ' }, - { text: 'italic', italic: true }, - { text: ', or anything else you might want to do!' }, - ] }, - next: "L2-12", - firstChild: null, - }, - "L2-12": { - id: "L2-12", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - { text: 'select any piece of text and the menu will appear', bold: true }, - { text: '.' }, - ] }, - next: "L3-1", - firstChild: "L2-12-1", - }, - "L2-12-1": { - id: "L2-12-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: "L2-12-2", - firstChild: null, - }, - "L2-12-2": { - id: "L2-12-2", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - },"L3-1": { - id: "L3-1", - type: BlockType.HeadingBlock, - data: { level: 1, content: [{ text: 'Heading 1' }] }, - next: "L3-2", - firstChild: null, - }, - "L3-2": { - id: "L3-2", - type: BlockType.HeadingBlock, - data: { level: 2, content: [{ text: 'Heading 2' }] }, - next: "L3-3", - firstChild: null, - }, - "L3-3": { - id: "L3-3", - type: BlockType.HeadingBlock, - data: { level: 3, content: [{ text: 'Heading 3' }] }, - next: "L3-4", - firstChild: null, - }, - "L3-4": { - id: "L3-4", - type: BlockType.TextBlock, - data: { content: [ - { - text: - 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', - }, - { text: 'bold', bold: true }, - { text: ', ' }, - { text: 'italic', italic: true }, - { text: ', or anything else you might want to do!' }, - ] }, - next: "L3-5", - firstChild: null, - }, - "L3-5": { - id: "L3-5", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - { text: 'select any piece of text and the menu will appear', bold: true }, - { text: '.' }, - ] }, - next: "L3-6", - firstChild: "L3-5-1", - }, - "L3-5-1": { - id: "L3-5-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: "L3-5-2", - firstChild: null, - }, - "L3-5-2": { - id: "L3-5-2", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L3-6": { - id: "L3-6", - type: BlockType.ListBlock, - data: { type: 'bulleted', content: [ - { - text: - "Since it's rich text, you can do things like turn a selection of text ", - }, - { text: 'bold', bold: true }, - { - text: - ', or add a semantically rendered block quote in the middle of the page, like this:', - }, - ] }, - next: "L3-7", - firstChild: "L3-6-1", - }, - "L3-6-1": { - id: "L3-6-1", - type: BlockType.ListBlock, - data: { type: 'numbered', content: [ - { - text: - "Since it's rich text, you can do things like turn a selection of text ", - }, - - ] }, - - next: "L3-6-2", - firstChild: null, - }, - "L3-6-2": { - id: "L3-6-2", - type: BlockType.ListBlock, - data: { type: 'numbered', content: [ - { - text: - "Since it's rich text, you can do things like turn a selection of text ", - }, - - ] }, - - next: "L3-6-3", - firstChild: null, - }, - - "L3-6-3": { - id: "L3-6-3", - type: BlockType.TextBlock, - data: { content: [{ text: 'A wise quote.' }] }, - next: null, - firstChild: null, - }, - - "L3-7": { - id: "L3-7", - type: BlockType.ListBlock, - data: { type: 'column' }, - - next: "L3-8", - firstChild: "L3-7-1", - }, - "L3-7-1": { - id: "L3-7-1", - type: BlockType.ColumnBlock, - data: { ratio: '0.33' }, - next: "L3-7-2", - firstChild: "L3-7-1-1", - }, - "L3-7-1-1": { - id: "L3-7-1-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L3-7-2": { - id: "L3-7-2", - type: BlockType.ColumnBlock, - data: { ratio: '0.33' }, - next: "L3-7-3", - firstChild: "L3-7-2-1", - }, - "L3-7-2-1": { - id: "L3-7-2-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: "L3-7-2-2", - firstChild: null, - }, - "L3-7-2-2": { - id: "L3-7-2-2", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L3-7-3": { - id: "L3-7-3", - type: BlockType.ColumnBlock, - data: { ratio: '0.33' }, - next: null, - firstChild: "L3-7-3-1", - }, - "L3-7-3-1": { - id: "L3-7-3-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - "L3-8": { - id: "L3-8", - type: BlockType.HeadingBlock, - data: { level: 1, content: [{ text: 'Heading 1' }] }, - next: "L3-9", - firstChild: null, - }, - "L3-9": { - id: "L3-9", - type: BlockType.HeadingBlock, - data: { level: 2, content: [{ text: 'Heading 2' }] }, - next: "L3-10", - firstChild: null, - }, - "L3-10": { - id: "L3-10", - type: BlockType.HeadingBlock, - data: { level: 3, content: [{ text: 'Heading 3' }] }, - next: "L3-11", - firstChild: null, - }, - "L3-11": { - id: "L3-11", - type: BlockType.TextBlock, - data: { content: [ - { - text: - 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', - }, - { text: 'bold', bold: true }, - { text: ', ' }, - { text: 'italic', italic: true }, - { text: ', or anything else you might want to do!' }, - ] }, - next: "L3-12", - firstChild: null, - }, - "L3-12": { - id: "L3-12", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - { text: 'select any piece of text and the menu will appear', bold: true }, - { text: '.' }, - ] }, - next: null, - firstChild: "L3-12-1", - }, - "L3-12-1": { - id: "L3-12-1", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: "L3-12-2", - firstChild: null, - }, - "L3-12-2": { - id: "L3-12-2", - type: BlockType.TextBlock, - data: { content: [ - { text: 'Try it out yourself! Just ' }, - ] }, - next: null, - firstChild: null, - }, - } -} export const useDocument = () => { const params = useParams(); - const [blockId, setBlockId] = useState(); - const blockEditorRef = useRef(null) - + const [ documentId, setDocumentId ] = useState(); + const [ documentData, setDocumentData ] = useState(); + const [ controller, setController ] = useState(null); useEffect(() => { void (async () => { if (!params?.id) return; - const data = await loadBlockData(params.id); - console.log('==== enter ====', params?.id, data); - - if (!blockEditorRef.current) { - blockEditorRef.current = new BlockEditor(params?.id, data); - } else { - blockEditorRef.current.changeDoc(params?.id, data); - } - - setBlockId(params.id) + const c = new YDocController(params.id); + setController(c); + const res = await c.open(); + console.log(res) + setDocumentData(res) + setDocumentId(params.id) })(); return () => { console.log('==== leave ====', params?.id) } }, [params.id]); - return { blockId, blockEditor: blockEditorRef.current }; + return { documentId, documentData, controller }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx index 8ab2e71b07..7386c106a6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx @@ -1,27 +1,23 @@ import { useDocument } from './DocumentPage.hooks'; -import BlockList from '../components/block/BlockList'; -import { BlockContext } from '../utils/block'; import { createTheme, ThemeProvider } from '@mui/material'; +import Root from '../components/document/Root'; +import { YDocControllerContext } from '../stores/effects/document/document_controller'; const theme = createTheme({ typography: { fontFamily: ['Poppins'].join(','), }, }); -export const DocumentPage = () => { - const { blockId, blockEditor } = useDocument(); - if (!blockId || !blockEditor) return
; +export const DocumentPage = () => { + const { documentId, documentData, controller } = useDocument(); + + if (!documentId || !documentData || !controller) return null; return ( - - - + + + ); }; From 35c21c0d842797a91946fcd3c09553a6d2e1878e Mon Sep 17 00:00:00 2001 From: qinluhe Date: Mon, 27 Mar 2023 14:40:07 +0800 Subject: [PATCH 5/7] refactor: document component --- .../block_editor/blocks/text_block/index.ts | 123 ---------- .../blocks/text_block/text_selection.ts | 35 --- .../appflowy_app/block_editor/core/block.ts | 107 --------- .../block_editor/core/block_chain.ts | 226 ------------------ .../block_editor/core/op_adapter.ts | 16 -- .../block_editor/core/operation.ts | 167 ------------- .../appflowy_app/block_editor/core/sync.ts | 48 ---- .../src/appflowy_app/block_editor/index.ts | 60 ----- .../block_editor/view/block_position.ts | 87 ------- .../appflowy_app/block_editor/view/tree.ts | 173 -------------- .../block_editor/view/tree_node.ts | 78 ------ .../BlockComponent/BlockComponet.hooks.ts | 36 --- .../components/block/BlockComponent/index.tsx | 91 ------- .../block/BlockList/BlockList.hooks.tsx | 91 ------- .../block/BlockList/BlockListTitle.tsx | 18 -- .../block/BlockList/ListFallbackComponent.tsx | 31 --- .../components/block/BlockList/Overlay.tsx | 14 -- .../components/block/BlockList/index.tsx | 58 ----- .../components/block/BlockPortal/index.tsx | 9 - .../BlockSideTools/BlockSideTools.hooks.tsx | 64 ----- .../components/block/CodeBlock/index.tsx | 6 - .../components/block/HeadingBlock/index.tsx | 17 -- .../block/HoveringToolbar/FormatButton.tsx | 32 --- .../block/HoveringToolbar/FormatIcon.tsx | 20 -- .../block/HoveringToolbar/index.hooks.ts | 36 --- .../block/HoveringToolbar/index.tsx | 31 --- .../block/ListBlock/ColumnListBlock.tsx | 18 -- .../block/ListBlock/NumberedListBlock.tsx | 31 --- .../components/block/PageBlock/index.tsx | 6 - .../components/block/TextBlock/Leaf.tsx | 41 ---- .../components/block/TextBlock/index.hooks.ts | 118 --------- .../components/block/TextBlock/index.tsx | 43 ---- .../components/document/BlockPortal/index.tsx | 2 +- .../BlockSelection/BlockSelection.hooks.tsx | 27 +-- .../BlockSelection/index.tsx | 4 - .../BlockSideTools/BlockSideTools.hooks.tsx | 126 ++++++++++ .../BlockSideTools/index.tsx | 11 +- .../components/document/CodeBlock/index.tsx | 3 + .../{block => document}/ColumnBlock/index.tsx | 21 +- .../DocumentTitle/DocumentTitle.hooks.ts | 5 +- .../document/DocumentTitle/index.tsx | 5 +- .../document/HeadingBlock/index.tsx | 17 ++ .../document/HoveringToolbar/index.hooks.ts | 2 +- .../ListBlock/BulletedListBlock.tsx | 21 +- .../document/ListBlock/ColumnListBlock.tsx | 23 ++ .../document/ListBlock/NumberedListBlock.tsx | 30 +++ .../{block => document}/ListBlock/index.tsx | 18 +- .../components/document/Node/Node.hooks.ts | 27 ++- .../components/document/Node/index.tsx | 17 +- .../components/document/Overlay/index.tsx | 13 + .../components/document/Root/Tree.hooks.tsx | 60 +---- .../document/TextBlock/BindYjs.hooks.ts | 3 +- .../document/TextBlock/TextBlock.hooks.ts | 46 +++- .../components/document/TextBlock/index.tsx | 12 +- .../document/VirtualizerList/index.tsx | 63 ++--- .../document/_shared/SubscribeNode.hooks.ts | 28 ++- .../effects/document/document_controller.ts | 88 ++++++- .../reducers/document}/region_grid.ts | 17 +- .../stores/reducers/document/slice.ts | 86 +++++-- .../src/appflowy_app/utils/block.ts | 34 --- 60 files changed, 580 insertions(+), 2160 deletions(-) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/Overlay.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockPortal/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/BlockSideTools.hooks.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatButton.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatIcon.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.hooks.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/BlockSelection/BlockSelection.hooks.tsx (84%) rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/BlockSelection/index.tsx (84%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/BlockSideTools/index.tsx (78%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/ColumnBlock/index.tsx (59%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/ListBlock/BulletedListBlock.tsx (54%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{block => document}/ListBlock/index.tsx (51%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx rename frontend/appflowy_tauri/src/appflowy_app/{block_editor/view => stores/reducers/document}/region_grid.ts (79%) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block.ts diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts deleted file mode 100644 index 13a5ac74f8..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { BaseEditor, BaseSelection, Descendant, Editor, Transforms } from "slate"; -import { TreeNode } from '$app/block_editor/view/tree_node'; -import { Operation } from "$app/block_editor/core/operation"; -import { TextBlockSelectionManager } from './text_selection'; -import { BlockType } from "@/appflowy_app/interfaces"; -import { ReactEditor } from "slate-react"; - -export class TextBlockManager { - public selectionManager: TextBlockSelectionManager; - private editorMap: Map = new Map(); - - constructor(private rootId: string, private operation: Operation) { - this.selectionManager = new TextBlockSelectionManager(); - } - - register(id: string, editor: BaseEditor & ReactEditor) { - this.editorMap.set(id, editor); - } - - unregister(id: string) { - this.editorMap.delete(id); - } - - setSelection(node: TreeNode, selection: BaseSelection) { - // console.log(node.id, selection); - this.selectionManager.setSelection(node.id, selection) - } - - update(node: TreeNode, path: string[], data: Descendant[]) { - this.operation.updateNode(node.id, path, data); - } - - deleteNode(node: TreeNode) { - if (node.type !== BlockType.TextBlock) { - this.operation.updateNode(node.id, ['type'], BlockType.TextBlock); - this.operation.updateNode(node.id, ['data'], { content: node.data.content }); - return; - } - - if (!node.block.next && node.parent!.id !== this.rootId) { - const newParent = node.parent!.parent!; - const newPrev = node.parent; - this.operation.moveNode(node.id, newParent.id, newPrev?.id || ''); - return; - } - if (!node.prevLine) return; - - const retainData = node.prevLine.data.content; - const editor = this.editorMap.get(node.prevLine.id); - if (editor) { - const index = retainData.length - 1; - const anchor = { - path: [0, index], - offset: retainData[index].text.length, - }; - const selection = { - anchor, - focus: {...anchor} - }; - ReactEditor.focus(editor); - Transforms.select(editor, selection); - } - - this.operation.updateNode(node.prevLine.id, ['data', 'content'], [ - ...retainData, - ...node.data.content, - ]); - - this.operation.deleteNode(node.id); - - } - - splitNode(node: TreeNode, editor: BaseEditor) { - const focus = editor.selection?.focus; - const path = focus?.path || [0, editor.children.length - 1]; - const offset = focus?.offset || 0; - const parentIndex = path[0]; - const index = path[1]; - const editorNode = editor.children[parentIndex]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const children: { [key: string]: boolean | string; text: string }[] = editorNode.children; - const retainItems = children.filter((_: any, i: number) => i < index); - const splitItem: { [key: string]: boolean | string } = children[index]; - const text = splitItem.text.toString(); - const prevText = text.substring(0, offset); - const afterText = text.substring(offset); - retainItems.push({ - ...splitItem, - text: prevText - }); - - const removeItems = children.filter((_: any, i: number) => i > index); - - const data = { - type: node.type, - data: { - ...node.data, - content: [ - { - ...splitItem, - text: afterText - }, - ...removeItems - ] - } - }; - - const newBlock = this.operation.splitNode(node.id, { - path: ['data', 'content'], - value: retainItems, - }, data); - newBlock && this.selectionManager.focusStart(newBlock.id); - } - - destroy() { - this.selectionManager.destroy(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.operation = null; - } - -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts deleted file mode 100644 index b25d7f6268..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts +++ /dev/null @@ -1,35 +0,0 @@ -export class TextBlockSelectionManager { - private focusId = ''; - private selection?: any; - - getFocusSelection() { - return { - focusId: this.focusId, - selection: this.selection - } - } - - focusStart(blockId: string) { - this.focusId = blockId; - this.setSelection(blockId, { - focus: { - path: [0, 0], - offset: 0, - }, - anchor: { - path: [0, 0], - offset: 0, - }, - }) - } - - setSelection(blockId: string, selection: any) { - this.focusId = blockId; - this.selection = selection; - } - - destroy() { - this.focusId = ''; - this.selection = undefined; - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts deleted file mode 100644 index c550213daa..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { BlockType, BlockData } from '$app/interfaces/index'; -import { generateBlockId } from '$app/utils/block'; - -/** - * Represents a single block of content in a document. - */ -export class Block { - id: string; - type: T; - data: BlockData; - parent: Block | null = null; // Pointer to the parent block - prev: Block | null = null; // Pointer to the previous sibling block - next: Block | null = null; // Pointer to the next sibling block - firstChild: Block | null = null; // Pointer to the first child block - - constructor(id: string, type: T, data: BlockData) { - this.id = id; - this.type = type; - this.data = data; - } - - /** - * Adds a new child block to the beginning of the current block's children list. - * - * @param {Object} content - The content of the new block, including its type and data. - * @param {string} content.type - The type of the new block. - * @param {Object} content.data - The data associated with the new block. - * @returns {Block} The newly created child block. - */ - prependChild(content: { type: T, data: BlockData }): Block | null { - const id = generateBlockId(); - const newBlock = new Block(id, content.type, content.data); - newBlock.reposition(this, null); - return newBlock; - } - - /** - * Add a new sibling block after this block. - * - * @param content The type and data for the new sibling block. - * @returns The newly created sibling block. - */ - addSibling(content: { type: T, data: BlockData }): Block | null { - const id = generateBlockId(); - const newBlock = new Block(id, content.type, content.data); - newBlock.reposition(this.parent, this); - return newBlock; - } - - /** - * Remove this block and its descendants from the tree. - * - */ - remove() { - this.detach(); - let child = this.firstChild; - while (child) { - const next = child.next; - child.remove(); - child = next; - } - } - - reposition(newParent: Block | null, newPrev: Block | null) { - // Update the block's parent and siblings - this.parent = newParent; - this.prev = newPrev; - this.next = null; - - if (newParent) { - const prev = newPrev; - if (!prev) { - const next = newParent.firstChild; - newParent.firstChild = this; - if (next) { - this.next = next; - next.prev = this; - } - - } else { - // Update the next and prev pointers of the newPrev and next blocks - if (prev.next !== this) { - const next = prev.next; - if (next) { - next.prev = this - this.next = next; - } - prev.next = this; - } - } - - } - } - - // detach the block from its current position in the tree - detach() { - if (this.prev) { - this.prev.next = this.next; - } else if (this.parent) { - this.parent.firstChild = this.next; - } - if (this.next) { - this.next.prev = this.prev; - } - } - -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts deleted file mode 100644 index 0dbf0ee2b9..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { BlockData, BlockInterface, BlockType } from '$app/interfaces/index'; -import { set } from '../../utils/tool'; -import { Block } from './block'; -export interface BlockChangeProps { - block?: Block, - startBlock?: Block, - endBlock?: Block, - oldParentId?: string, - oldPrevId?: string -} -export class BlockChain { - private map: Map> = new Map(); - public head: Block | null = null; - - constructor(private onBlockChange: (command: string, data: BlockChangeProps) => void) { - - } - /** - * generate blocks from doc data - * @param id doc id - * @param map doc data - */ - rebuild = (id: string, map: Record>) => { - this.map.clear(); - this.head = this.createBlock(id, map[id].type, map[id].data); - - const callback = (block: Block) => { - const firstChildId = map[block.id].firstChild; - const nextId = map[block.id].next; - if (!block.firstChild && firstChildId) { - block.firstChild = this.createBlock(firstChildId, map[firstChildId].type, map[firstChildId].data); - block.firstChild.parent = block; - block.firstChild.prev = null; - } - if (!block.next && nextId) { - block.next = this.createBlock(nextId, map[nextId].type, map[nextId].data); - block.next.parent = block.parent; - block.next.prev = block; - } - } - this.traverse(callback); - } - - /** - * Traversing the block list from front to back - * @param callback It will be call when the block visited - * @param block block item, it will be equal head node when the block item is undefined - */ - traverse(callback: (_block: Block) => void, block?: Block) { - let currentBlock: Block | null = block || this.head; - while (currentBlock) { - callback(currentBlock); - if (currentBlock.firstChild) { - this.traverse(callback, currentBlock.firstChild); - } - currentBlock = currentBlock.next; - } - } - - /** - * get block data - * @param blockId string - * @returns Block - */ - getBlock = (blockId: string) => { - return this.map.get(blockId) || null; - } - - destroy() { - this.map.clear(); - this.head = null; - this.onBlockChange = () => null; - } - - /** - * Adds a new child block to the beginning of the current block's children list. - * - * @param {string} parentId - * @param {Object} content - The content of the new block, including its type and data. - * @param {string} content.type - The type of the new block. - * @param {Object} content.data - The data associated with the new block. - * @returns {Block} The newly created child block. - */ - prependChild(blockId: string, content: { type: BlockType, data: BlockData }): Block | null { - const parent = this.getBlock(blockId); - if (!parent) return null; - const newBlock = parent.prependChild(content); - - if (newBlock) { - this.map.set(newBlock?.id, newBlock); - this.onBlockChange('insert', { block: newBlock }); - } - - return newBlock; - } - - /** - * Add a new sibling block after this block. - * @param {string} blockId - * @param content The type and data for the new sibling block. - * @returns The newly created sibling block. - */ - addSibling(blockId: string, content: { type: BlockType, data: BlockData }): Block | null { - const block = this.getBlock(blockId); - if (!block) return null; - const newBlock = block.addSibling(content); - if (newBlock) { - this.map.set(newBlock?.id, newBlock); - this.onBlockChange('insert', { block: newBlock }); - } - return newBlock; - } - - /** - * Remove this block and its descendants from the tree. - * @param {string} blockId - */ - remove(blockId: string) { - const block = this.getBlock(blockId); - if (!block) return; - const oldParentId = block.parent?.id; - block.remove(); - this.map.delete(block.id); - this.onBlockChange('remove', { oldParentId }); - return block; - } - - /** - * Move this block to a new position in the tree. - * @param {string} blockId - * @param newParentId The new parent block of this block. If null, the block becomes a top-level block. - * @param newPrevId The new previous sibling block of this block. If null, the block becomes the first child of the new parent. - * @returns This block after it has been moved. - */ - move(blockId: string, newParentId: string, newPrevId: string): Block | null { - const block = this.getBlock(blockId); - if (!block) return null; - const oldParentId = block.parent?.id; - const oldPrevId = block.prev?.id; - block.detach(); - const newParent = this.getBlock(newParentId); - const newPrev = this.getBlock(newPrevId); - block.reposition(newParent, newPrev); - this.onBlockChange('move', { - block, - oldParentId, - oldPrevId - }); - return block; - } - - updateBlock(id: string, data: { path: string[], value: any }) { - const block = this.getBlock(id); - if (!block) return null; - - set(block, data.path, data.value); - this.onBlockChange('update', { - block - }); - return block; - } - - - moveBulk(startBlockId: string, endBlockId: string, newParentId: string, newPrevId: string): [Block, Block] | null { - const startBlock = this.getBlock(startBlockId); - const endBlock = this.getBlock(endBlockId); - if (!startBlock || !endBlock) return null; - - if (startBlockId === endBlockId) { - const block = this.move(startBlockId, newParentId, ''); - if (!block) return null; - return [block, block]; - } - - const oldParent = startBlock.parent; - const prev = startBlock.prev; - const newParent = this.getBlock(newParentId); - if (!oldParent || !newParent) return null; - - if (oldParent.firstChild === startBlock) { - oldParent.firstChild = endBlock.next; - } else if (prev) { - prev.next = endBlock.next; - } - startBlock.prev = null; - endBlock.next = null; - - startBlock.parent = newParent; - endBlock.parent = newParent; - const newPrev = this.getBlock(newPrevId); - if (!newPrev) { - const firstChild = newParent.firstChild; - newParent.firstChild = startBlock; - if (firstChild) { - endBlock.next = firstChild; - firstChild.prev = endBlock; - } - } else { - const next = newPrev.next; - newPrev.next = startBlock; - endBlock.next = next; - if (next) { - next.prev = endBlock; - } - } - - this.onBlockChange('move', { - startBlock, - endBlock, - oldParentId: oldParent.id, - oldPrevId: prev?.id - }); - - return [ - startBlock, - endBlock - ]; - } - - - private createBlock(id: string, type: BlockType, data: BlockData) { - const block = new Block(id, type, data); - this.map.set(id, block); - return block; - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts deleted file mode 100644 index 0c5c0b3190..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BackendOp, LocalOp } from "$app/interfaces"; - -export class OpAdapter { - - toBackendOp(localOp: LocalOp): BackendOp { - const backendOp: BackendOp = { ...localOp }; - // switch localOp type and generate backendOp - return backendOp; - } - - toLocalOp(backendOp: BackendOp): LocalOp { - const localOp: LocalOp = { ...backendOp }; - // switch backendOp type and generate localOp - return localOp; - } -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts deleted file mode 100644 index 1755e05f8a..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { BlockChain } from './block_chain'; -import { BlockInterface, BlockType, InsertOpData, LocalOp, UpdateOpData, moveOpData, moveRangeOpData, removeOpData, BlockData } from '$app/interfaces'; -import { BlockEditorSync } from './sync'; -import { Block } from './block'; - -export class Operation { - private sync: BlockEditorSync; - - constructor(private blockChain: BlockChain) { - this.sync = new BlockEditorSync(); - } - - - splitNode( - retainId: string, - retainData: { path: string[], value: any }, - newBlockData: { - type: BlockType; - data: BlockData - }) { - const ops: { - type: LocalOp['type']; - data: LocalOp['data']; - }[] = []; - const newBlock = this.blockChain.addSibling(retainId, newBlockData); - const parentId = newBlock?.parent?.id; - const retainBlock = this.blockChain.getBlock(retainId); - if (!newBlock || !parentId || !retainBlock) return null; - - const insertOp = this.getInsertNodeOp({ - id: newBlock.id, - next: newBlock.next?.id || null, - firstChild: newBlock.firstChild?.id || null, - data: newBlock.data, - type: newBlock.type, - }, parentId, retainId); - - const updateOp = this.getUpdateNodeOp(retainId, retainData.path, retainData.value); - this.blockChain.updateBlock(retainId, retainData); - - ops.push(insertOp, updateOp); - const startBlock = retainBlock.firstChild; - if (startBlock) { - const startBlockId = startBlock.id; - let next: Block | null = startBlock.next; - let endBlockId = startBlockId; - while (next) { - endBlockId = next.id; - next = next.next; - } - - const moveOp = this.getMoveRangeOp([startBlockId, endBlockId], newBlock.id); - this.blockChain.moveBulk(startBlockId, endBlockId, newBlock.id, ''); - ops.push(moveOp); - } - - this.sync.sendOps(ops); - - return newBlock; - } - - updateNode(blockId: string, path: string[], value: T) { - const op = this.getUpdateNodeOp(blockId, path, value); - this.blockChain.updateBlock(blockId, { - path, - value - }); - this.sync.sendOps([op]); - } - - moveNode(blockId: string, newParentId: string, newPrevId: string) { - const op = this.getMoveOp(blockId, newParentId, newPrevId); - this.blockChain.move(blockId, newParentId, newPrevId); - this.sync.sendOps([op]); - } - - deleteNode(blockId: string) { - const op = this.getRemoveOp(blockId); - this.blockChain.remove(blockId); - this.sync.sendOps([op]); - } - - private getUpdateNodeOp(blockId: string, path: string[], value: T): { - type: 'update', - data: UpdateOpData - } { - return { - type: 'update', - data: { - blockId, - path: path, - value - } - }; - } - - private getInsertNodeOp(block: T, parentId: string, prevId?: string): { - type: 'insert'; - data: InsertOpData - } { - return { - type: 'insert', - data: { - block, - parentId, - prevId - } - } - } - - private getMoveRangeOp(range: [string, string], newParentId: string, newPrevId?: string): { - type: 'move_range', - data: moveRangeOpData - } { - return { - type: 'move_range', - data: { - range, - newParentId, - newPrevId, - } - } - } - - private getMoveOp(blockId: string, newParentId: string, newPrevId?: string): { - type: 'move', - data: moveOpData - } { - return { - type: 'move', - data: { - blockId, - newParentId, - newPrevId - } - } - } - - private getRemoveOp(blockId: string): { - type: 'remove' - data: removeOpData - } { - return { - type: 'remove', - data: { - blockId - } - } - } - - applyOperation(op: LocalOp) { - switch (op.type) { - case 'insert': - - break; - - default: - break; - } - } - - destroy() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.blockChain = null; - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts deleted file mode 100644 index 24070c0cd5..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { BackendOp, LocalOp } from '$app/interfaces'; -import { OpAdapter } from './op_adapter'; - -/** - * BlockEditorSync is a class that synchronizes changes made to a block chain with a server. - * It allows for adding, removing, and moving blocks in the chain, and sends pending operations to the server. - */ -export class BlockEditorSync { - private version = 0; - private opAdapter: OpAdapter; - private pendingOps: BackendOp[] = []; - private appliedOps: LocalOp[] = []; - - constructor() { - this.opAdapter = new OpAdapter(); - } - - private applyOp(op: BackendOp): void { - const localOp = this.opAdapter.toLocalOp(op); - this.appliedOps.push(localOp); - } - - private receiveOps(ops: BackendOp[]): void { - // Apply the incoming operations to the local document - ops.sort((a, b) => a.version - b.version); - for (const op of ops) { - this.applyOp(op); - } - } - - private resolveConflict(): void { - // Implement conflict resolution logic here - } - - public sendOps(ops: { - type: LocalOp["type"]; - data: LocalOp["data"] - }[]) { - const backendOps = ops.map(op => this.opAdapter.toBackendOp({ - ...op, - version: this.version - })); - this.pendingOps.push(...backendOps); - // Send the pending operations to the server - console.log('==== sync pending ops ====', [...this.pendingOps]); - } - -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts deleted file mode 100644 index 658b284906..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Import dependencies -import { BlockInterface } from '../interfaces'; -import { BlockChain, BlockChangeProps } from './core/block_chain'; -import { RenderTree } from './view/tree'; -import { Operation } from './core/operation'; - -/** - * The BlockEditor class manages a block chain and a render tree for a document editor. - * The block chain stores the content blocks of the document in sequence, while the - * render tree displays the document as a hierarchical tree structure. - */ -export class BlockEditor { - // Public properties - public blockChain: BlockChain; // (local data) the block chain used to store the document - public renderTree: RenderTree; // the render tree used to display the document - public operation: Operation; - /** - * Constructs a new BlockEditor object. - * @param id - the ID of the document - * @param data - the initial data for the document - */ - constructor(private id: string, data: Record) { - // Create the block chain and render tree - this.blockChain = new BlockChain(this.blockChange); - this.operation = new Operation(this.blockChain); - this.changeDoc(id, data); - - this.renderTree = new RenderTree(this.blockChain); - } - - /** - * Updates the document ID and block chain when the document changes. - * @param id - the new ID of the document - * @param data - the updated data for the document - */ - changeDoc = (id: string, data: Record) => { - console.log('==== change document ====', id, data); - - // Update the document ID and rebuild the block chain - this.id = id; - this.blockChain.rebuild(id, data); - } - - - /** - * Destroys the block chain and render tree. - */ - destroy = () => { - // Destroy the block chain and render tree - this.blockChain.destroy(); - this.renderTree.destroy(); - this.operation.destroy(); - } - - private blockChange = (command: string, data: BlockChangeProps) => { - this.renderTree.onBlockChange(command, data); - } - -} - 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 deleted file mode 100644 index ded70389c9..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { RegionGrid, BlockPosition } from './region_grid'; -export class BlockPositionManager { - private regionGrid: RegionGrid; - private viewportBlocks: Set = new Set(); - private blockPositions: Map = new Map(); - private container: HTMLDivElement | null = null; - - constructor(container: HTMLDivElement) { - this.container = container; - this.regionGrid = new RegionGrid(container.offsetHeight); - } - - isInViewport(nodeId: string) { - return this.viewportBlocks.has(nodeId); - } - - observeBlock(node: HTMLDivElement) { - const blockId = node.getAttribute('data-block-id'); - if (blockId) { - this.updateBlockPosition(blockId); - this.viewportBlocks.add(blockId); - } - return { - unobserve: () => { - if (blockId) this.viewportBlocks.delete(blockId); - }, - } - } - - getBlockPosition(blockId: string) { - if (!this.blockPositions.has(blockId)) { - this.updateBlockPosition(blockId); - } - return this.blockPositions.get(blockId); - } - - updateBlockPosition(blockId: string) { - if (!this.container) return; - const node = document.querySelector(`[data-block-id=${blockId}]`) as HTMLDivElement; - if (!node) return; - const rect = node.getBoundingClientRect(); - const position = { - id: blockId, - x: rect.x, - y: rect.y + this.container.scrollTop, - height: rect.height, - width: rect.width - }; - const prevPosition = this.blockPositions.get(blockId); - if (prevPosition && prevPosition.x === position.x && - prevPosition.y === position.y && - prevPosition.height === position.height && - prevPosition.width === position.width) { - return; - } - this.blockPositions.set(blockId, position); - this.regionGrid.removeBlock(blockId); - this.regionGrid.addBlock(position); - } - - getIntersectBlocks(startX: number, startY: number, endX: number, endY: number): BlockPosition[] { - 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; - } - -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts deleted file mode 100644 index 150990e23c..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { BlockChain, BlockChangeProps } from '../core/block_chain'; -import { Block } from '../core/block'; -import { TreeNode } from "./tree_node"; -import { BlockPositionManager } from './block_position'; -import { filterSelections } from '@/appflowy_app/utils/block_selection'; - -export class RenderTree { - public blockPositionManager?: BlockPositionManager; - - private map: Map = new Map(); - private root: TreeNode | null = null; - private selections: Set = new Set(); - constructor(private blockChain: BlockChain) { - } - - - createPositionManager(container: HTMLDivElement) { - this.blockPositionManager = new BlockPositionManager(container); - } - - observeBlock(node: HTMLDivElement) { - return this.blockPositionManager?.observeBlock(node); - } - - getBlockPosition(nodeId: string) { - return this.blockPositionManager?.getBlockPosition(nodeId) || null; - } - /** - * Get the TreeNode data by nodeId - * @param nodeId string - * @returns TreeNode|null - */ - getTreeNode = (nodeId: string): TreeNode | null => { - // Return the TreeNode instance from the map or null if it does not exist - return this.map.get(nodeId) || null; - } - - private createNode(block: Block): TreeNode { - if (this.map.has(block.id)) { - return this.map.get(block.id)!; - } - const node = new TreeNode(block); - this.map.set(block.id, node); - return node; - } - - - buildDeep(rootId: string): TreeNode | null { - this.map.clear(); - // Define a callback function for the blockChain.traverse() method - const callback = (block: Block) => { - // Check if the TreeNode instance already exists in the map - const node = this.createNode(block); - - // Add the TreeNode instance to the map - this.map.set(block.id, node); - - // Add the first child of the block as a child of the current TreeNode instance - const firstChild = block.firstChild; - if (firstChild) { - const child = this.createNode(firstChild); - node.addChild(child); - this.map.set(child.id, child); - } - - // Add the next block as a sibling of the current TreeNode instance - const next = block.next; - if (next) { - const nextNode = this.createNode(next); - node.parent?.addChild(nextNode); - this.map.set(next.id, nextNode); - } - } - - // Traverse the blockChain using the callback function - this.blockChain.traverse(callback); - - // Get the root node from the map and return it - const root = this.map.get(rootId)!; - this.root = root; - return root || null; - } - - - forceUpdate(nodeId: string, shouldUpdateChildren = false) { - const block = this.blockChain.getBlock(nodeId); - if (!block) return null; - const node = this.createNode(block); - if (!node) return null; - if (!shouldUpdateChildren) { - node.update(node.block, node.children); - node.reRender(); - return; - } - - - const children: TreeNode[] = []; - let childBlock = block.firstChild; - - while (childBlock) { - const child = this.createNode(childBlock); - child.update(childBlock, child.children); - children.push(child); - childBlock = childBlock.next; - } - - node.update(block, children); - node.reRender(); - node.children.forEach(child => { - child.reRender(); - }); - } - - onBlockChange(command: string, data: BlockChangeProps) { - const { block, startBlock, endBlock, oldParentId = '', oldPrevId = '' } = data; - switch (command) { - case 'insert': - if (block?.parent) this.forceUpdate(block.parent.id, true); - break; - case 'update': - this.forceUpdate(block!.id); - break; - case 'remove': - if (oldParentId) this.forceUpdate(oldParentId, true); - break; - case 'move': - if (oldParentId) this.forceUpdate(oldParentId, true); - if (block?.parent) this.forceUpdate(block.parent.id, true); - if (startBlock?.parent) this.forceUpdate(startBlock.parent.id, true); - break; - default: - break; - } - - } - - updateSelections(selections: string[]) { - const newSelections = filterSelections(selections, this.map); - - const selectedBlocksSet = new Set(newSelections); - - const updateNotSelected: string[] = []; - const updateSelected: string[] = []; - Array.from(this.selections).forEach((id) => { - if (!selectedBlocksSet.has(id)) { - updateNotSelected.push(id); - } - }); - newSelections.forEach(id => { - if (!this.selections.has(id)) { - updateSelected.push(id); - } - }); - - this.selections = selectedBlocksSet; - [...updateNotSelected, ...updateSelected].forEach((id) => { - this.forceUpdate(id); - }); - } - - isSelected(nodeId: string) { - return this.selections.has(nodeId); - } - - /** - * Destroy the RenderTreeRectManager instance - */ - destroy() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.blockChain = null; - } -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts deleted file mode 100644 index 32cba14edf..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { BlockData, BlockType } from '$app/interfaces/index'; -import { Block } from '../core/block'; - -/** - * Represents a node in a tree structure of blocks. - */ -export class TreeNode { - id: string; - type: BlockType; - parent: TreeNode | null = null; - children: TreeNode[] = []; - data: BlockData; - - private forceUpdate?: () => void; - - /** - * Create a new TreeNode instance. - * @param block - The block data used to create the node. - */ - constructor(private _block: Block) { - this.id = _block.id; - this.data = _block.data; - this.type = _block.type; - } - - registerUpdate(forceUpdate: () => void) { - this.forceUpdate = forceUpdate; - } - - unregisterUpdate() { - this.forceUpdate = undefined; - } - - reRender() { - this.forceUpdate?.(); - } - - update(block: Block, children: TreeNode[]) { - this.type = block.type; - this.data = block.data; - this.children = []; - children.forEach(child => { - this.addChild(child); - }) - } - - /** - * Add a child node to the current node. - * @param node - The child node to add. - */ - addChild(node: TreeNode) { - node.parent = this; - this.children.push(node); - } - - get lastChild() { - return this.children[this.children.length - 1]; - } - - get prevLine(): TreeNode | null { - if (!this.parent) return null; - const index = this.parent?.children.findIndex(item => item.id === this.id); - if (index === 0) { - return this.parent; - } - const prev = this.parent.children[index - 1]; - let line = prev; - while(line.lastChild) { - line = line.lastChild; - } - return line; - } - - get block() { - return this._block; - } - -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts deleted file mode 100644 index 20e31a1793..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect, useState, useRef, useContext } from 'react'; - -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { BlockContext } from '$app/utils/block'; - -export function useBlockComponent({ - node -}: { - node: TreeNode -}) { - const { blockEditor } = useContext(BlockContext); - - const [version, forceUpdate] = useState(0); - const myRef = useRef(null); - - const isSelected = blockEditor?.renderTree.isSelected(node.id); - - useEffect(() => { - if (!myRef.current) { - return; - } - const observe = blockEditor?.renderTree.observeBlock(myRef.current); - node.registerUpdate(() => forceUpdate((prev) => prev + 1)); - - return () => { - node.unregisterUpdate(); - observe?.unobserve(); - }; - }, []); - return { - version, - myRef, - isSelected, - className: `relative my-[1px] px-1` - } -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx deleted file mode 100644 index 9c8ee223dd..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { forwardRef } from 'react'; -import { BlockCommonProps, BlockType } from '$app/interfaces'; -import PageBlock from '../PageBlock'; -import TextBlock from '../TextBlock'; -import HeadingBlock from '../HeadingBlock'; -import ListBlock from '../ListBlock'; -import CodeBlock from '../CodeBlock'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { withErrorBoundary } from 'react-error-boundary'; -import { ErrorBoundaryFallbackComponent } from '../BlockList/BlockList.hooks'; -import { useBlockComponent } from './BlockComponet.hooks'; - -const BlockComponent = forwardRef( - ( - { - node, - renderChild, - ...props - }: { node: TreeNode; renderChild?: (_node: TreeNode) => React.ReactNode } & React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLDivElement - >, - ref: React.ForwardedRef - ) => { - const { myRef, className, version, isSelected } = useBlockComponent({ - node, - }); - - const renderComponent = () => { - let BlockComponentClass: (_: BlockCommonProps) => JSX.Element | null; - switch (node.type) { - case BlockType.PageBlock: - BlockComponentClass = PageBlock; - break; - case BlockType.TextBlock: - BlockComponentClass = TextBlock; - break; - case BlockType.HeadingBlock: - BlockComponentClass = HeadingBlock; - break; - case BlockType.ListBlock: - BlockComponentClass = ListBlock; - break; - case BlockType.CodeBlock: - BlockComponentClass = CodeBlock; - break; - default: - break; - } - - const blockProps: BlockCommonProps = { - version, - node, - }; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (BlockComponentClass) { - return ; - } - return null; - }; - - return ( -
{ - myRef.current = el; - if (typeof ref === 'function') { - ref(el); - } else if (ref) { - ref.current = el; - } - }} - {...props} - data-block-id={node.id} - data-block-selected={isSelected} - className={props.className ? `${props.className} ${className}` : className} - > - {renderComponent()} - {renderChild ? node.children.map(renderChild) : null} -
- {isSelected ?
: null} -
- ); - } -); - -const ComponentWithErrorBoundary = withErrorBoundary(BlockComponent, { - FallbackComponent: ErrorBoundaryFallbackComponent, -}); -export default React.memo(ComponentWithErrorBoundary); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx deleted file mode 100644 index e55bb3f7d7..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { BlockEditor } from '@/appflowy_app/block_editor'; -import { TreeNode } from '$app/block_editor/view/tree_node'; -import { Alert } from '@mui/material'; -import { FallbackProps } from 'react-error-boundary'; -import { TextBlockManager } from '@/appflowy_app/block_editor/blocks/text_block'; -import { TextBlockContext } from '@/appflowy_app/utils/slate/context'; -import { useVirtualizer } from '@tanstack/react-virtual'; -export interface BlockListProps { - blockId: string; - blockEditor: BlockEditor; -} - -const defaultSize = 45; - -export function useBlockList({ blockId, blockEditor }: BlockListProps) { - const [root, setRoot] = useState(null); - - const parentRef = useRef(null); - - const rowVirtualizer = useVirtualizer({ - count: root?.children.length || 0, - getScrollElement: () => parentRef.current, - estimateSize: () => { - return defaultSize; - }, - }); - - const [version, forceUpdate] = useState(0); - - const buildDeepTree = useCallback(() => { - const treeNode = blockEditor.renderTree.buildDeep(blockId); - setRoot(treeNode); - }, [blockEditor]); - - useEffect(() => { - if (!parentRef.current) return; - blockEditor.renderTree.createPositionManager(parentRef.current); - buildDeepTree(); - - return () => { - blockEditor.destroy(); - }; - }, [blockId, blockEditor]); - - useEffect(() => { - root?.registerUpdate(() => forceUpdate((prev) => prev + 1)); - return () => { - root?.unregisterUpdate(); - }; - }, [root]); - - return { - root, - rowVirtualizer, - parentRef, - blockEditor, - }; -} - -export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: FallbackProps) { - return ( - -

Something went wrong:

-
{error.message}
- -
- ); -} - -export function withTextBlockManager(Component: (props: BlockListProps) => React.ReactElement) { - return (props: BlockListProps) => { - const textBlockManager = new TextBlockManager(props.blockId, props.blockEditor.operation); - - useEffect(() => { - return () => { - textBlockManager.destroy(); - }; - }, []); - - return ( - - - - ); - }; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx deleted file mode 100644 index f74ae72283..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import TextBlock from '../TextBlock'; -import { TreeNode } from '$app/block_editor/view/tree_node'; - -export default function BlockListTitle({ node }: { node: TreeNode | null }) { - if (!node) return null; - return ( -
- -
- ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx deleted file mode 100644 index 6078180374..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import Typography, { TypographyProps } from '@mui/material/Typography'; -import Skeleton from '@mui/material/Skeleton'; -import Grid from '@mui/material/Grid'; - -const variants = ['h1', 'h3', 'body1', 'caption'] as readonly TypographyProps['variant'][]; - -export default function ListFallbackComponent() { - return ( -
-
-
- - - -
-
- - - {variants.map((variant) => ( - - - - ))} - - -
-
-
- ); -} 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 deleted file mode 100644 index 17ce15c98e..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/Overlay.tsx +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 0e5873ae44..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { BlockListProps, useBlockList, withTextBlockManager } from './BlockList.hooks'; -import { withErrorBoundary } from 'react-error-boundary'; -import ListFallbackComponent from './ListFallbackComponent'; -import BlockListTitle from './BlockListTitle'; -import BlockComponent from '../BlockComponent'; -import Overlay from './Overlay'; - -function BlockList(props: BlockListProps) { - const { root, rowVirtualizer, parentRef, blockEditor } = useBlockList(props); - - const virtualItems = rowVirtualizer.getVirtualItems(); - return ( -
-
-
- {root && virtualItems.length ? ( -
- {virtualItems.map((virtualRow) => { - const id = root.children[virtualRow.index].id; - return ( -
- {virtualRow.index === 0 ? : null} - -
- ); - })} -
- ) : null} -
-
- {parentRef.current ? : null} -
- ); -} - -const ListWithErrorBoundary = withErrorBoundary(withTextBlockManager(BlockList), { - FallbackComponent: ListFallbackComponent, -}); - -export default React.memo(ListWithErrorBoundary); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockPortal/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockPortal/index.tsx deleted file mode 100644 index bdd969616d..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockPortal/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import ReactDOM from 'react-dom'; - -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 BlockPortal; 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 deleted file mode 100644 index b9cdc1d5e1..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/BlockSideTools.hooks.tsx +++ /dev/null @@ -1,64 +0,0 @@ -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/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx deleted file mode 100644 index eb34844d2c..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { BlockCommonProps } from '@/appflowy_app/interfaces'; - -export default function CodeBlock({ node }: BlockCommonProps) { - return
{node.data.text}
; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx deleted file mode 100644 index f0a1bd3323..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import TextBlock from '../TextBlock'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { BlockCommonProps } from '@/appflowy_app/interfaces'; - -const fontSize: Record = { - 1: 'mt-8 text-3xl', - 2: 'mt-6 text-2xl', - 3: 'mt-4 text-xl', -}; - -export default function HeadingBlock({ node, version }: BlockCommonProps) { - return ( -
- -
- ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatButton.tsx deleted file mode 100644 index 1409680f24..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatButton.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format'; -import IconButton from '@mui/material/IconButton'; -import Tooltip from '@mui/material/Tooltip'; - -import { command } from '$app/constants/toolbar'; -import FormatIcon from './FormatIcon'; -import { BaseEditor } from 'slate'; - -const FormatButton = ({ editor, format, icon }: { editor: BaseEditor; format: string; icon: string }) => { - return ( - - {command[format].title} - {command[format].key} -
- } - placement='top-start' - > - toggleFormat(editor, format)} - > - - - - ); -}; - -export default FormatButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatIcon.tsx deleted file mode 100644 index 371ec6585c..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material'; -import { iconSize } from '$app/constants/toolbar'; - -export default function FormatIcon({ icon }: { icon: string }) { - switch (icon) { - case 'bold': - return ; - case 'underlined': - return ; - case 'italic': - return ; - case 'code': - return ; - case 'strikethrough': - return ; - default: - return null; - } -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.hooks.ts deleted file mode 100644 index 8319291046..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.hooks.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { useFocused, useSlate } from 'slate-react'; -import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar'; -import { TreeNode } from '$app/block_editor/view/tree_node'; - -export function useHoveringToolbar({node}: { - node: TreeNode -}) { - const editor = useSlate(); - const inFocus = useFocused(); - const ref = useRef(null); - - useEffect(() => { - const el = ref.current; - if (!el) return; - const nodeRect = document.querySelector(`[data-block-id=${node.id}]`)?.getBoundingClientRect(); - - if (!nodeRect) return; - const position = calcToolbarPosition(editor, el, nodeRect); - - if (!position) { - el.style.opacity = '0'; - el.style.zIndex = '-1'; - } else { - el.style.opacity = '1'; - el.style.zIndex = '1'; - el.style.top = position.top; - el.style.left = position.left; - } - }); - return { - ref, - inFocus, - editor - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx deleted file mode 100644 index 0d02fbf665..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import FormatButton from './FormatButton'; -import Portal from '../BlockPortal'; -import { TreeNode } from '$app/block_editor/view/tree_node'; -import { useHoveringToolbar } from './index.hooks'; - -const HoveringToolbar = ({ blockId, node }: { blockId: string; node: TreeNode }) => { - const { inFocus, ref, editor } = useHoveringToolbar({ node }); - if (!inFocus) return null; - - return ( - -
{ - // prevent toolbar from taking focus away from editor - e.preventDefault(); - }} - > - {['bold', 'italic', 'underlined', 'strikethrough', 'code'].map((format) => ( - - ))} -
-
- ); -}; - -export default HoveringToolbar; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx deleted file mode 100644 index ce0a1254d3..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import React, { useMemo } from 'react'; -import ColumnBlock from '../ColumnBlock'; - -export default function ColumnListBlock({ node }: { node: TreeNode }) { - const resizerWidth = useMemo(() => { - return 46 * (node.children?.length || 0); - }, [node.children?.length]); - return ( - <> -
- {node.children?.map((item, index) => ( - - ))} -
- - ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx deleted file mode 100644 index 6bc63d41ef..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import BlockComponent from '../BlockComponent'; -import { BlockType } from '@/appflowy_app/interfaces'; -import { Block } from '@/appflowy_app/block_editor/core/block'; - -export default function NumberedListBlock({ title, node }: { title: JSX.Element; node: TreeNode }) { - let prev = node.block.prev; - let index = 1; - while (prev && prev.type === BlockType.ListBlock && (prev as Block).data.type === 'numbered') { - index++; - prev = prev.prev; - } - return ( -
-
-
{`${index} .`}
- {title} -
- -
- {node.children?.map((item) => ( -
- -
- ))} -
-
- ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx deleted file mode 100644 index a79e036dbe..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { BlockCommonProps } from '@/appflowy_app/interfaces'; - -export default function PageBlock({ node }: BlockCommonProps) { - return
{node.data.title}
; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx deleted file mode 100644 index aa5dcd1efa..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { BaseText } from 'slate'; -import { RenderLeafProps } from 'slate-react'; - -const Leaf = ({ - attributes, - children, - leaf, -}: RenderLeafProps & { - leaf: BaseText & { - bold?: boolean; - code?: boolean; - italic?: boolean; - underlined?: boolean; - strikethrough?: boolean; - }; -}) => { - let newChildren = children; - if (leaf.bold) { - newChildren = {children}; - } - - if (leaf.code) { - newChildren = {newChildren}; - } - - if (leaf.italic) { - newChildren = {newChildren}; - } - - if (leaf.underlined) { - newChildren = {newChildren}; - } - - return ( - - {newChildren} - - ); -}; - -export default Leaf; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts deleted file mode 100644 index 0275be16f5..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { TreeNode } from "@/appflowy_app/block_editor/view/tree_node"; -import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey"; -import { useCallback, useContext, useEffect, useLayoutEffect, useState } from "react"; -import { Transforms, createEditor, Descendant, Range } from 'slate'; -import { ReactEditor, withReact } from 'slate-react'; -import { TextBlockContext } from '$app/utils/slate/context'; - -export function useTextBlock({ - node, -}: { - node: TreeNode; -}) { - const [editor] = useState(() => withReact(createEditor())); - - const { textBlockManager } = useContext(TextBlockContext); - - const value = [ - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - type: 'paragraph', - children: node.data.content, - }, - ]; - - - const onChange = useCallback( - (e: Descendant[]) => { - if (!editor.operations || editor.operations.length === 0) return; - if (editor.operations[0].type !== 'set_selection') { - console.log('====text block ==== ', editor.operations) - const children = 'children' in e[0] ? e[0].children : []; - textBlockManager?.update(node, ['data', 'content'], children); - } else { - const newProperties = editor.operations[0].newProperties; - textBlockManager?.setSelection(node, editor.selection); - } - }, - [node.id, editor], - ); - - - const onKeyDownCapture = (event: React.KeyboardEvent) => { - switch (event.key) { - case 'Enter': { - event.stopPropagation(); - event.preventDefault(); - textBlockManager?.splitNode(node, editor); - - return; - } - case 'Backspace': { - if (!editor.selection) return; - const { anchor } = editor.selection; - const isCollapase = Range.isCollapsed(editor.selection); - if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') { - event.stopPropagation(); - event.preventDefault(); - textBlockManager?.deleteNode(node); - } - } - } - - triggerHotkey(event, editor); - } - - - - const { focusId, selection } = textBlockManager!.selectionManager.getFocusSelection(); - - editor.children = value; - Transforms.collapse(editor); - - useEffect(() => { - textBlockManager?.register(node.id, editor); - - return () => { - textBlockManager?.unregister(node.id); - } - }, [ editor ]) - - - useLayoutEffect(() => { - - let timer: NodeJS.Timeout; - if (focusId === node.id && selection) { - ReactEditor.focus(editor); - Transforms.select(editor, selection); - // Use setTimeout to delay setting the selection - // until Slate has fully loaded and rendered all components and contents, - // to ensure that the operation succeeds. - timer = setTimeout(() => { - Transforms.select(editor, selection); - }, 100); - } - - return () => timer && clearTimeout(timer) - }, [editor]); - - const onDOMBeforeInput = useCallback((e: InputEvent) => { - // COMPAT: in Apple, `compositionend` is dispatched after the - // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese. - // Here, prevent the beforeInput event and wait for the compositionend event to take effect - if (e.inputType === 'insertFromComposition') { - e.preventDefault(); - } - - }, []); - - - return { - editor, - value, - onChange, - onKeyDownCapture, - onDOMBeforeInput, - } -} \ No newline at end of file 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 deleted file mode 100644 index 11b43bf2b9..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import BlockComponent from '../BlockComponent'; -import { Slate, Editable } from 'slate-react'; -import Leaf from './Leaf'; -import HoveringToolbar from '@/appflowy_app/components/block/HoveringToolbar'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { useTextBlock } from './index.hooks'; -import { BlockCommonProps, TextBlockToolbarProps } from '@/appflowy_app/interfaces'; -import { toolbarDefaultProps } from '@/appflowy_app/constants/toolbar'; - -export default function TextBlock({ - node, - needRenderChildren = true, - toolbarProps, - ...props -}: { - needRenderChildren?: boolean; - toolbarProps?: TextBlockToolbarProps; -} & BlockCommonProps & - React.HTMLAttributes) { - const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock({ node }); - const { showGroups } = toolbarProps || toolbarDefaultProps; - - return ( -
- - {showGroups.length > 0 && } - } - placeholder='Enter some text...' - /> - - {needRenderChildren && node.children.length > 0 ? ( -
- {node.children.map((item) => ( - - ))} -
- ) : null} -
- ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx index bdd969616d..49ede75648 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx @@ -1,7 +1,7 @@ import ReactDOM from 'react-dom'; const BlockPortal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => { - const root = document.querySelectorAll(`[data-block-id=${blockId}] > .block-overlay`)[0]; + const root = document.querySelectorAll(`[data-block-id="${blockId}"] > .block-overlay`)[0]; return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx similarity index 84% rename from frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx index 480841d5d5..0404fe42b8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx @@ -1,17 +1,16 @@ -import { BlockEditor } from '@/appflowy_app/block_editor'; import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; +import { useAppDispatch } from '$app/stores/store'; +import { documentActions } from '@/appflowy_app/stores/reducers/document/slice'; export function useBlockSelection({ container, - blockEditor, onDragging, }: { container: HTMLDivElement; - blockEditor: BlockEditor; onDragging?: (_isDragging: boolean) => void; }) { - const blockPositionManager = blockEditor.renderTree.blockPositionManager; const ref = useRef(null); + const disaptch = useAppDispatch(); const [isDragging, setDragging] = useState(false); const pointRef = useRef([]); @@ -75,7 +74,7 @@ export function useBlockSelection({ const calcIntersectBlocks = useCallback( (clientX: number, clientY: number) => { - if (!isDragging || !blockPositionManager) return; + if (!isDragging) return; const [startX, startY] = pointRef.current; const endX = clientX + container.scrollLeft; const endY = clientY + container.scrollTop; @@ -86,21 +85,21 @@ export function useBlockSelection({ endX, endY, }); - const selectedBlocks = blockPositionManager.getIntersectBlocks( - Math.min(startX, endX), - Math.min(startY, endY), - Math.max(startX, endX), - Math.max(startY, endY) + disaptch( + documentActions.changeSelectionByIntersectRect({ + startX: Math.min(startX, endX), + startY: Math.min(startY, endY), + endX: Math.max(startX, endX), + endY: Math.max(startY, endY), + }) ); - const ids = selectedBlocks.map((item) => item.id); - blockEditor.renderTree.updateSelections(ids); }, [isDragging] ); const handleDraging = useCallback( (e: MouseEvent) => { - if (!isDragging || !blockPositionManager) return; + if (!isDragging) return; e.preventDefault(); e.stopPropagation(); calcIntersectBlocks(e.clientX, e.clientY); @@ -120,7 +119,7 @@ export function useBlockSelection({ const handleDragEnd = useCallback( (e: MouseEvent) => { if (isPointInBlock(e.target as HTMLElement) && !isDragging) { - blockEditor.renderTree.updateSelections([]); + disaptch(documentActions.updateSelections([])); return; } if (!isDragging) return; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx similarity index 84% rename from frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx index 67aa371748..0a3ac62a84 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/index.tsx @@ -1,19 +1,15 @@ import { useBlockSelection } from './BlockSelection.hooks'; -import { BlockEditor } from '$app/block_editor'; import React from 'react'; function BlockSelection({ container, - blockEditor, onDragging, }: { container: HTMLDivElement; - blockEditor: BlockEditor; onDragging?: (_isDragging: boolean) => void; }) { const { isDragging, style, ref } = useBlockSelection({ container, - blockEditor, onDragging, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx new file mode 100644 index 0000000000..9773339a45 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx @@ -0,0 +1,126 @@ +import { BlockType } from '@/appflowy_app/interfaces/document'; +import { useAppSelector } from '@/appflowy_app/stores/store'; +import { debounce } from '@/appflowy_app/utils/tool'; +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { YDocControllerContext } from '../../../stores/effects/document/document_controller'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import { v4 } from 'uuid'; + +export function useBlockSideTools({ container }: { container: HTMLDivElement }) { + const [nodeId, setHoverNodeId] = useState(''); + const ref = useRef(null); + const nodes = useAppSelector((state) => state.document.nodes); + const { insertAfter } = useController(); + + const handleMouseMove = useCallback((e: MouseEvent) => { + const { clientX, clientY } = e; + const x = clientX; + const y = clientY; + const id = getNodeIdByPoint(x, y); + if (!id) { + setHoverNodeId(''); + } else { + if ([BlockType.ColumnBlock].includes(nodes[id].type)) { + setHoverNodeId(''); + return; + } + setHoverNodeId(id); + } + }, []); + + const debounceMove = useMemo(() => debounce(handleMouseMove, 30), [handleMouseMove]); + + useEffect(() => { + const el = ref.current; + if (!el || !nodeId) return; + + const node = nodes[nodeId]; + if (!node) { + el.style.opacity = '0'; + el.style.zIndex = '-1'; + } else { + el.style.opacity = '1'; + el.style.zIndex = '1'; + el.style.top = '1px'; + if (node?.type === BlockType.HeadingBlock) { + if (node.data.style?.level === 1) { + el.style.top = '8px'; + } else if (node.data.style?.level === 2) { + el.style.top = '6px'; + } else { + el.style.top = '5px'; + } + } + } + }, [nodeId, nodes]); + + const handleAddClick = useCallback(() => { + if (!nodeId) return; + insertAfter(nodes[nodeId]); + }, [nodeId, nodes]); + + useEffect(() => { + container.addEventListener('mousemove', debounceMove); + return () => { + container.removeEventListener('mousemove', debounceMove); + }; + }, [debounceMove]); + + return { + nodeId, + ref, + handleAddClick, + }; +} + +function useController() { + const controller = useContext(YDocControllerContext); + + const insertAfter = useCallback((node: Node) => { + const parentId = node.parent; + if (!parentId || !controller) return; + + controller.transact([ + () => { + const newNode = { + id: v4(), + delta: [], + type: BlockType.TextBlock, + }; + controller.insert(newNode, parentId, node.id); + }, + ]); + }, []); + + return { + insertAfter, + }; +} + +function getNodeIdByPoint(x: number, y: number) { + const viewportNodes = document.querySelectorAll('[data-block-id]'); + let node: { + el: Element; + rect: DOMRect; + } | null = null; + viewportNodes.forEach((el) => { + const rect = el.getBoundingClientRect(); + + if (rect.x + rect.width - 1 >= x && rect.y + rect.height - 1 >= y && rect.y <= y) { + if (!node || rect.y > node.rect.y) { + node = { + el, + rect, + }; + } + } + }); + return node + ? ( + node as { + el: Element; + rect: DOMRect; + } + ).el.getAttribute('data-block-id') + : null; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx similarity index 78% rename from frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx index 624d2a98ce..cf2631f474 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSideTools/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/index.tsx @@ -1,6 +1,5 @@ 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'; @@ -8,12 +7,12 @@ 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); +export default function BlockSideTools(props: { container: HTMLDivElement }) { + const { nodeId, ref, handleAddClick } = useBlockSideTools(props); - if (!hoverBlock) return null; + if (!nodeId) return null; return ( - +
- + handleAddClick()} sx={sx}> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx new file mode 100644 index 0000000000..b4a152a824 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx @@ -0,0 +1,3 @@ +export default function CodeBlock({ id }: { id: string }) { + return
{id}
; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnBlock/index.tsx similarity index 59% rename from frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnBlock/index.tsx index 8a6298bb2b..cd12b16f06 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnBlock/index.tsx @@ -1,17 +1,7 @@ import React from 'react'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; +import NodeComponent from '../Node'; -import BlockComponent from '../BlockComponent'; - -export default function ColumnBlock({ - node, - resizerWidth, - index, -}: { - node: TreeNode; - resizerWidth: number; - index: number; -}) { +export default function ColumnBlock({ id, index, width }: { id: string; index: number; width: string }) { const renderResizer = () => { return (
@@ -35,15 +25,14 @@ export default function ColumnBlock({ renderResizer() )} - } + id={id} /> ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts index 6c5e80c721..dc67320b26 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts @@ -1,7 +1,8 @@ import { useSubscribeNode } from '../_shared/SubscribeNode.hooks'; export function useDocumentTitle(id: string) { - const { node } = useSubscribeNode(id); + const { node, delta } = useSubscribeNode(id); return { - node + node, + delta } } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx index 9923e5cf76..2a7815b536 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx @@ -3,12 +3,11 @@ import { useDocumentTitle } from './DocumentTitle.hooks'; import TextBlock from '../TextBlock'; export default function DocumentTitle({ id }: { id: string }) { - const { node } = useDocumentTitle(id); + const { node, delta } = useDocumentTitle(id); if (!node) return null; - return (
- +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx new file mode 100644 index 0000000000..186d98e51c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx @@ -0,0 +1,17 @@ +import TextBlock from '../TextBlock'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; + +const fontSize: Record = { + 1: 'mt-8 text-3xl', + 2: 'mt-6 text-2xl', + 3: 'mt-4 text-xl', +}; + +export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) { + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts index bddc3be4f7..ac512b536f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts @@ -11,7 +11,7 @@ export function useHoveringToolbar(id: string) { useEffect(() => { const el = ref.current; if (!el) return; - const nodeRect = document.querySelector(`[data-block-id=${id}]`)?.getBoundingClientRect(); + const nodeRect = document.querySelector(`[data-block-id="${id}"]`)?.getBoundingClientRect(); if (!nodeRect) return; const position = calcToolbarPosition(editor, el, nodeRect); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/BulletedListBlock.tsx similarity index 54% rename from frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/BulletedListBlock.tsx index 38f5b743ea..00349acf8a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/BulletedListBlock.tsx @@ -1,9 +1,16 @@ +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import { Circle } from '@mui/icons-material'; +import NodeComponent from '../Node'; -import BlockComponent from '../BlockComponent'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; - -export default function BulletedListBlock({ title, node }: { title: JSX.Element; node: TreeNode }) { +export default function BulletedListBlock({ + title, + node, + childIds, +}: { + title: JSX.Element; + node: Node; + childIds?: string[]; +}) { return (
@@ -14,10 +21,8 @@ export default function BulletedListBlock({ title, node }: { title: JSX.Element;
- {node.children?.map((item) => ( -
- -
+ {childIds?.map((item) => ( + ))}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx new file mode 100644 index 0000000000..82fd423e9d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/ColumnListBlock.tsx @@ -0,0 +1,23 @@ +import React, { useMemo } from 'react'; +import ColumnBlock from '../ColumnBlock'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; + +export default function ColumnListBlock({ node, childIds }: { node: Node; childIds?: string[] }) { + const resizerWidth = useMemo(() => { + return 46 * (node.children?.length || 0); + }, [node.children?.length]); + return ( + <> +
+ {childIds?.map((item, index) => ( + + ))} +
+ + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx new file mode 100644 index 0000000000..5c66f61133 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/NumberedListBlock.tsx @@ -0,0 +1,30 @@ +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import NodeComponent from '../Node'; + +export default function NumberedListBlock({ + title, + node, + childIds, +}: { + title: JSX.Element; + node: Node; + childIds?: string[]; +}) { + const index = 1; + return ( +
+
+
{`${index} .`}
+ {title} +
+ +
+ {childIds?.map((item) => ( + + ))} +
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx similarity index 51% rename from frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx index 87c31795ce..a33b36cbde 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx @@ -3,28 +3,28 @@ import TextBlock from '../TextBlock'; import NumberedListBlock from './NumberedListBlock'; import BulletedListBlock from './BulletedListBlock'; import ColumnListBlock from './ColumnListBlock'; -import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -import { BlockCommonProps } from '@/appflowy_app/interfaces'; +import { Node } from '@/appflowy_app/stores/reducers/document/slice'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; -export default function ListBlock({ node, version }: BlockCommonProps) { +export default function ListBlock({ node, delta }: { node: Node; delta: TextDelta[] }) { const title = useMemo(() => { - if (node.data.type === 'column') return <>; + if (node.data.style?.type === 'column') return <>; return (
- +
); - }, [node, version]); + }, [node, delta]); - if (node.data.type === 'numbered') { + if (node.data.style?.type === 'numbered') { return ; } - if (node.data.type === 'bulleted') { + if (node.data.style?.type === 'bulleted') { return ; } - if (node.data.type === 'column') { + if (node.data.style?.type === 'column') { return ; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts index 5517908214..1bb2e2b25d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts @@ -1,11 +1,36 @@ +import { useEffect, useRef } from 'react'; import { useSubscribeNode } from '../_shared/SubscribeNode.hooks'; +import { useAppDispatch } from '$app/stores/store'; +import { documentActions } from '$app/stores/reducers/document/slice'; export function useNode(id: string) { - const { node, childIds } = useSubscribeNode(id); + const { node, childIds, delta, isSelected } = useSubscribeNode(id); + const ref = useRef(null); + const dispatch = useAppDispatch(); + + useEffect(() => { + if (!ref.current) return; + const rect = ref.current.getBoundingClientRect(); + + const scrollContainer = document.querySelector('.doc-scroller-container') as HTMLDivElement; + dispatch(documentActions.updateNodePosition({ + id, + rect: { + x: rect.x, + y: rect.y + scrollContainer.scrollTop, + height: rect.height, + width: rect.width + } + })) + }, []) + return { + ref, node, childIds, + delta, + isSelected } } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx index b53530d8f1..bfe2e9649b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx @@ -4,14 +4,17 @@ import { withErrorBoundary } from 'react-error-boundary'; import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent'; import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import TextBlock from '../TextBlock'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; -function NodeComponent({ id }: { id: string }) { - const { node, childIds } = useNode(id); +function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes) { + const { node, childIds, delta, isSelected, ref } = useNode(id); - const renderBlock = useCallback((props: { node: Node; childIds?: string[] }) => { - switch (props.node.type) { + console.log('=====', id); + const renderBlock = useCallback((_props: { node: Node; childIds?: string[]; delta?: TextDelta[] }) => { + switch (_props.node.type) { case 'text': - return ; + if (!_props.delta) return null; + return ; default: break; } @@ -20,12 +23,14 @@ function NodeComponent({ id }: { id: string }) { if (!node) return null; return ( -
+
{renderBlock({ node, childIds, + delta, })}
+ {isSelected ?
: null}
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx new file mode 100644 index 0000000000..62d15de804 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx @@ -0,0 +1,13 @@ +import React, { useState } from 'react'; +import BlockSideTools from '../BlockSideTools'; +import BlockSelection from '../BlockSelection'; + +export default function Overlay({ container }: { container: HTMLDivElement }) { + const [isDragging, setDragging] = useState(false); + return ( + <> + {isDragging ? null : } + + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx index 143c1d7ac5..1191705f0b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx @@ -1,60 +1,20 @@ import { useEffect } from 'react'; -import { DocumentData, NestedBlock } from '$app/interfaces/document'; +import { DocumentData } from '$app/interfaces/document'; import { useAppDispatch } from '@/appflowy_app/stores/store'; -import { documentActions, Node } from '$app/stores/reducers/document/slice'; +import { documentActions } from '$app/stores/reducers/document/slice'; export function useParseTree(documentData: DocumentData) { const dispatch = useAppDispatch(); - const { blocks, ytexts, yarrays, rootId } = documentData; - const flattenNestedBlocks = ( - block: NestedBlock - ): (Node & { - children: string[]; - })[] => { - const node: Node & { - children: string[]; - } = { - id: block.id, - delta: ytexts[block.data.text], - data: block.data, - type: block.type, - parent: block.parent, - children: yarrays[block.children], - }; - - const nodes = [node]; - node.children.forEach((child) => { - const childBlock = blocks[child]; - nodes.push(...flattenNestedBlocks(childBlock)); - }); - return nodes; - }; - - const initializeNodeHierarchy = (parentId: string, children: string[]) => { - children.forEach((childId) => { - dispatch(documentActions.addChild({ parentId, childId })); - const child = blocks[childId]; - initializeNodeHierarchy(childId, yarrays[child.children]); - }); - }; + const { blocks, ytexts, yarrays } = documentData; useEffect(() => { - const root = documentData.blocks[rootId]; - - const initialNodes = flattenNestedBlocks(root); - - initialNodes.forEach((node) => { - const _node = { - id: node.id, - parent: node.parent, - data: node.data, - type: node.type, - delta: node.delta, - }; - dispatch(documentActions.addNode(_node)); - }); - - initializeNodeHierarchy(rootId, yarrays[root.children]); + dispatch( + documentActions.createTree({ + nodes: blocks, + delta: ytexts, + children: yarrays, + }) + ); return () => { dispatch(documentActions.clear()); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts index 8e0d31d6b3..f30afffad4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts @@ -46,8 +46,7 @@ export function useBindYjs(delta: TextDelta[], update: (_delta: Delta) => void) if (!yText) return; const textEventHandler = (event: Y.YTextEvent) => { - console.log(event.delta, event.target.toDelta()); - update(event.delta as Delta); + update(event.changes.delta as Delta); } yText.applyDelta(delta); yText.observe(textEventHandler); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts index 6d896c2182..cea09635ac 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts @@ -1,29 +1,65 @@ import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey"; -import { useCallback, useContext, useState } from "react"; +import { useCallback, useContext, useMemo, useRef, useState } from "react"; import { Descendant, Range } from "slate"; import { useBindYjs } from "./BindYjs.hooks"; import { YDocControllerContext } from '../../../stores/effects/document/document_controller'; import { Delta } from "@slate-yjs/core/dist/model/types"; import { TextDelta } from '../../../interfaces/document'; +import { debounce } from "@/appflowy_app/utils/tool"; function useController(textId: string) { const docController = useContext(YDocControllerContext); - + const update = useCallback( (delta: Delta) => { docController?.yTextApply(textId, delta) }, [textId], + ); + const transact = useCallback( + (actions: (() => void)[]) => { + docController?.transact(actions) + }, + [textId], ) return { - update + update, + transact + } +} + +function useTransact(textId: string) { + const pendingActions = useRef<(() => void)[]>([]); + const { update, transact } = useController(textId); + + const sendTransact = useCallback( + () => { + const actions = pendingActions.current; + transact(actions); + }, + [transact], + ) + + const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]); + + const sendDelta = useCallback( + (delta: Delta) => { + const action = () => update(delta); + pendingActions.current.push(action); + debounceSendTransact() + }, + [update, debounceSendTransact], + ); + return { + sendDelta } } export function useTextBlock(text: string, delta: TextDelta[]) { - const { update } = useController(text); - const { editor } = useBindYjs(delta, update); + const { sendDelta } = useTransact(text); + + const { editor } = useBindYjs(delta, sendDelta); const [value, setValue] = useState([]); const onChange = useCallback( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx index e721337145..a64bd56990 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx @@ -4,21 +4,25 @@ import { useTextBlock } from './TextBlock.hooks'; import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import NodeComponent from '../Node'; import HoveringToolbar from '../HoveringToolbar'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; +import React from 'react'; -export default function TextBlock({ +function TextBlock({ node, childIds, placeholder, + delta, ...props }: { node: Node; + delta: TextDelta[]; childIds?: string[]; placeholder?: string; } & React.HTMLAttributes) { - const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, node.delta); + const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, delta); return ( -
+
); } + +export default React.memo(TextBlock); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx index 7d84f19450..5b3253b299 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useVirtualizerList } from './VirtualizerList.hooks'; import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import DocumentTitle from '../DocumentTitle'; +import Overlay from '../Overlay'; export default function VirtualizerList({ childIds, @@ -17,36 +18,42 @@ export default function VirtualizerList({ const virtualItems = rowVirtualizer.getVirtualItems(); return ( -
+ <>
- {node && childIds && virtualItems.length ? ( -
- {virtualItems.map((virtualRow) => { - const id = childIds[virtualRow.index]; - return ( -
- {virtualRow.index === 0 ? : null} - {renderNode(id)} -
- ); - })} -
- ) : null} +
+ {node && childIds && virtualItems.length ? ( +
+ {virtualItems.map((virtualRow) => { + const id = childIds[virtualRow.index]; + return ( +
+ {virtualRow.index === 0 ? : null} + {renderNode(id)} +
+ ); + })} +
+ ) : null} +
-
+ {parentRef.current ? : null} + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts index cf0530f1ac..1b3b4b71c8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts @@ -1,16 +1,32 @@ import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import { useAppSelector } from '@/appflowy_app/stores/store'; import { useMemo } from 'react'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; export function useSubscribeNode(id: string) { - const node = useAppSelector(state => state.document.nodes[id]); - const childIds = useAppSelector(state => state.document.children[id]); + const node = useAppSelector(state => state.document.nodes[id]); + const childIds = useAppSelector(state => { + const childrenId = state.document.nodes[id]?.children; + if (!childrenId) return; + return state.document.children[childrenId]; + }); + const delta = useAppSelector(state => { + const deltaId = state.document.nodes[id]?.data?.text; + if (!deltaId) return; + return state.document.delta[deltaId]; + }); + const isSelected = useAppSelector(state => { + return state.document.selections?.includes(id) || false; + }); - const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.type]); + const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]); const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]); - + const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]); + return { node: memoizedNode, - childIds: memoizedChildIds - } + childIds: memoizedChildIds, + delta: memoizedDelta, + isSelected + }; } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts index 67bd8d01d0..6b17cfbbd9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts @@ -1,8 +1,9 @@ import * as Y from 'yjs'; import { IndexeddbPersistence } from 'y-indexeddb'; import { v4 } from 'uuid'; -import { DocumentData } from '@/appflowy_app/interfaces/document'; +import { DocumentData, NestedBlock } from '@/appflowy_app/interfaces/document'; import { createContext } from 'react'; +import { BlockType } from '@/appflowy_app/interfaces'; export type DeltaAttributes = { retain: number; @@ -21,6 +22,7 @@ export type Delta = Array< DeltaRetain | DeltaDelete | DeltaInsert | DeltaAttributes >; + export const YDocControllerContext = createContext(null); export class YDocController { @@ -30,6 +32,12 @@ export class YDocController { constructor(private id: string) { this._ydoc = new Y.Doc(); this.provider = new IndexeddbPersistence(`document-${this.id}`, this._ydoc); + this._ydoc.on('update', this.handleUpdate); + } + + handleUpdate = (update: Uint8Array, origin: any) => { + const isLocal = origin === null; + Y.logUpdate(update); } @@ -83,9 +91,7 @@ export class YDocController { open = async (): Promise => { await this.provider.whenSynced; const ydoc = this._ydoc; - ydoc.on('updateV2', (update) => { - console.log('======', update); - }) + const blocks = ydoc.getMap('blocks'); const obj: DocumentData = { rootId: ydoc.getArray('root').toArray()[0] || '', @@ -93,28 +99,96 @@ export class YDocController { ytexts: {}, yarrays: {} }; + Object.keys(obj.blocks).forEach(key => { const value = obj.blocks[key]; if (value.children) { + const yarray = ydoc.getArray(value.children); Object.assign(obj.yarrays, { - [value.children]: ydoc.getArray(value.children).toArray() + [value.children]: yarray.toArray() }); } if (value.data.text) { + const ytext = ydoc.getText(value.data.text); Object.assign(obj.ytexts, { - [value.data.text]: ydoc.getText(value.data.text).toDelta() + [value.data.text]: ytext.toDelta() }) } }); + + blocks.observe(this.handleBlocksEvent); return obj; } + insert(node: { + id: string, + type: BlockType, + delta?: Delta + }, parentId: string, prevId: string) { + const blocks = this._ydoc.getMap('blocks'); + const parent = blocks.get(parentId); + if (!parent) return; + const insertNode = { + id: node.id, + type: node.type, + data: { + text: '' + }, + children: '', + parent: '' + } + // create ytext + if (node.delta) { + const ytextId = v4(); + const ytext = this._ydoc.getText(ytextId); + ytext.applyDelta(node.delta); + insertNode.data.text = ytextId; + } + // create children + const yArrayId = v4(); + this._ydoc.getArray(yArrayId); + insertNode.children = yArrayId; + // insert in parent's children + const children = this._ydoc.getArray(parent.children); + const index = children.toArray().indexOf(prevId) + 1; + children.insert(index, [node.id]); + insertNode.parent = parentId; + // set in blocks + this._ydoc.getMap('blocks').set(node.id, insertNode); + } + + transact(actions: (() => void)[]) { + const ydoc = this._ydoc; + console.log('====transact') + ydoc.transact(() => { + actions.forEach(action => { + action(); + }); + }); + } yTextApply = (yTextId: string, delta: Delta) => { - console.log("====", yTextId, delta); const ydoc = this._ydoc; const ytext = ydoc.getText(yTextId); ytext.applyDelta(delta); + console.log("====", yTextId, delta); + } + + close = () => { + const blocks = this._ydoc.getMap('blocks'); + blocks.unobserve(this.handleBlocksEvent); + } + + private handleBlocksEvent = (mapEvent: Y.YMapEvent) => { + console.log(mapEvent.changes); + } + + private handleTextEvent = (textEvent: Y.YTextEvent) => { + console.log(textEvent.changes); + } + + private handleArrayEvent = (arrayEvent: Y.YArrayEvent) => { + console.log(arrayEvent.changes); } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/region_grid.ts similarity index 79% rename from frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/region_grid.ts index 5f06f253ad..e7c7fd38ea 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/region_grid.ts @@ -14,6 +14,7 @@ interface BlockRegion { export class RegionGrid { private regions: BlockRegion[][]; private regionSize: number; + private blocks = new Map(); constructor(regionSize: number) { this.regionSize = regionSize; @@ -36,9 +37,22 @@ export class RegionGrid { } this.regions[regionY][regionX] = region; } - + this.blocks.set(blockPosition.id, blockPosition); region.blocks.push(blockPosition); } + + updateBlock(blockId: string, position: BlockPosition) { + const prevPosition = this.blocks.get(blockId); + if (prevPosition && prevPosition.x === position.x && + prevPosition.y === position.y && + prevPosition.height === position.height && + prevPosition.width === position.width) { + return; + } + this.blocks.set(blockId, position); + this.removeBlock(blockId); + this.addBlock(position); + } removeBlock(blockId: string) { for (const rows of this.regions) { @@ -51,6 +65,7 @@ export class RegionGrid { } } } + this.blocks.delete(blockId); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts index eb98209a07..4ef40d3781 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts @@ -1,24 +1,32 @@ import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document"; import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import { RegionGrid } from "./region_grid"; export interface Node { id: string; - parent: string | null; type: BlockType; - selected?: boolean; - delta: TextDelta[]; data: { text?: string; + style?: Record }; + parent: string | null; + children: string; } -export type NodeState = { +export interface NodeState { nodes: Record; children: Record; -}; + delta: Record; + selections: string[]; +} + +const regionGrid = new RegionGrid(50); + const initialState: NodeState = { nodes: {}, children: {}, + delta: {}, + selections: [], }; export const documentSlice = createSlice({ @@ -28,34 +36,68 @@ export const documentSlice = createSlice({ clear: (state, action: PayloadAction) => { return initialState; }, - addNode: (state, action: PayloadAction) => { - state.nodes[action.payload.id] = action.payload; - }, - addChild: (state, action: PayloadAction<{ parentId: string, childId: string }>) => { - const children = state.children[action.payload.parentId]; - if (children) { - children.push(action.payload.childId); - } else { - state.children[action.payload.parentId] = [action.payload.childId] - } + + createTree: (state, action: PayloadAction<{ + nodes: Record; + children: Record; + delta: Record; + }>) => { + const { nodes, children, delta } = action.payload; + state.nodes = nodes; + state.children = children; + state.delta = delta; }, - updateNode: (state, action: PayloadAction<{id: string; parent?: string; type?: BlockType; data?: any }>) => { + updateSelections: (state, action: PayloadAction) => { + state.selections = action.payload; + }, + + changeSelectionByIntersectRect: (state, action: PayloadAction<{ + startX: number; + startY: number; + endX: number; + endY: number + }>) => { + const { startX, startY, endX, endY } = action.payload; + const blocks = regionGrid.getIntersectBlocks(startX, startY, endX, endY); + state.selections = blocks.map(block => block.id); + }, + + updateNodePosition: (state, action: PayloadAction<{id: string; rect: { + x: number; + y: number; + width: number; + height: number; + }}>) => { + const { id, rect } = action.payload; + const position = { + id, + ...rect + }; + regionGrid.updateBlock(id, position); + }, + + updateNode: (state, action: PayloadAction<{id: string; type?: BlockType; data?: any }>) => { state.nodes[action.payload.id] = { ...state.nodes[action.payload.id], ...action.payload } }, - removeNode: (state, action: PayloadAction) => { - const parentId = state.nodes[action.payload].parent; - delete state.nodes[action.payload]; - if (parentId) { - const index = state.children[parentId].indexOf(action.payload); + const { children, data, parent } = state.nodes[action.payload]; + if (parent) { + const index = state.children[state.nodes[parent].children].indexOf(action.payload); if (index > -1) { - state.children[parentId].splice(index, 1); + state.children[state.nodes[parent].children].splice(index, 1); } } + if (children) { + delete state.children[children]; + } + if (data && data.text) { + delete state.delta[data.text]; + } + delete state.nodes[action.payload]; }, }, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts deleted file mode 100644 index 57d9b53822..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts +++ /dev/null @@ -1,34 +0,0 @@ - -import { createContext } from 'react'; -import { ulid } from "ulid"; -import { BlockEditor } from '../block_editor/index'; -import { BlockType } from '../interfaces'; - -export const BlockContext = createContext<{ - id?: string; - blockEditor?: BlockEditor; -}>({}); - - -export function generateBlockId() { - const blockId = ulid() - return `block-id-${blockId}`; -} - -const AVERAGE_BLOCK_HEIGHT = 30; -export function calculateViewportBlockMaxCount() { - const viewportHeight = window.innerHeight; - const viewportBlockCount = Math.ceil(viewportHeight / AVERAGE_BLOCK_HEIGHT); - - return viewportBlockCount; -} - - -export interface NestedNode { - id: string; - children: string; - parent: string | null; - type: BlockType; - data: any; -} - From 886766c88783461909a37da197d7fadccebb10a0 Mon Sep 17 00:00:00 2001 From: qinluhe Date: Mon, 27 Mar 2023 15:48:50 +0800 Subject: [PATCH 6/7] refactor: document controller --- .../BlockSelection/BlockSelection.hooks.tsx | 8 +- .../BlockSideTools/BlockSideTools.hooks.tsx | 4 +- .../components/document/Root/Tree.hooks.tsx | 8 +- .../components/document/Root/index.tsx | 4 +- .../document/TextBlock/BindYjs.hooks.ts | 61 ------ .../document/TextBlock/TextBlock.hooks.ts | 88 ++------ .../components/document/TextBlock/index.tsx | 2 +- ...st.hooks.tsx => VirtualizedList.hooks.tsx} | 6 +- .../document/VirtualizerList/index.tsx | 13 +- .../HoveringToolbar/FormatButton.tsx | 2 +- .../HoveringToolbar/FormatIcon.tsx | 0 .../HoveringToolbar/index.hooks.ts | 9 +- .../{ => _shared}/HoveringToolbar/index.tsx | 2 +- .../document/_shared/SubscribeNode.hooks.ts | 16 +- .../document/_shared/TextInput.hooks.ts | 116 ++++++++++ .../NavigationPanel/FolderItem.hooks.ts | 5 - .../src/appflowy_app/constants/toolbar.ts | 14 -- .../src/appflowy_app/interfaces/document.ts | 8 +- .../src/appflowy_app/interfaces/index.ts | 113 +--------- .../effects/document/document_controller.ts | 200 +++--------------- .../stores/reducers/document/slice.ts | 50 +++-- .../src/appflowy_app/utils/block_selection.ts | 36 ---- .../src/appflowy_app/utils/slate/context.ts | 6 - .../appflowy_app/views/DocumentPage.hooks.ts | 8 +- .../src/appflowy_app/views/DocumentPage.tsx | 6 +- 25 files changed, 253 insertions(+), 532 deletions(-) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts rename frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/{VirtualizerList.hooks.tsx => VirtualizedList.hooks.tsx} (73%) rename frontend/appflowy_tauri/src/appflowy_app/components/document/{ => _shared}/HoveringToolbar/FormatButton.tsx (92%) rename frontend/appflowy_tauri/src/appflowy_app/components/document/{ => _shared}/HoveringToolbar/FormatIcon.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/document/{ => _shared}/HoveringToolbar/index.hooks.ts (89%) rename frontend/appflowy_tauri/src/appflowy_app/components/document/{ => _shared}/HoveringToolbar/index.tsx (95%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx index 0404fe42b8..6015440964 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx @@ -72,7 +72,7 @@ export function useBlockSelection({ }); }, []); - const calcIntersectBlocks = useCallback( + const updateSelctionsByPoint = useCallback( (clientX: number, clientY: number) => { if (!isDragging) return; const [startX, startY] = pointRef.current; @@ -86,7 +86,7 @@ export function useBlockSelection({ endY, }); disaptch( - documentActions.changeSelectionByIntersectRect({ + documentActions.setSelectionByRect({ startX: Math.min(startX, endX), startY: Math.min(startY, endY), endX: Math.max(startX, endX), @@ -102,7 +102,7 @@ export function useBlockSelection({ if (!isDragging) return; e.preventDefault(); e.stopPropagation(); - calcIntersectBlocks(e.clientX, e.clientY); + updateSelctionsByPoint(e.clientX, e.clientY); const { top, bottom } = container.getBoundingClientRect(); if (e.clientY >= bottom) { @@ -124,7 +124,7 @@ export function useBlockSelection({ } if (!isDragging) return; e.preventDefault(); - calcIntersectBlocks(e.clientX, e.clientY); + updateSelctionsByPoint(e.clientX, e.clientY); setDragging(false); setRect(null); }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx index 9773339a45..c707e4c4e1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx @@ -2,7 +2,7 @@ import { BlockType } from '@/appflowy_app/interfaces/document'; import { useAppSelector } from '@/appflowy_app/stores/store'; import { debounce } from '@/appflowy_app/utils/tool'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { YDocControllerContext } from '../../../stores/effects/document/document_controller'; +import { DocumentControllerContext } from '$app/stores/effects/document/document_controller'; import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import { v4 } from 'uuid'; @@ -74,7 +74,7 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement }) } function useController() { - const controller = useContext(YDocControllerContext); + const controller = useContext(DocumentControllerContext); const insertAfter = useCallback((node: Node) => { const parentId = node.parent; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx index 1191705f0b..62e249d354 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx @@ -5,14 +5,14 @@ import { documentActions } from '$app/stores/reducers/document/slice'; export function useParseTree(documentData: DocumentData) { const dispatch = useAppDispatch(); - const { blocks, ytexts, yarrays } = documentData; + const { blocks, meta } = documentData; useEffect(() => { dispatch( - documentActions.createTree({ + documentActions.create({ nodes: blocks, - delta: ytexts, - children: yarrays, + delta: meta.text_map, + children: meta.children_map, }) ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx index 3e89c1b31e..556370e6d4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx @@ -4,7 +4,7 @@ import { useRoot } from './Root.hooks'; import Node from '../Node'; import { withErrorBoundary } from 'react-error-boundary'; import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent'; -import VirtualizerList from '../VirtualizerList'; +import VirtualizedList from '../VirtualizerList'; import { Skeleton } from '@mui/material'; function Root({ documentData }: { documentData: DocumentData }) { @@ -20,7 +20,7 @@ function Root({ documentData }: { documentData: DocumentData }) { return (
- +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts deleted file mode 100644 index f30afffad4..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts +++ /dev/null @@ -1,61 +0,0 @@ - - -import { useEffect, useMemo, useRef } from "react"; -import { createEditor } from "slate"; -import { withReact } from "slate-react"; - -import * as Y from 'yjs'; -import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core'; -import { Delta } from '@slate-yjs/core/dist/model/types'; -import { TextDelta } from '@/appflowy_app/interfaces/document'; - -const initialValue = [{ - type: 'paragraph', - children: [{ text: '' }], -}]; - -export function useBindYjs(delta: TextDelta[], update: (_delta: Delta) => void) { - const yTextRef = useRef(); - // Create a yjs document and get the shared type - const sharedType = useMemo(() => { - const ydoc = new Y.Doc() - const _sharedType = ydoc.get('content', Y.XmlText) as Y.XmlText; - - const insertDelta = slateNodesToInsertDelta(initialValue); - // Load the initial value into the yjs document - _sharedType.applyDelta(insertDelta); - - const yText = insertDelta[0].insert as Y.XmlText; - yTextRef.current = yText; - - return _sharedType; - }, []); - - const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []); - - useEffect(() => { - YjsEditor.connect(editor); - return () => { - yTextRef.current = undefined; - YjsEditor.disconnect(editor); - } - }, [editor]); - - useEffect(() => { - const yText = yTextRef.current; - if (!yText) return; - - const textEventHandler = (event: Y.YTextEvent) => { - update(event.changes.delta as Delta); - } - yText.applyDelta(delta); - yText.observe(textEventHandler); - - return () => { - yText.unobserve(textEventHandler); - } - }, [delta]) - - - return { editor } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts index cea09635ac..a513bb2521 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts @@ -1,72 +1,18 @@ -import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey"; -import { useCallback, useContext, useMemo, useRef, useState } from "react"; -import { Descendant, Range } from "slate"; -import { useBindYjs } from "./BindYjs.hooks"; -import { YDocControllerContext } from '../../../stores/effects/document/document_controller'; -import { Delta } from "@slate-yjs/core/dist/model/types"; -import { TextDelta } from '../../../interfaces/document'; -import { debounce } from "@/appflowy_app/utils/tool"; - -function useController(textId: string) { - const docController = useContext(YDocControllerContext); - - const update = useCallback( - (delta: Delta) => { - docController?.yTextApply(textId, delta) - }, - [textId], - ); - const transact = useCallback( - (actions: (() => void)[]) => { - docController?.transact(actions) - }, - [textId], - ) - - return { - update, - transact - } -} - -function useTransact(textId: string) { - const pendingActions = useRef<(() => void)[]>([]); - const { update, transact } = useController(textId); - - const sendTransact = useCallback( - () => { - const actions = pendingActions.current; - transact(actions); - }, - [transact], - ) - - const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]); - - const sendDelta = useCallback( - (delta: Delta) => { - const action = () => update(delta); - pendingActions.current.push(action); - debounceSendTransact() - }, - [update, debounceSendTransact], - ); - return { - sendDelta - } -} +import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey'; +import { useCallback, useState } from 'react'; +import { Descendant, Range } from 'slate'; +import { TextDelta } from '$app/interfaces/document'; +import { useTextInput } from '../_shared/TextInput.hooks'; export function useTextBlock(text: string, delta: TextDelta[]) { - const { sendDelta } = useTransact(text); - - const { editor } = useBindYjs(delta, sendDelta); + const { editor } = useTextInput(text, delta); const [value, setValue] = useState([]); - + const onChange = useCallback( (e: Descendant[]) => { setValue(e); }, - [editor], + [editor] ); const onKeyDownCapture = (event: React.KeyboardEvent) => { @@ -74,14 +20,13 @@ export function useTextBlock(text: string, delta: TextDelta[]) { case 'Enter': { event.stopPropagation(); event.preventDefault(); - return; } case 'Backspace': { if (!editor.selection) return; const { anchor } = editor.selection; - const isCollapase = Range.isCollapsed(editor.selection); - if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') { + const isCollapsed = Range.isCollapsed(editor.selection); + if (isCollapsed && anchor.offset === 0 && anchor.path.toString() === '0,0') { event.stopPropagation(); event.preventDefault(); return; @@ -89,16 +34,15 @@ export function useTextBlock(text: string, delta: TextDelta[]) { } } triggerHotkey(event, editor); - } + }; const onDOMBeforeInput = useCallback((e: InputEvent) => { - // COMPAT: in Apple, `compositionend` is dispatched after the - // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese. - // Here, prevent the beforeInput event and wait for the compositionend event to take effect + // COMPAT: in Apple, `compositionend` is dispatched after the `beforeinput` for "insertFromComposition". + // It will cause repeated characters when inputting Chinese. + // Here, prevent the beforeInput event and wait for the compositionend event to take effect. if (e.inputType === 'insertFromComposition') { e.preventDefault(); } - }, []); return { @@ -106,6 +50,6 @@ export function useTextBlock(text: string, delta: TextDelta[]) { onKeyDownCapture, onDOMBeforeInput, editor, - value - } + value, + }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx index a64bd56990..2c9439af6c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx @@ -3,7 +3,7 @@ import Leaf from './Leaf'; import { useTextBlock } from './TextBlock.hooks'; import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import NodeComponent from '../Node'; -import HoveringToolbar from '../HoveringToolbar'; +import HoveringToolbar from '../_shared/HoveringToolbar'; import { TextDelta } from '@/appflowy_app/interfaces/document'; import React from 'react'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx similarity index 73% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx index c0e543bf5f..e9a8513f2e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx @@ -3,10 +3,10 @@ import { useRef } from 'react'; const defaultSize = 60; -export function useVirtualizerList(count: number) { +export function useVirtualizedList(count: number) { const parentRef = useRef(null); - const rowVirtualizer = useVirtualizer({ + const Virtualize = useVirtualizer({ count, getScrollElement: () => parentRef.current, estimateSize: () => { @@ -15,7 +15,7 @@ export function useVirtualizerList(count: number) { }); return { - rowVirtualizer, + Virtualize: Virtualize, parentRef, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx index 5b3253b299..83f2731213 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { useVirtualizerList } from './VirtualizerList.hooks'; +import { useVirtualizedList } from './VirtualizedList.hooks'; import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import DocumentTitle from '../DocumentTitle'; import Overlay from '../Overlay'; -export default function VirtualizerList({ +export default function VirtualizedList({ childIds, node, renderNode, @@ -13,9 +13,8 @@ export default function VirtualizerList({ node: Node; renderNode: (nodeId: string) => JSX.Element; }) { - const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length); - - const virtualItems = rowVirtualizer.getVirtualItems(); + const { Virtualize, parentRef } = useVirtualizedList(childIds.length); + const virtualItems = Virtualize.getVirtualItems(); return ( <> @@ -26,7 +25,7 @@ export default function VirtualizerList({
@@ -43,7 +42,7 @@ export default function VirtualizerList({ {virtualItems.map((virtualRow) => { const id = childIds[virtualRow.index]; return ( -
+
{virtualRow.index === 0 ? : null} {renderNode(id)}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx similarity index 92% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx index 1409680f24..903603480e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx @@ -1,4 +1,4 @@ -import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format'; +import { toggleFormat, isFormatActive } from '$app/utils/slate/format'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatIcon.tsx similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatIcon.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts similarity index 89% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts index ac512b536f..c5099732f8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts @@ -1,7 +1,6 @@ import { useEffect, useRef } from 'react'; import { useFocused, useSlate } from 'slate-react'; -import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar'; - +import { calcToolbarPosition } from '$app/utils/slate/toolbar'; export function useHoveringToolbar(id: string) { const editor = useSlate(); @@ -29,6 +28,6 @@ export function useHoveringToolbar(id: string) { return { ref, inFocus, - editor - } -} \ No newline at end of file + editor, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx similarity index 95% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx index a35588033c..d4a671ec83 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx @@ -1,5 +1,5 @@ import FormatButton from './FormatButton'; -import Portal from '../BlockPortal'; +import Portal from '../../BlockPortal'; import { useHoveringToolbar } from './index.hooks'; const HoveringToolbar = ({ id }: { id: string }) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts index 1b3b4b71c8..2adfb073f8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts @@ -3,22 +3,36 @@ import { useAppSelector } from '@/appflowy_app/stores/store'; import { useMemo } from 'react'; import { TextDelta } from '@/appflowy_app/interfaces/document'; +/** + * Subscribe to a node and its children + * It will be change when the node or its children is changed + * And it will not be change when other node is changed + * @param id + */ export function useSubscribeNode(id: string) { const node = useAppSelector(state => state.document.nodes[id]); + const childIds = useAppSelector(state => { const childrenId = state.document.nodes[id]?.children; if (!childrenId) return; return state.document.children[childrenId]; }); + const delta = useAppSelector(state => { - const deltaId = state.document.nodes[id]?.data?.text; + const externalType = state.document.nodes[id]?.externalType; + if (externalType !== 'text') return; + const deltaId = state.document.nodes[id]?.externalId; if (!deltaId) return; return state.document.delta[deltaId]; }); + const isSelected = useAppSelector(state => { return state.document.selections?.includes(id) || false; }); + // Memoize the node and its children + // So that the component will not be re-rendered when other node is changed + // It very important for performance const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]); const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]); const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts new file mode 100644 index 0000000000..579bdccec4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts @@ -0,0 +1,116 @@ +import { useCallback, useContext, useMemo, useRef, useEffect } from 'react'; +import { DocumentControllerContext } from '$app/stores/effects/document/document_controller'; +import { TextDelta } from '$app/interfaces/document'; +import { debounce } from '@/appflowy_app/utils/tool'; +import { createEditor } from 'slate'; +import { withReact } from 'slate-react'; + +import * as Y from 'yjs'; +import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core'; + +export function useTextInput(text: string, delta: TextDelta[]) { + const { sendDelta } = useTransact(text); + const { editor } = useBindYjs(delta, sendDelta); + + return { + editor, + }; +} + +function useController(textId: string) { + const docController = useContext(DocumentControllerContext); + + const update = useCallback( + (delta: TextDelta[]) => { + docController?.yTextApply(textId, delta); + }, + [textId] + ); + const transact = useCallback( + (actions: (() => void)[]) => { + docController?.transact(actions); + }, + [textId] + ); + + return { + update, + transact, + }; +} + +function useTransact(textId: string) { + const pendingActions = useRef<(() => void)[]>([]); + const { update, transact } = useController(textId); + + const sendTransact = useCallback(() => { + const actions = pendingActions.current; + transact(actions); + }, [transact]); + + const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]); + + const sendDelta = useCallback( + (delta: TextDelta[]) => { + const action = () => update(delta); + pendingActions.current.push(action); + debounceSendTransact(); + }, + [update, debounceSendTransact] + ); + return { + sendDelta, + }; +} + +const initialValue = [ + { + type: 'paragraph', + children: [{ text: '' }], + }, +]; + +export function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) { + const yTextRef = useRef(); + // Create a yjs document and get the shared type + const sharedType = useMemo(() => { + const doc = new Y.Doc(); + const _sharedType = doc.get('content', Y.XmlText) as Y.XmlText; + + const insertDelta = slateNodesToInsertDelta(initialValue); + // Load the initial value into the yjs document + _sharedType.applyDelta(insertDelta); + + const yText = insertDelta[0].insert as Y.XmlText; + yTextRef.current = yText; + + return _sharedType; + }, []); + + const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []); + + useEffect(() => { + YjsEditor.connect(editor); + return () => { + yTextRef.current = undefined; + YjsEditor.disconnect(editor); + }; + }, [editor]); + + useEffect(() => { + const yText = yTextRef.current; + if (!yText) return; + + const textEventHandler = (event: Y.YTextEvent) => { + update(event.changes.delta as TextDelta[]); + }; + yText.applyDelta(delta); + yText.observe(textEventHandler); + + return () => { + yText.unobserve(textEventHandler); + }; + }, [delta]); + + return { editor }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts index b03ecd865d..3c38ab70cd 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts @@ -9,7 +9,6 @@ import { useError } from '../../error/Error.hooks'; import { AppObserver } from '../../../stores/effects/folder/app/app_observer'; import { useNavigate } from 'react-router-dom'; import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants'; -import { YDocController } from '$app/stores/effects/document/document_controller'; export const useFolderEvents = (folder: IFolder, pages: IPage[]) => { const appDispatch = useAppDispatch(); @@ -133,10 +132,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => { layoutType: ViewLayoutTypePB.Document, }); - // temp: let me try it by yjs - const ydocController = new YDocController(newView.id); - await ydocController.createDocument(); - appDispatch( pagesActions.addPage({ folderId: folder.id, diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts index a0efb98d60..61c9a88e06 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts @@ -1,4 +1,3 @@ -import { TextBlockToolbarGroup } from "../interfaces"; export const iconSize = { width: 18, height: 18 }; @@ -24,16 +23,3 @@ export const command: Record = { key: '⌘ + Shift + S or ⌘ + Shift + X', }, }; - -export const toolbarDefaultProps = { - showGroups: [ - TextBlockToolbarGroup.ASK_AI, - TextBlockToolbarGroup.BLOCK_SELECT, - TextBlockToolbarGroup.ADD_LINK, - TextBlockToolbarGroup.COMMENT, - TextBlockToolbarGroup.TEXT_FORMAT, - TextBlockToolbarGroup.TEXT_COLOR, - TextBlockToolbarGroup.MENTION, - TextBlockToolbarGroup.MORE, - ], -}; \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts index 0cfefe0d75..90d91c6d94 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts @@ -16,6 +16,8 @@ export interface NestedBlock { id: string; type: BlockType; data: Record; + externalId: string; + externalType: 'text' | 'array' | 'map'; parent: string | null; children: string; } @@ -26,6 +28,8 @@ export interface TextDelta { export interface DocumentData { rootId: string; blocks: Record; - ytexts: Record; - yarrays: Record; + meta: { + text_map: Record; + children_map: Record; + } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts index e6d0760f64..db6c7f48b3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts @@ -1,112 +1 @@ -import { Descendant } from "slate"; - -// eslint-disable-next-line no-shadow -export enum BlockType { - PageBlock = 'page', - HeadingBlock = 'heading', - ListBlock = 'list', - TextBlock = 'text', - CodeBlock = 'code', - EmbedBlock = 'embed', - QuoteBlock = 'quote', - DividerBlock = 'divider', - MediaBlock = 'media', - TableBlock = 'table', - ColumnBlock = 'column' - -} - -export type BlockData = T extends BlockType.TextBlock ? TextBlockData : -T extends BlockType.PageBlock ? PageBlockData : -T extends BlockType.HeadingBlock ? HeadingBlockData : -T extends BlockType.ListBlock ? ListBlockData : -T extends BlockType.ColumnBlock ? ColumnBlockData : any; - - -export interface BlockInterface { - id: string; - type: BlockType; - data: BlockData; - next: string | null; - firstChild: string | null; -} - - -export interface TextBlockData { - content: Descendant[]; -} - -interface PageBlockData { - title: string; -} - -interface ListBlockData extends TextBlockData { - type: 'numbered' | 'bulleted' | 'column'; -} - -interface HeadingBlockData extends TextBlockData { - level: number; -} - -interface ColumnBlockData { - ratio: string; -} - -// eslint-disable-next-line no-shadow -export enum TextBlockToolbarGroup { - ASK_AI, - BLOCK_SELECT, - ADD_LINK, - COMMENT, - TEXT_FORMAT, - TEXT_COLOR, - MENTION, - MORE -} -export interface TextBlockToolbarProps { - showGroups: TextBlockToolbarGroup[] -} - - -export interface BlockCommonProps { - version: number; - node: T; -} - -export interface BackendOp { - type: 'update' | 'insert' | 'remove' | 'move' | 'move_range'; - version: number; - data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData; -} -export interface LocalOp { - type: 'update' | 'insert' | 'remove' | 'move' | 'move_range'; - version: number; - data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData; -} - -export interface UpdateOpData { - blockId: string; - value: BlockData; - path: string[]; -} -export interface InsertOpData { - block: BlockInterface; - parentId: string; - prevId?: string -} - -export interface moveRangeOpData { - range: [string, string]; - newParentId: string; - newPrevId?: string -} - -export interface moveOpData { - blockId: string; - newParentId: string; - newPrevId?: string -} - -export interface removeOpData { - blockId: string -} \ No newline at end of file +export interface Document {} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts index 6b17cfbbd9..286c2cb3b5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts @@ -1,194 +1,48 @@ -import * as Y from 'yjs'; -import { IndexeddbPersistence } from 'y-indexeddb'; -import { v4 } from 'uuid'; -import { DocumentData, NestedBlock } from '@/appflowy_app/interfaces/document'; +import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document'; import { createContext } from 'react'; -import { BlockType } from '@/appflowy_app/interfaces'; +import { DocumentBackendService } from './document_bd_svc'; -export type DeltaAttributes = { - retain: number; - attributes: Record; -}; +export const DocumentControllerContext = createContext(null); -export type DeltaRetain = { retain: number }; -export type DeltaDelete = { delete: number }; -export type DeltaInsert = { - insert: string | Y.XmlText; - attributes?: Record; -}; +export class DocumentController { + private readonly backendService: DocumentBackendService; -export type InsertDelta = Array; -export type Delta = Array< - DeltaRetain | DeltaDelete | DeltaInsert | DeltaAttributes ->; - - -export const YDocControllerContext = createContext(null); - -export class YDocController { - private _ydoc: Y.Doc; - private readonly provider: IndexeddbPersistence; - - constructor(private id: string) { - this._ydoc = new Y.Doc(); - this.provider = new IndexeddbPersistence(`document-${this.id}`, this._ydoc); - this._ydoc.on('update', this.handleUpdate); + constructor(public readonly viewId: string) { + this.backendService = new DocumentBackendService(viewId); } - handleUpdate = (update: Uint8Array, origin: any) => { - const isLocal = origin === null; - Y.logUpdate(update); - } - - - createDocument = async () => { - await this.provider.whenSynced; - const ydoc = this._ydoc; - const blocks = ydoc.getMap('blocks'); - const rootNode = ydoc.getArray("root"); - - // create page block for root node - const rootId = v4(); - rootNode.push([rootId]) - const rootChildrenId = v4(); - const rootChildren = ydoc.getArray(rootChildrenId); - const rootTitleId = v4(); - const yTitle = ydoc.getText(rootTitleId); - yTitle.insert(0, ""); - const root = { - id: rootId, - type: 'page', - data: { - text: rootTitleId - }, - parent: null, - children: rootChildrenId - }; - blocks.set(root.id, root); - - // create text block for first line - const textId = v4(); - const yTextId = v4(); - const ytext = ydoc.getText(yTextId); - ytext.insert(0, ""); - const textChildrenId = v4(); - ydoc.getArray(textChildrenId); - const text = { - id: textId, - type: 'text', - data: { - text: yTextId, - }, - parent: rootId, - children: textChildrenId, + open = async (): Promise => { + const openDocumentResult = await this.backendService.open(); + if (openDocumentResult.ok) { + return { + rootId: '', + blocks: {}, + ytexts: {}, + yarrays: {} + }; + } else { + return null; } - - // add text block to root children - rootChildren.push([textId]); - blocks.set(text.id, text); - } + }; - open = async (): Promise => { - await this.provider.whenSynced; - const ydoc = this._ydoc; - - const blocks = ydoc.getMap('blocks'); - const obj: DocumentData = { - rootId: ydoc.getArray('root').toArray()[0] || '', - blocks: blocks.toJSON(), - ytexts: {}, - yarrays: {} - }; - - Object.keys(obj.blocks).forEach(key => { - const value = obj.blocks[key]; - if (value.children) { - const yarray = ydoc.getArray(value.children); - Object.assign(obj.yarrays, { - [value.children]: yarray.toArray() - }); - } - if (value.data.text) { - const ytext = ydoc.getText(value.data.text); - Object.assign(obj.ytexts, { - [value.data.text]: ytext.toDelta() - }) - } - }); - - blocks.observe(this.handleBlocksEvent); - return obj; - } insert(node: { id: string, type: BlockType, - delta?: Delta + delta?: TextDelta[] }, parentId: string, prevId: string) { - const blocks = this._ydoc.getMap('blocks'); - const parent = blocks.get(parentId); - if (!parent) return; - const insertNode = { - id: node.id, - type: node.type, - data: { - text: '' - }, - children: '', - parent: '' - } - // create ytext - if (node.delta) { - const ytextId = v4(); - const ytext = this._ydoc.getText(ytextId); - ytext.applyDelta(node.delta); - insertNode.data.text = ytextId; - } - // create children - const yArrayId = v4(); - this._ydoc.getArray(yArrayId); - insertNode.children = yArrayId; - // insert in parent's children - const children = this._ydoc.getArray(parent.children); - const index = children.toArray().indexOf(prevId) + 1; - children.insert(index, [node.id]); - insertNode.parent = parentId; - // set in blocks - this._ydoc.getMap('blocks').set(node.id, insertNode); + // } transact(actions: (() => void)[]) { - const ydoc = this._ydoc; - console.log('====transact') - ydoc.transact(() => { - actions.forEach(action => { - action(); - }); - }); + // } - yTextApply = (yTextId: string, delta: Delta) => { - const ydoc = this._ydoc; - const ytext = ydoc.getText(yTextId); - ytext.applyDelta(delta); - console.log("====", yTextId, delta); - } - - close = () => { - const blocks = this._ydoc.getMap('blocks'); - blocks.unobserve(this.handleBlocksEvent); - } - - private handleBlocksEvent = (mapEvent: Y.YMapEvent) => { - console.log(mapEvent.changes); - } - - private handleTextEvent = (textEvent: Y.YTextEvent) => { - console.log(textEvent.changes); - } - - private handleArrayEvent = (arrayEvent: Y.YArrayEvent) => { - console.log(arrayEvent.changes); + yTextApply = (yTextId: string, delta: TextDelta[]) => { + // } + dispose = async () => { + await this.backendService.close(); + }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts index 4ef40d3781..dc19a528de 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts @@ -1,17 +1,8 @@ -import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document"; +import { BlockType, NestedBlock, TextDelta } from "@/appflowy_app/interfaces/document"; import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { RegionGrid } from "./region_grid"; -export interface Node { - id: string; - type: BlockType; - data: { - text?: string; - style?: Record - }; - parent: string | null; - children: string; -} +export type Node = NestedBlock; export interface NodeState { nodes: Record; @@ -33,11 +24,11 @@ export const documentSlice = createSlice({ name: 'document', initialState: initialState, reducers: { - clear: (state, action: PayloadAction) => { + clear: () => { return initialState; }, - createTree: (state, action: PayloadAction<{ + create: (state, action: PayloadAction<{ nodes: Record; children: Record; delta: Record; @@ -52,7 +43,7 @@ export const documentSlice = createSlice({ state.selections = action.payload; }, - changeSelectionByIntersectRect: (state, action: PayloadAction<{ + setSelectionByRect: (state, action: PayloadAction<{ startX: number; startY: number; endX: number; @@ -77,26 +68,57 @@ export const documentSlice = createSlice({ regionGrid.updateBlock(id, position); }, + addNode: (state, action: PayloadAction) => { + state.nodes[action.payload.id] = action.payload; + }, + + addChild: (state, action: PayloadAction<{ parentId: string, childId: string, prevId: string }>) => { + const { parentId, childId, prevId } = action.payload; + const parentChildrenId = state.nodes[parentId].children; + const children = state.children[parentChildrenId]; + const prevIndex = children.indexOf(prevId); + if (prevIndex === -1) { + children.push(childId) + } else { + children.splice(prevIndex + 1, 0, childId); + } + }, + + updateChildren: (state, action: PayloadAction<{ id: string; childIds: string[] }>) => { + const { id, childIds } = action.payload; + state.children[id] = childIds; + }, + + updateDelta: (state, action: PayloadAction<{ id: string; delta: TextDelta[] }>) => { + const { id, delta } = action.payload; + state.delta[id] = delta; + }, + updateNode: (state, action: PayloadAction<{id: string; type?: BlockType; data?: any }>) => { state.nodes[action.payload.id] = { ...state.nodes[action.payload.id], ...action.payload } }, + removeNode: (state, action: PayloadAction) => { const { children, data, parent } = state.nodes[action.payload]; + // remove from parent if (parent) { const index = state.children[state.nodes[parent].children].indexOf(action.payload); if (index > -1) { state.children[state.nodes[parent].children].splice(index, 1); } } + // remove children if (children) { delete state.children[children]; } + // remove delta if (data && data.text) { delete state.delta[data.text]; } + // remove node delete state.nodes[action.payload]; }, }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts deleted file mode 100644 index 8bc67522ce..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BlockData, BlockType } from "../interfaces"; - - -export function filterSelections(ids: string[], nodeMap: Map): string[] { - const selected = new Set(ids); - const newSelected = new Set(); - ids.forEach(selectedId => { - const node = nodeMap.get(selectedId); - if (!node) return; - if (node.type === BlockType.ListBlock && node.data.type === 'column') { - return; - } - if (node.children.length === 0) { - newSelected.add(selectedId); - return; - } - const hasChildSelected = node.children.some(i => selected.has(i.id)); - if (!hasChildSelected) { - newSelected.add(selectedId); - return; - } - const hasSiblingSelected = node.parent?.children.filter(i => i.id !== selectedId).some(i => selected.has(i.id)); - if (hasChildSelected && hasSiblingSelected) { - newSelected.add(selectedId); - return; - } - }); - - return Array.from(newSelected); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts deleted file mode 100644 index 387b74ff50..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createContext } from "react"; -import { TextBlockManager } from '../../block_editor/blocks/text_block'; - -export const TextBlockContext = createContext<{ - textBlockManager?: TextBlockManager -}>({}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts index c91ba322d4..d9016e2586 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts @@ -6,24 +6,26 @@ import { } from '../../services/backend/events/flowy-document'; import { useParams } from 'react-router-dom'; import { DocumentData } from '../interfaces/document'; -import { YDocController } from '$app/stores/effects/document/document_controller'; +import { DocumentController } from '$app/stores/effects/document/document_controller'; export const useDocument = () => { const params = useParams(); const [ documentId, setDocumentId ] = useState(); const [ documentData, setDocumentData ] = useState(); - const [ controller, setController ] = useState(null); + const [ controller, setController ] = useState(null); useEffect(() => { void (async () => { if (!params?.id) return; - const c = new YDocController(params.id); + const c = new DocumentController(params.id); setController(c); const res = await c.open(); console.log(res) + if (!res) return; setDocumentData(res) setDocumentId(params.id) + })(); return () => { console.log('==== leave ====', params?.id) diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx index 7386c106a6..301c241081 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx @@ -1,7 +1,7 @@ import { useDocument } from './DocumentPage.hooks'; import { createTheme, ThemeProvider } from '@mui/material'; import Root from '../components/document/Root'; -import { YDocControllerContext } from '../stores/effects/document/document_controller'; +import { DocumentControllerContext } from '../stores/effects/document/document_controller'; const theme = createTheme({ typography: { @@ -15,9 +15,9 @@ export const DocumentPage = () => { if (!documentId || !documentData || !controller) return null; return ( - + - + ); }; From 9ed5bfa51e3a9a6b17ea2b15762e4b54894ca1bf Mon Sep 17 00:00:00 2001 From: qinluhe Date: Thu, 13 Apr 2023 19:41:31 +0800 Subject: [PATCH 7/7] fix: delete useless files --- frontend/appflowy_tauri/package.json | 10 +- frontend/appflowy_tauri/pnpm-lock.yaml | 130 ++++++++++++++++++ .../DocumentTitle/DocumentTitle.hooks.ts | 7 +- .../components/document/Node/Node.hooks.ts | 34 ++--- .../components/document/Node/index.tsx | 23 +++- .../components/document/Root/Tree.hooks.tsx | 3 +- .../components/document/Root/index.tsx | 2 +- .../document/TextBlock/BindYjs.hooks.ts | 61 -------- .../document/TextBlock/TextBlock.hooks.ts | 4 +- .../components/document/TextBlock/index.tsx | 8 +- .../VirtualizedList.hooks.tsx | 0 .../index.tsx | 0 .../VirtualizerList/VirtualizerList.hooks.tsx | 21 --- .../_shared/HoveringToolbar/index.hooks.ts | 7 - .../_shared/HoveringToolbar/index.tsx | 4 - .../document/_shared/SubscribeNode.hooks.ts | 28 ++-- .../document/_shared/TextInput.hooks.ts | 56 ++++---- .../src/appflowy_app/interfaces/document.ts | 11 +- .../effects/document/document_controller.ts | 27 ++-- .../stores/reducers/document/slice.ts | 80 +++++------ 20 files changed, 274 insertions(+), 242 deletions(-) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts rename frontend/appflowy_tauri/src/appflowy_app/components/document/{VirtualizerList => VirtualizedList}/VirtualizedList.hooks.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/document/{VirtualizerList => VirtualizedList}/index.tsx (100%) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index 9cac4d87c2..b801eb8384 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -23,6 +23,7 @@ "@slate-yjs/core": "^0.3.1", "@tanstack/react-virtual": "3.0.0-beta.54", "@tauri-apps/api": "^1.2.0", + "dayjs": "^1.11.7", "events": "^3.3.0", "google-protobuf": "^3.21.2", "i18next": "^22.4.10", @@ -32,6 +33,8 @@ "nanoid": "^4.0.0", "protoc-gen-ts": "^0.8.5", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-calendar": "^4.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", "react-i18next": "^12.2.0", @@ -44,8 +47,8 @@ "slate-react": "^0.91.9", "ts-results": "^3.3.0", "utf8": "^3.0.0", - "yjs": "^13.5.51", - "y-indexeddb": "^9.0.9" + "y-indexeddb": "^9.0.9", + "yjs": "^13.5.51" }, "devDependencies": { "@tauri-apps/cli": "^1.2.2", @@ -53,6 +56,7 @@ "@types/is-hotkey": "^0.1.7", "@types/node": "^18.7.10", "@types/react": "^18.0.15", + "@types/react-beautiful-dnd": "^13.1.3", "@types/react-dom": "^18.0.6", "@types/utf8": "^3.0.1", "@types/uuid": "^9.0.1", @@ -70,4 +74,4 @@ "uuid": "^9.0.0", "vite": "^4.0.0" } -} +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml index 426fb22859..335502f468 100644 --- a/frontend/appflowy_tauri/pnpm-lock.yaml +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -14,6 +14,7 @@ specifiers: '@types/is-hotkey': ^0.1.7 '@types/node': ^18.7.10 '@types/react': ^18.0.15 + '@types/react-beautiful-dnd': ^13.1.3 '@types/react-dom': ^18.0.6 '@types/utf8': ^3.0.1 '@types/uuid': ^9.0.1 @@ -21,6 +22,7 @@ specifiers: '@typescript-eslint/parser': ^5.51.0 '@vitejs/plugin-react': ^3.0.0 autoprefixer: ^10.4.13 + dayjs: ^1.11.7 eslint: ^8.34.0 eslint-plugin-react: ^7.32.2 events: ^3.3.0 @@ -35,6 +37,8 @@ specifiers: prettier-plugin-tailwindcss: ^0.2.2 protoc-gen-ts: ^0.8.5 react: ^18.2.0 + react-beautiful-dnd: ^13.1.1 + react-calendar: ^4.1.0 react-dom: ^18.2.0 react-error-boundary: ^3.1.4 react-i18next: ^12.2.0 @@ -63,6 +67,7 @@ dependencies: '@slate-yjs/core': 0.3.1_slate@0.91.4+yjs@13.5.51 '@tanstack/react-virtual': 3.0.0-beta.54_react@18.2.0 '@tauri-apps/api': 1.2.0 + dayjs: 1.11.7 events: 3.3.0 google-protobuf: 3.21.2 i18next: 22.4.10 @@ -72,6 +77,8 @@ dependencies: nanoid: 4.0.1 protoc-gen-ts: 0.8.6_ss7alqtodw6rv4lluxhr36xjoa react: 18.2.0 + react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y + react-calendar: 4.2.1_biqbaboplfbrettd7655fr4n2y react-dom: 18.2.0_react@18.2.0 react-error-boundary: 3.1.4_react@18.2.0 react-i18next: 12.2.0_3yopsigl4h4eb2nqrqfsy65uwi @@ -93,6 +100,7 @@ devDependencies: '@types/is-hotkey': 0.1.7 '@types/node': 18.14.6 '@types/react': 18.0.28 + '@types/react-beautiful-dnd': 13.1.4 '@types/react-dom': 18.0.11 '@types/utf8': 3.0.1 '@types/uuid': 9.0.1 @@ -1513,6 +1521,12 @@ packages: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true + /@types/lodash.memoize/4.1.7: + resolution: {integrity: sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==} + dependencies: + '@types/lodash': 4.14.191 + dev: false + /@types/lodash/4.14.191: resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} dev: false @@ -1531,6 +1545,12 @@ packages: /@types/prop-types/15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + /@types/react-beautiful-dnd/13.1.4: + resolution: {integrity: sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA==} + dependencies: + '@types/react': 18.0.28 + dev: true + /@types/react-dom/18.0.11: resolution: {integrity: sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==} dependencies: @@ -1542,6 +1562,15 @@ packages: '@types/react': 18.0.28 dev: false + /@types/react-redux/7.1.25: + resolution: {integrity: sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==} + dependencies: + '@types/hoist-non-react-statics': 3.3.1 + '@types/react': 18.0.28 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + dev: false + /@types/react-transition-group/4.4.5: resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} dependencies: @@ -1734,6 +1763,10 @@ packages: - supports-color dev: true + /@wojtekmaj/date-utils/1.1.3: + resolution: {integrity: sha512-rHrDuTl1cx5LYo8F4K4HVauVjwzx4LwrKfEk4br4fj4nK8JjJZ8IG6a6pBHkYmPLBQHCOEDwstb0WNXMGsmdOw==} + dev: false + /acorn-jsx/5.3.2_acorn@8.8.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2149,6 +2182,12 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /css-box-model/1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + dependencies: + tiny-invariant: 1.0.6 + dev: false + /cssesc/3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2158,6 +2197,10 @@ packages: /csstype/3.1.1: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + /dayjs/1.11.7: + resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} + dev: false + /debug/4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2723,6 +2766,13 @@ packages: get-intrinsic: 1.2.0 dev: true + /get-user-locale/2.2.1: + resolution: {integrity: sha512-3814zipTZ2MvczOcppEXB3jXu+0HWwj5WmPI6//SeCnUIUaRXu7W4S54eQZTEPadlMZefE+jAlPOn+zY3tD4Qw==} + dependencies: + '@types/lodash.memoize': 4.1.7 + lodash.memoize: 4.1.2 + dev: false + /glob-parent/5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -3635,6 +3685,10 @@ packages: p-locate: 5.0.0 dev: true + /lodash.memoize/4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: false + /lodash.merge/4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -3680,6 +3734,10 @@ packages: tmpl: 1.0.5 dev: false + /memoize-one/5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: false @@ -4121,6 +4179,44 @@ packages: engines: {node: '>=10'} dev: true + /raf-schd/4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + dev: false + + /react-beautiful-dnd/13.1.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.21.0 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-redux: 7.2.9_biqbaboplfbrettd7655fr4n2y + redux: 4.2.1 + use-memo-one: 1.1.3_react@18.2.0 + transitivePeerDependencies: + - react-native + dev: false + + /react-calendar/4.2.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-T5oKXD+KLy/g6bmJJkZ7E9wj0iRMesWMZcrC7q2kI6ybOsu9NlPQx8uXJzG4A4C3Sh5Xi0deznyzWIVsUpF8tA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@types/react': 18.0.28 + '@wojtekmaj/date-utils': 1.1.3 + clsx: 1.2.1 + get-user-locale: 2.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + /react-dom/18.2.0_react@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -4164,10 +4260,36 @@ packages: /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + /react-is/17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: false + /react-is/18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: false + /react-redux/7.2.9_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.21.0 + '@types/react-redux': 7.1.25 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 17.0.2 + dev: false + /react-redux/8.0.5_ctrls2ti7t7iutxbwkm5ipogyy: resolution: {integrity: sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==} peerDependencies: @@ -4752,6 +4874,14 @@ packages: punycode: 2.3.0 dev: true + /use-memo-one/1.1.3_react@18.2.0: + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /use-sync-external-store/1.2.0_react@18.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts index dc67320b26..bb99948ed5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts @@ -1,8 +1,7 @@ import { useSubscribeNode } from '../_shared/SubscribeNode.hooks'; export function useDocumentTitle(id: string) { - const { node, delta } = useSubscribeNode(id); + const { node } = useSubscribeNode(id); return { node, - delta - } -} \ No newline at end of file + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts index 1bb2e2b25d..b4739699f6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts @@ -1,11 +1,10 @@ - import { useEffect, useRef } from 'react'; import { useSubscribeNode } from '../_shared/SubscribeNode.hooks'; import { useAppDispatch } from '$app/stores/store'; import { documentActions } from '$app/stores/reducers/document/slice'; export function useNode(id: string) { - const { node, childIds, delta, isSelected } = useSubscribeNode(id); + const { node, childIds, isSelected } = useSubscribeNode(id); const ref = useRef(null); const dispatch = useAppDispatch(); @@ -15,22 +14,23 @@ export function useNode(id: string) { const rect = ref.current.getBoundingClientRect(); const scrollContainer = document.querySelector('.doc-scroller-container') as HTMLDivElement; - dispatch(documentActions.updateNodePosition({ - id, - rect: { - x: rect.x, - y: rect.y + scrollContainer.scrollTop, - height: rect.height, - width: rect.width - } - })) - }, []) - + dispatch( + documentActions.updateNodePosition({ + id, + rect: { + x: rect.x, + y: rect.y + scrollContainer.scrollTop, + height: rect.height, + width: rect.width, + }, + }) + ); + }, []); + return { ref, node, childIds, - delta, - isSelected - } -} \ No newline at end of file + isSelected, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx index bfe2e9649b..b875a329de 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx @@ -7,14 +7,26 @@ import TextBlock from '../TextBlock'; import { TextDelta } from '@/appflowy_app/interfaces/document'; function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes) { - const { node, childIds, delta, isSelected, ref } = useNode(id); + const { node, childIds, isSelected, ref } = useNode(id); console.log('=====', id); - const renderBlock = useCallback((_props: { node: Node; childIds?: string[]; delta?: TextDelta[] }) => { + const renderBlock = useCallback((_props: { node: Node; childIds?: string[] }) => { switch (_props.node.type) { - case 'text': - if (!_props.delta) return null; - return ; + case 'text': { + const delta = _props.node.data.delta; + if (!delta) return null; + return ( + + ); + } default: break; } @@ -27,7 +39,6 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes {isSelected ?
: null} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx index 62e249d354..6153978af9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx @@ -11,8 +11,7 @@ export function useParseTree(documentData: DocumentData) { dispatch( documentActions.create({ nodes: blocks, - delta: meta.text_map, - children: meta.children_map, + children: meta.childrenMap, }) ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx index 556370e6d4..4a5b80a7af 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx @@ -4,7 +4,7 @@ import { useRoot } from './Root.hooks'; import Node from '../Node'; import { withErrorBoundary } from 'react-error-boundary'; import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent'; -import VirtualizedList from '../VirtualizerList'; +import VirtualizedList from '../VirtualizedList'; import { Skeleton } from '@mui/material'; function Root({ documentData }: { documentData: DocumentData }) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts deleted file mode 100644 index 01aa2d204f..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts +++ /dev/null @@ -1,61 +0,0 @@ - - -import { useEffect, useMemo, useRef } from "react"; -import { createEditor } from "slate"; -import { withReact } from "slate-react"; - -import * as Y from 'yjs'; -import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core'; -import { Delta } from '@slate-yjs/core/dist/model/types'; -import { TextDelta } from '@/appflowy_app/interfaces/document'; - -const initialValue = [{ - type: 'paragraph', - children: [{ text: '' }], -}]; - -export function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) { - const yTextRef = useRef(); - // Create a yjs document and get the shared type - const sharedType = useMemo(() => { - const ydoc = new Y.Doc() - const _sharedType = ydoc.get('content', Y.XmlText) as Y.XmlText; - - const insertDelta = slateNodesToInsertDelta(initialValue); - // Load the initial value into the yjs document - _sharedType.applyDelta(insertDelta); - - const yText = insertDelta[0].insert as Y.XmlText; - yTextRef.current = yText; - - return _sharedType; - }, []); - - const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []); - - useEffect(() => { - YjsEditor.connect(editor); - return () => { - yTextRef.current = undefined; - YjsEditor.disconnect(editor); - } - }, [editor]); - - useEffect(() => { - const yText = yTextRef.current; - if (!yText) return; - - const textEventHandler = (event: Y.YTextEvent) => { - update(event.changes.delta as TextDelta[]); - } - yText.applyDelta(delta); - yText.observe(textEventHandler); - - return () => { - yText.unobserve(textEventHandler); - } - }, [delta]) - - - return { editor } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts index a513bb2521..93bf11bd51 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts @@ -4,8 +4,8 @@ import { Descendant, Range } from 'slate'; import { TextDelta } from '$app/interfaces/document'; import { useTextInput } from '../_shared/TextInput.hooks'; -export function useTextBlock(text: string, delta: TextDelta[]) { - const { editor } = useTextInput(text, delta); +export function useTextBlock(delta: TextDelta[]) { + const { editor } = useTextInput(delta); const [value, setValue] = useState([]); const onChange = useCallback( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx index 2c9439af6c..cb6e06d181 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx @@ -4,22 +4,20 @@ import { useTextBlock } from './TextBlock.hooks'; import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import NodeComponent from '../Node'; import HoveringToolbar from '../_shared/HoveringToolbar'; -import { TextDelta } from '@/appflowy_app/interfaces/document'; import React from 'react'; +import { TextDelta } from '@/appflowy_app/interfaces/document'; function TextBlock({ node, childIds, placeholder, - delta, ...props }: { - node: Node; - delta: TextDelta[]; + node: Node & { data: { delta: TextDelta[] } }; childIds?: string[]; placeholder?: string; } & React.HTMLAttributes) { - const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, delta); + const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.delta); return (
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/VirtualizedList.hooks.tsx similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/VirtualizedList.hooks.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/index.tsx similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/index.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx deleted file mode 100644 index c0e543bf5f..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useVirtualizer } from '@tanstack/react-virtual'; -import { useRef } from 'react'; - -const defaultSize = 60; - -export function useVirtualizerList(count: number) { - const parentRef = useRef(null); - - const rowVirtualizer = useVirtualizer({ - count, - getScrollElement: () => parentRef.current, - estimateSize: () => { - return defaultSize; - }, - }); - - return { - rowVirtualizer, - parentRef, - }; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts index cf4c803a38..e8346c0596 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts @@ -1,13 +1,6 @@ import { useEffect, useRef } from 'react'; import { useFocused, useSlate } from 'slate-react'; -<<<<<<<< HEAD:frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts import { calcToolbarPosition } from '$app/utils/slate/toolbar'; - -======== -import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar'; - - ->>>>>>>> 341dce67d45ebe46ae55e11349a19191ac99b4cf:frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts export function useHoveringToolbar(id: string) { const editor = useSlate(); const inFocus = useFocused(); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx index 1bb8911aa2..d4a671ec83 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx @@ -1,9 +1,5 @@ import FormatButton from './FormatButton'; -<<<<<<<< HEAD:frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx import Portal from '../../BlockPortal'; -======== -import Portal from '../BlockPortal'; ->>>>>>>> 341dce67d45ebe46ae55e11349a19191ac99b4cf:frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx import { useHoveringToolbar } from './index.hooks'; const HoveringToolbar = ({ id }: { id: string }) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts index 2adfb073f8..0848828c66 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts @@ -1,7 +1,6 @@ import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import { useAppSelector } from '@/appflowy_app/stores/store'; import { useMemo } from 'react'; -import { TextDelta } from '@/appflowy_app/interfaces/document'; /** * Subscribe to a node and its children @@ -10,37 +9,30 @@ import { TextDelta } from '@/appflowy_app/interfaces/document'; * @param id */ export function useSubscribeNode(id: string) { - const node = useAppSelector(state => state.document.nodes[id]); + const node = useAppSelector((state) => state.document.nodes[id]); - const childIds = useAppSelector(state => { + const childIds = useAppSelector((state) => { const childrenId = state.document.nodes[id]?.children; if (!childrenId) return; return state.document.children[childrenId]; }); - const delta = useAppSelector(state => { - const externalType = state.document.nodes[id]?.externalType; - if (externalType !== 'text') return; - const deltaId = state.document.nodes[id]?.externalId; - if (!deltaId) return; - return state.document.delta[deltaId]; - }); - - const isSelected = useAppSelector(state => { + const isSelected = useAppSelector((state) => { return state.document.selections?.includes(id) || false; }); // Memoize the node and its children // So that the component will not be re-rendered when other node is changed // It very important for performance - const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]); + const memoizedNode = useMemo( + () => node, + [node?.id, JSON.stringify(node?.data), node?.parent, node?.type, node?.children] + ); const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]); - const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]); - + return { node: memoizedNode, childIds: memoizedChildIds, - delta: memoizedDelta, - isSelected + isSelected, }; -} \ No newline at end of file +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts index 579bdccec4..b8d52f1b0d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts @@ -8,8 +8,8 @@ import { withReact } from 'slate-react'; import * as Y from 'yjs'; import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core'; -export function useTextInput(text: string, delta: TextDelta[]) { - const { sendDelta } = useTransact(text); +export function useTextInput(delta: TextDelta[]) { + const { sendDelta } = useTransact(); const { editor } = useBindYjs(delta, sendDelta); return { @@ -17,49 +17,45 @@ export function useTextInput(text: string, delta: TextDelta[]) { }; } -function useController(textId: string) { +function useController() { const docController = useContext(DocumentControllerContext); const update = useCallback( (delta: TextDelta[]) => { - docController?.yTextApply(textId, delta); + docController?.applyActions([ + { + type: 'update', + payload: { + block: { + data: { + delta, + }, + }, + }, + }, + ]); }, - [textId] - ); - const transact = useCallback( - (actions: (() => void)[]) => { - docController?.transact(actions); - }, - [textId] + [docController] ); return { update, - transact, }; } -function useTransact(textId: string) { - const pendingActions = useRef<(() => void)[]>([]); - const { update, transact } = useController(textId); - - const sendTransact = useCallback(() => { - const actions = pendingActions.current; - transact(actions); - }, [transact]); - - const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]); +function useTransact() { + const { update } = useController(); const sendDelta = useCallback( (delta: TextDelta[]) => { - const action = () => update(delta); - pendingActions.current.push(action); - debounceSendTransact(); + update(delta); }, - [update, debounceSendTransact] + [update] ); + const debounceSendDelta = useMemo(() => debounce(sendDelta, 300), [sendDelta]); + return { - sendDelta, + sendDelta: debounceSendDelta, }; } @@ -70,7 +66,7 @@ const initialValue = [ }, ]; -export function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) { +function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) { const yTextRef = useRef(); // Create a yjs document and get the shared type const sharedType = useMemo(() => { @@ -102,7 +98,9 @@ export function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => if (!yText) return; const textEventHandler = (event: Y.YTextEvent) => { - update(event.changes.delta as TextDelta[]); + const textDelta = event.target.toDelta(); + console.log('delta', textDelta); + update(textDelta); }; yText.applyDelta(delta); yText.observe(textEventHandler); diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts index 90d91c6d94..9f4198fbc0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts @@ -10,12 +10,14 @@ export enum BlockType { DividerBlock = 'divider', MediaBlock = 'media', TableBlock = 'table', - ColumnBlock = 'column' + ColumnBlock = 'column', } export interface NestedBlock { id: string; type: BlockType; - data: Record; + data: { + delta?: TextDelta[]; + }; externalId: string; externalType: 'text' | 'array' | 'map'; parent: string | null; @@ -29,7 +31,6 @@ export interface DocumentData { rootId: string; blocks: Record; meta: { - text_map: Record; - children_map: Record; - } + childrenMap: Record; + }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts index 286c2cb3b5..82a24d273b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts @@ -17,30 +17,23 @@ export class DocumentController { return { rootId: '', blocks: {}, - ytexts: {}, - yarrays: {} + meta: { + childrenMap: {}, + }, }; } else { return null; } }; - - insert(node: { - id: string, - type: BlockType, - delta?: TextDelta[] - }, parentId: string, prevId: string) { + applyActions = ( + actions: { + type: string; + payload: any; + }[] + ) => { // - } - - transact(actions: (() => void)[]) { - // - } - - yTextApply = (yTextId: string, delta: TextDelta[]) => { - // - } + }; dispose = async () => { await this.backendService.close(); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts index dc19a528de..d1fe6a8027 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts @@ -1,13 +1,12 @@ -import { BlockType, NestedBlock, TextDelta } from "@/appflowy_app/interfaces/document"; -import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { RegionGrid } from "./region_grid"; +import { BlockType, NestedBlock, TextDelta } from '@/appflowy_app/interfaces/document'; +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { RegionGrid } from './region_grid'; export type Node = NestedBlock; export interface NodeState { nodes: Record; children: Record; - delta: Record; selections: string[]; } @@ -16,7 +15,6 @@ const regionGrid = new RegionGrid(50); const initialState: NodeState = { nodes: {}, children: {}, - delta: {}, selections: [], }; @@ -28,42 +26,52 @@ export const documentSlice = createSlice({ return initialState; }, - create: (state, action: PayloadAction<{ - nodes: Record; - children: Record; - delta: Record; - }>) => { - const { nodes, children, delta } = action.payload; + create: ( + state, + action: PayloadAction<{ + nodes: Record; + children: Record; + }> + ) => { + const { nodes, children } = action.payload; state.nodes = nodes; state.children = children; - state.delta = delta; }, updateSelections: (state, action: PayloadAction) => { state.selections = action.payload; }, - setSelectionByRect: (state, action: PayloadAction<{ - startX: number; - startY: number; - endX: number; - endY: number - }>) => { + setSelectionByRect: ( + state, + action: PayloadAction<{ + startX: number; + startY: number; + endX: number; + endY: number; + }> + ) => { const { startX, startY, endX, endY } = action.payload; const blocks = regionGrid.getIntersectBlocks(startX, startY, endX, endY); - state.selections = blocks.map(block => block.id); + state.selections = blocks.map((block) => block.id); }, - updateNodePosition: (state, action: PayloadAction<{id: string; rect: { - x: number; - y: number; - width: number; - height: number; - }}>) => { + updateNodePosition: ( + state, + action: PayloadAction<{ + id: string; + rect: { + x: number; + y: number; + width: number; + height: number; + }; + }> + ) => { const { id, rect } = action.payload; const position = { id, - ...rect + ...rect, }; regionGrid.updateBlock(id, position); }, @@ -72,13 +80,13 @@ export const documentSlice = createSlice({ state.nodes[action.payload.id] = action.payload; }, - addChild: (state, action: PayloadAction<{ parentId: string, childId: string, prevId: string }>) => { + addChild: (state, action: PayloadAction<{ parentId: string; childId: string; prevId: string }>) => { const { parentId, childId, prevId } = action.payload; const parentChildrenId = state.nodes[parentId].children; const children = state.children[parentChildrenId]; const prevIndex = children.indexOf(prevId); if (prevIndex === -1) { - children.push(childId) + children.push(childId); } else { children.splice(prevIndex + 1, 0, childId); } @@ -89,16 +97,11 @@ export const documentSlice = createSlice({ state.children[id] = childIds; }, - updateDelta: (state, action: PayloadAction<{ id: string; delta: TextDelta[] }>) => { - const { id, delta } = action.payload; - state.delta[id] = delta; - }, - - updateNode: (state, action: PayloadAction<{id: string; type?: BlockType; data?: any }>) => { + updateNode: (state, action: PayloadAction<{ id: string; data: any }>) => { state.nodes[action.payload.id] = { ...state.nodes[action.payload.id], - ...action.payload - } + ...action.payload, + }; }, removeNode: (state, action: PayloadAction) => { @@ -114,10 +117,7 @@ export const documentSlice = createSlice({ if (children) { delete state.children[children]; } - // remove delta - if (data && data.text) { - delete state.delta[data.text]; - } + // remove node delete state.nodes[action.payload]; },