From 35c21c0d842797a91946fcd3c09553a6d2e1878e Mon Sep 17 00:00:00 2001 From: qinluhe Date: Mon, 27 Mar 2023 14:40:07 +0800 Subject: [PATCH] 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; -} -