mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Feat appflowy list block (#1949)
* feat: Initialize appflowy block data and render block list * feat: Implement column layout rendering * feat: Implement list redering * feat: Cache block rect info * fix: The input chars will repeated when inputting Chinese * fix: Remove unnecessary fields in the block and encapsulate the block manager * fix: fix ts error
This commit is contained in:
parent
79a43de2d5
commit
ed2c5c17d8
@ -0,0 +1,28 @@
|
||||
import { BlockInterface, BlockType } from '$app/interfaces/index';
|
||||
|
||||
|
||||
export class BlockDataManager {
|
||||
private head: BlockInterface<BlockType.PageBlock> | null = null;
|
||||
constructor(id: string, private map: Record<string, BlockInterface<BlockType>> | null) {
|
||||
if (!map) return;
|
||||
this.head = map[id];
|
||||
}
|
||||
|
||||
setBlocksMap = (id: string, map: Record<string, BlockInterface<BlockType>>) => {
|
||||
this.map = map;
|
||||
this.head = map[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* get block data
|
||||
* @param blockId string
|
||||
* @returns Block
|
||||
*/
|
||||
getBlock = (blockId: string) => {
|
||||
return this.map?.[blockId] || null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.map = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import { BlockInterface } from '../interfaces';
|
||||
import { BlockDataManager } from './block';
|
||||
import { TreeManager } from './tree';
|
||||
|
||||
/**
|
||||
* BlockEditor is a document data manager that operates on and renders data through managing blockData and RenderTreeManager.
|
||||
* The render tree will be re-render and update react component when block makes changes to the data.
|
||||
* RectManager updates the cache of node rect when the react component update is completed.
|
||||
*/
|
||||
export class BlockEditor {
|
||||
// blockData manages document block data, including operations such as add, delete, update, and move.
|
||||
public blockData: BlockDataManager;
|
||||
// RenderTreeManager manages data rendering, including the construction and updating of the render tree.
|
||||
public renderTree: TreeManager;
|
||||
|
||||
constructor(private id: string, data: Record<string, BlockInterface>) {
|
||||
this.blockData = new BlockDataManager(id, data);
|
||||
this.renderTree = new TreeManager(this.blockData.getBlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* update id and map when the doc is change
|
||||
* @param id
|
||||
* @param data
|
||||
*/
|
||||
changeDoc = (id: string, data: Record<string, BlockInterface>) => {
|
||||
console.log('==== change document ====', id, data)
|
||||
this.id = id;
|
||||
this.blockData.setBlocksMap(id, data);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.renderTree.destroy();
|
||||
this.blockData.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let blockEditorInstance: BlockEditor | null;
|
||||
|
||||
export function getBlockEditor() {
|
||||
return blockEditorInstance;
|
||||
}
|
||||
|
||||
export function createBlockEditor(id: string, data: Record<string, BlockInterface>) {
|
||||
blockEditorInstance = new BlockEditor(id, data);
|
||||
return blockEditorInstance;
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { TreeNodeInterface } from "../interfaces";
|
||||
|
||||
|
||||
export function calculateBlockRect(blockId: string) {
|
||||
const el = document.querySelectorAll(`[data-block-id=${blockId}]`)[0];
|
||||
return el?.getBoundingClientRect();
|
||||
}
|
||||
|
||||
export class RectManager {
|
||||
map: Map<string, DOMRect>;
|
||||
|
||||
orderList: Set<string>;
|
||||
|
||||
private updatedQueue: Set<string>;
|
||||
|
||||
constructor(private getTreeNode: (nodeId: string) => TreeNodeInterface | null) {
|
||||
this.map = new Map();
|
||||
this.orderList = new Set();
|
||||
this.updatedQueue = new Set();
|
||||
}
|
||||
|
||||
build() {
|
||||
console.log('====update all blocks position====')
|
||||
this.orderList.forEach(id => this.updateNodeRect(id));
|
||||
}
|
||||
|
||||
getNodeRect = (nodeId: string) => {
|
||||
return this.map.get(nodeId) || null;
|
||||
}
|
||||
|
||||
update() {
|
||||
// In order to avoid excessive calculation frequency
|
||||
// calculate and update the block position information in the queue every frame
|
||||
requestAnimationFrame(() => {
|
||||
// there is nothing to do if the updated queue is empty
|
||||
if (this.updatedQueue.size === 0) return;
|
||||
console.log(`==== update ${this.updatedQueue.size} blocks rect cache ====`)
|
||||
this.updatedQueue.forEach((id: string) => {
|
||||
const rect = calculateBlockRect(id);
|
||||
this.map.set(id, rect);
|
||||
this.updatedQueue.delete(id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateNodeRect = (nodeId: string) => {
|
||||
if (this.updatedQueue.has(nodeId)) return;
|
||||
let node: TreeNodeInterface | null = this.getTreeNode(nodeId);
|
||||
|
||||
// When one of the blocks is updated
|
||||
// the positions of all its parent and child blocks need to be updated
|
||||
while(node) {
|
||||
node.parent?.children.forEach(child => this.updatedQueue.add(child.id));
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.map.clear();
|
||||
this.orderList.clear();
|
||||
this.updatedQueue.clear();
|
||||
}
|
||||
|
||||
}
|
140
frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts
Normal file
140
frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { RectManager } from "./rect";
|
||||
import { BlockInterface, BlockData, BlockType, TreeNodeInterface } from '../interfaces/index';
|
||||
|
||||
export class TreeManager {
|
||||
|
||||
// RenderTreeManager holds RectManager, which manages the position information of each node in the render tree.
|
||||
private rect: RectManager;
|
||||
|
||||
root: TreeNode | null = null;
|
||||
|
||||
map: Map<string, TreeNode> = new Map();
|
||||
|
||||
constructor(private getBlock: (blockId: string) => BlockInterface | null) {
|
||||
this.rect = new RectManager(this.getTreeNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get render node data by nodeId
|
||||
* @param nodeId string
|
||||
* @returns TreeNode
|
||||
*/
|
||||
getTreeNode = (nodeId: string): TreeNodeInterface | null => {
|
||||
return this.map.get(nodeId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* build tree node for rendering
|
||||
* @param rootId
|
||||
* @returns
|
||||
*/
|
||||
build(rootId: string): TreeNode | null {
|
||||
const head = this.getBlock(rootId);
|
||||
|
||||
if (!head) return null;
|
||||
|
||||
this.root = new TreeNode(head);
|
||||
|
||||
let node = this.root;
|
||||
|
||||
// loop line
|
||||
while (node) {
|
||||
this.map.set(node.id, node);
|
||||
this.rect.orderList.add(node.id);
|
||||
|
||||
const block = this.getBlock(node.id)!;
|
||||
const next = block.next ? this.getBlock(block.next) : null;
|
||||
const firstChild = block.firstChild ? this.getBlock(block.firstChild) : null;
|
||||
|
||||
// find next line
|
||||
if (firstChild) {
|
||||
// the next line is node's first child
|
||||
const child = new TreeNode(firstChild);
|
||||
node.addChild(child);
|
||||
node = child;
|
||||
} else if (next) {
|
||||
// the next line is node's sibling
|
||||
const sibling = new TreeNode(next);
|
||||
node.parent?.addChild(sibling);
|
||||
node = sibling;
|
||||
} else {
|
||||
// the next line is parent's sibling
|
||||
let isFind = false;
|
||||
while(node.parent) {
|
||||
const parentId = node.parent.id;
|
||||
const parent = this.getBlock(parentId)!;
|
||||
const parentNext = parent.next ? this.getBlock(parent.next) : null;
|
||||
if (parentNext) {
|
||||
const parentSibling = new TreeNode(parentNext);
|
||||
node.parent?.parent?.addChild(parentSibling);
|
||||
node = parentSibling;
|
||||
isFind = true;
|
||||
break;
|
||||
} else {
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFind) {
|
||||
// Exit if next line not found
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return this.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* update dom rects cache
|
||||
*/
|
||||
updateRects = () => {
|
||||
this.rect.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* get block rect cache
|
||||
* @param id string
|
||||
* @returns DOMRect
|
||||
*/
|
||||
getNodeRect = (nodeId: string) => {
|
||||
return this.rect.getNodeRect(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* update block rect cache
|
||||
* @param id string
|
||||
*/
|
||||
updateNodeRect = (nodeId: string) => {
|
||||
this.rect.updateNodeRect(nodeId);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.rect?.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TreeNode implements TreeNodeInterface {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
parent: TreeNode | null = null;
|
||||
children: TreeNode[] = [];
|
||||
data: BlockData<BlockType>;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
type,
|
||||
data
|
||||
}: BlockInterface) {
|
||||
this.id = id;
|
||||
this.data = data;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
addChild(node: TreeNode) {
|
||||
node.parent = this;
|
||||
this.children.push(node);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { useSlate } from 'slate-react';
|
||||
import { toggleFormat, isFormatActive } from '$app/utils/editor/format';
|
||||
import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { useMemo } from 'react';
|
||||
|
@ -0,0 +1,9 @@
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
const Portal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => {
|
||||
const root = document.querySelectorAll(`[data-block-id=${blockId}] > .block-overlay`)[0];
|
||||
|
||||
return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null;
|
||||
};
|
||||
|
||||
export default Portal;
|
@ -1,8 +1,8 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useFocused, useSlate } from 'slate-react';
|
||||
import FormatButton from './FormatButton';
|
||||
import { Portal } from './components';
|
||||
import { calcToolbarPosition } from '$app/utils/editor/toolbar';
|
||||
import Portal from './Portal';
|
||||
import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar';
|
||||
|
||||
const HoveringToolbar = ({ blockId }: { blockId: string }) => {
|
||||
const editor = useSlate();
|
||||
@ -13,10 +13,7 @@ const HoveringToolbar = ({ blockId }: { blockId: string }) => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const blockDom = document.querySelectorAll(`[data-block-id=${blockId}]`)[0];
|
||||
const blockRect = blockDom?.getBoundingClientRect();
|
||||
|
||||
const position = calcToolbarPosition(editor, el, blockRect);
|
||||
const position = calcToolbarPosition(editor, el, blockId);
|
||||
|
||||
if (!position) {
|
||||
el.style.opacity = '0';
|
||||
@ -33,6 +30,9 @@ const HoveringToolbar = ({ blockId }: { blockId: string }) => {
|
||||
<Portal blockId={blockId}>
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
opacity: 0,
|
||||
}}
|
||||
className='z-1 absolute mt-[-6px] inline-flex h-[32px] items-stretch overflow-hidden rounded-[8px] bg-[#333] p-2 leading-tight shadow-lg transition-opacity duration-700'
|
||||
onMouseDown={(e) => {
|
||||
// prevent toolbar from taking focus away from editor
|
||||
|
@ -1,33 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Block, BlockType } from '$app/interfaces';
|
||||
import { BlockType, TreeNodeInterface } from '$app/interfaces';
|
||||
import PageBlock from '../PageBlock';
|
||||
import TextBlock from '../TextBlock';
|
||||
import HeadingBlock from '../HeadingBlock';
|
||||
import ListBlock from '../ListBlock';
|
||||
import CodeBlock from '../CodeBlock';
|
||||
|
||||
export default function BlockComponent({ block }: { block: Block }) {
|
||||
function BlockComponent({
|
||||
node,
|
||||
...props
|
||||
}: { node: TreeNodeInterface } & React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {
|
||||
const renderComponent = () => {
|
||||
switch (block.type) {
|
||||
switch (node.type) {
|
||||
case BlockType.PageBlock:
|
||||
return <PageBlock block={block} />;
|
||||
return <PageBlock node={node} />;
|
||||
case BlockType.TextBlock:
|
||||
return <TextBlock block={block} />;
|
||||
return <TextBlock node={node} />;
|
||||
case BlockType.HeadingBlock:
|
||||
return <HeadingBlock block={block} />;
|
||||
return <HeadingBlock node={node} />;
|
||||
case BlockType.ListBlock:
|
||||
return <ListBlock block={block} />;
|
||||
return <ListBlock node={node} />;
|
||||
case BlockType.CodeBlock:
|
||||
return <CodeBlock block={block} />;
|
||||
|
||||
return <CodeBlock node={node} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative' data-block-id={block.id}>
|
||||
<div className='relative' data-block-id={node.id} {...props}>
|
||||
{renderComponent()}
|
||||
{props.children}
|
||||
<div className='block-overlay'></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BlockComponent);
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { BlockContext } from "$app/utils/block_context";
|
||||
import { buildTree } from "$app/utils/tree";
|
||||
import { Block } from "$app/interfaces";
|
||||
|
||||
export function useBlockList() {
|
||||
const blockContext = useContext(BlockContext);
|
||||
|
||||
const [blockList, setBlockList] = useState<Block[]>([]);
|
||||
|
||||
const [title, setTitle] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!blockContext) return;
|
||||
const { blocksMap, id } = blockContext;
|
||||
if (!id || !blocksMap) return;
|
||||
const root = buildTree(id, blocksMap);
|
||||
if (!root) return;
|
||||
console.log(root);
|
||||
setTitle(root.data.title);
|
||||
setBlockList(root.children || []);
|
||||
}, [blockContext]);
|
||||
|
||||
return {
|
||||
title,
|
||||
blockList
|
||||
}
|
||||
}
|
@ -1,17 +1,43 @@
|
||||
import { useBlockList } from './BlockList.hooks';
|
||||
import BlockComponent from './BlockComponent';
|
||||
import React, { useEffect } from 'react';
|
||||
import { debounce } from '@/appflowy_app/utils/tool';
|
||||
import { getBlockEditor } from '../../../block_editor';
|
||||
|
||||
export default function BlockList() {
|
||||
const { blockList, title } = useBlockList();
|
||||
const RESIZE_DELAY = 200;
|
||||
|
||||
function BlockList({ blockId }: { blockId: string }) {
|
||||
const blockEditor = getBlockEditor();
|
||||
if (!blockEditor) return null;
|
||||
|
||||
const root = blockEditor.renderTree.build(blockId);
|
||||
console.log('==== build tree ====', root);
|
||||
|
||||
useEffect(() => {
|
||||
// update rect cache when did mount
|
||||
blockEditor.renderTree.updateRects();
|
||||
|
||||
const resize = debounce(() => {
|
||||
// update rect cache when window resized
|
||||
blockEditor.renderTree.updateRects();
|
||||
}, RESIZE_DELAY);
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='min-x-[0%] p-lg w-[900px] max-w-[100%]'>
|
||||
<div className='my-[50px] flex px-14 text-4xl font-bold'>{title}</div>
|
||||
<div className='my-[50px] flex px-14 text-4xl font-bold'>{root?.data.title}</div>
|
||||
<div className='px-14'>
|
||||
{blockList?.map((block) => (
|
||||
<BlockComponent key={block.id} block={block} />
|
||||
))}
|
||||
{root && root.children.length > 0
|
||||
? root.children.map((node) => <BlockComponent key={node.id} node={node} />)
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BlockList);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Block } from '$app/interfaces';
|
||||
import { TreeNodeInterface } from '$app/interfaces';
|
||||
|
||||
export default function CodeBlock({ block }: { block: Block }) {
|
||||
return <div>{block.data.text}</div>;
|
||||
export default function CodeBlock({ node }: { node: TreeNodeInterface }) {
|
||||
return <div>{node.data.text}</div>;
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { TreeNodeInterface } from '$app/interfaces/index';
|
||||
import BlockComponent from '../BlockList/BlockComponent';
|
||||
|
||||
export default function ColumnBlock({
|
||||
node,
|
||||
resizerWidth,
|
||||
index,
|
||||
}: {
|
||||
node: TreeNodeInterface;
|
||||
resizerWidth: number;
|
||||
index: number;
|
||||
}) {
|
||||
const renderResizer = () => {
|
||||
return (
|
||||
<div className={`relative w-[46px] flex-shrink-0 flex-grow-0 transition-opacity`} style={{ opacity: 0 }}></div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{index === 0 ? (
|
||||
<div className='contents'>
|
||||
<div
|
||||
className='absolute flex'
|
||||
style={{
|
||||
inset: '0px 100% 0px auto',
|
||||
}}
|
||||
>
|
||||
{renderResizer()}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
renderResizer()
|
||||
)}
|
||||
|
||||
<BlockComponent
|
||||
className={`column-block py-3`}
|
||||
style={{
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
width: `calc((100% - ${resizerWidth}px) * ${node.data.ratio})`,
|
||||
}}
|
||||
node={node}
|
||||
>
|
||||
{node.children?.map((item) => (
|
||||
<BlockComponent key={item.id} node={item} />
|
||||
))}
|
||||
</BlockComponent>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Block } from '$app/interfaces';
|
||||
import TextBlock from '../TextBlock';
|
||||
import { TreeNodeInterface } from '$app/interfaces/index';
|
||||
|
||||
const fontSize: Record<string, string> = {
|
||||
1: 'mt-8 text-3xl',
|
||||
2: 'mt-6 text-2xl',
|
||||
3: 'mt-4 text-xl',
|
||||
};
|
||||
export default function HeadingBlock({ block }: { block: Block }) {
|
||||
export default function HeadingBlock({ node }: { node: TreeNodeInterface }) {
|
||||
return (
|
||||
<div className={`${fontSize[block.data.level]} font-semibold `}>
|
||||
<div className={`${fontSize[node.data.level]} font-semibold `}>
|
||||
<TextBlock
|
||||
block={{
|
||||
...block,
|
||||
node={{
|
||||
...node,
|
||||
children: [],
|
||||
}}
|
||||
/>
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { Circle } from '@mui/icons-material';
|
||||
|
||||
import BlockComponent from '../BlockList/BlockComponent';
|
||||
import { TreeNodeInterface } from '$app/interfaces/index';
|
||||
|
||||
export default function BulletedListBlock({ title, node }: { title: JSX.Element; node: TreeNodeInterface }) {
|
||||
return (
|
||||
<div className='bulleted-list-block relative'>
|
||||
<div className='relative flex'>
|
||||
<div className={`relative mb-2 min-w-[24px] leading-5`}>
|
||||
<Circle sx={{ width: 8, height: 8 }} />
|
||||
</div>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div className='pl-[24px]'>
|
||||
{node.children?.map((item) => (
|
||||
<div key={item.id}>
|
||||
<BlockComponent node={item} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { TreeNodeInterface } from '@/appflowy_app/interfaces';
|
||||
import React, { useMemo } from 'react';
|
||||
import ColumnBlock from '../ColumnBlock/index';
|
||||
|
||||
export default function ColumnListBlock({ node }: { node: TreeNodeInterface }) {
|
||||
const resizerWidth = useMemo(() => {
|
||||
return 46 * (node.children?.length || 0);
|
||||
}, [node.children?.length]);
|
||||
return (
|
||||
<>
|
||||
<div className='column-list-block flex-grow-1 flex flex-row'>
|
||||
{node.children?.map((item, index) => (
|
||||
<ColumnBlock key={item.id} index={index} resizerWidth={resizerWidth} node={item} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { TreeNodeInterface } from '@/appflowy_app/interfaces';
|
||||
import React, { useMemo } from 'react';
|
||||
import BlockComponent from '../BlockList/BlockComponent';
|
||||
|
||||
export default function NumberedListBlock({ title, node }: { title: JSX.Element; node: TreeNodeInterface }) {
|
||||
const index = useMemo(() => {
|
||||
const i = node.parent?.children?.findIndex((item) => item.id === node.id) || 0;
|
||||
return i + 1;
|
||||
}, [node]);
|
||||
return (
|
||||
<div className='numbered-list-block'>
|
||||
<div className='relative flex'>
|
||||
<div className={`relative mb-2 min-w-[24px] max-w-[24px]`}>{`${index} .`}</div>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div className='pl-[24px]'>
|
||||
{node.children?.map((item) => (
|
||||
<div key={item.id}>
|
||||
<BlockComponent node={item} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,29 +1,36 @@
|
||||
import React from 'react';
|
||||
import { Block } from '$app/interfaces';
|
||||
import BlockComponent from '../BlockList/BlockComponent';
|
||||
import React, { useMemo } from 'react';
|
||||
import TextBlock from '../TextBlock';
|
||||
import NumberedListBlock from './NumberedListBlock';
|
||||
import BulletedListBlock from './BulletedListBlock';
|
||||
import ColumnListBlock from './ColumnListBlock';
|
||||
import { TreeNodeInterface } from '$app/interfaces/index';
|
||||
|
||||
export default function ListBlock({ block }: { block: Block }) {
|
||||
const renderChildren = () => {
|
||||
return block.children?.map((item) => (
|
||||
<li key={item.id}>
|
||||
<BlockComponent block={item} />
|
||||
</li>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${block.data.type === 'ul' ? 'bulleted_list' : 'number_list'} flex`}>
|
||||
<li className='w-[24px]' />
|
||||
<div>
|
||||
export default function ListBlock({ node }: { node: TreeNodeInterface }) {
|
||||
const title = useMemo(() => {
|
||||
if (node.data.type === 'column') return <></>;
|
||||
return (
|
||||
<div className='flex-1'>
|
||||
<TextBlock
|
||||
block={{
|
||||
...block,
|
||||
node={{
|
||||
...node,
|
||||
children: [],
|
||||
}}
|
||||
/>
|
||||
{renderChildren()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}, [node]);
|
||||
|
||||
if (node.data.type === 'numbered') {
|
||||
return <NumberedListBlock title={title} node={node} />;
|
||||
}
|
||||
|
||||
if (node.data.type === 'bulleted') {
|
||||
return <BulletedListBlock title={title} node={node} />;
|
||||
}
|
||||
|
||||
if (node.data.type === 'column') {
|
||||
return <ColumnListBlock node={node} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Block } from '$app/interfaces';
|
||||
import { TreeNodeInterface } from '$app/interfaces';
|
||||
|
||||
export default function PageBlock({ block }: { block: Block }) {
|
||||
return <div className='cursor-pointer underline'>{block.data.title}</div>;
|
||||
export default function PageBlock({ node }: { node: TreeNodeInterface }) {
|
||||
return <div className='cursor-pointer underline'>{node.data.title}</div>;
|
||||
}
|
||||
|
@ -1,25 +1,38 @@
|
||||
import { BaseText } from 'slate';
|
||||
import { RenderLeafProps } from 'slate-react';
|
||||
|
||||
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
|
||||
const Leaf = ({
|
||||
attributes,
|
||||
children,
|
||||
leaf,
|
||||
}: RenderLeafProps & {
|
||||
leaf: BaseText & {
|
||||
bold?: boolean;
|
||||
code?: boolean;
|
||||
italic?: boolean;
|
||||
underlined?: boolean;
|
||||
strikethrough?: boolean;
|
||||
};
|
||||
}) => {
|
||||
let newChildren = children;
|
||||
if ('bold' in leaf && leaf.bold) {
|
||||
if (leaf.bold) {
|
||||
newChildren = <strong>{children}</strong>;
|
||||
}
|
||||
|
||||
if ('code' in leaf && leaf.code) {
|
||||
if (leaf.code) {
|
||||
newChildren = <code className='rounded-sm bg-[#F2FCFF] p-1'>{newChildren}</code>;
|
||||
}
|
||||
|
||||
if ('italic' in leaf && leaf.italic) {
|
||||
if (leaf.italic) {
|
||||
newChildren = <em>{newChildren}</em>;
|
||||
}
|
||||
|
||||
if ('underlined' in leaf && leaf.underlined) {
|
||||
if (leaf.underlined) {
|
||||
newChildren = <u>{newChildren}</u>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span {...attributes} className={'strikethrough' in leaf && leaf.strikethrough ? `line-through` : ''}>
|
||||
<span {...attributes} className={leaf.strikethrough ? `line-through` : ''}>
|
||||
{newChildren}
|
||||
</span>
|
||||
);
|
||||
|
@ -1,31 +1,52 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Block } from '$app/interfaces';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import { TreeNodeInterface } from '$app/interfaces';
|
||||
import BlockComponent from '../BlockList/BlockComponent';
|
||||
|
||||
import { createEditor } from 'slate';
|
||||
import { Slate, Editable, withReact } from 'slate-react';
|
||||
import Leaf from './Leaf';
|
||||
import HoveringToolbar from '$app/components/HoveringToolbar';
|
||||
import { triggerHotkey } from '$app/utils/editor/hotkey';
|
||||
import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey';
|
||||
import { BlockContext } from '$app/utils/block_context';
|
||||
import { debounce } from '@/appflowy_app/utils/tool';
|
||||
import { getBlockEditor } from '@/appflowy_app/block_editor/index';
|
||||
|
||||
const INPUT_CHANGE_CACHE_DELAY = 300;
|
||||
|
||||
export default function TextBlock({ node }: { node: TreeNodeInterface }) {
|
||||
const blockEditor = getBlockEditor();
|
||||
if (!blockEditor) return null;
|
||||
|
||||
export default function TextBlock({ block }: { block: Block }) {
|
||||
const [editor] = useState(() => withReact(createEditor()));
|
||||
|
||||
const { id } = useContext(BlockContext);
|
||||
|
||||
const debounceUpdateBlockCache = useMemo(
|
||||
() => debounce(blockEditor.renderTree.updateNodeRect, INPUT_CHANGE_CACHE_DELAY),
|
||||
[id, node.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='mb-2'>
|
||||
<Slate
|
||||
editor={editor}
|
||||
onChange={(e) => console.log('===', e, editor.operations)}
|
||||
onChange={(e) => {
|
||||
if (editor.operations[0].type !== 'set_selection') {
|
||||
console.log('=== text op ===', e, editor.operations);
|
||||
// Temporary code, in the future, it is necessary to monitor the OP changes of the document to determine whether the location cache of the block needs to be updated
|
||||
debounceUpdateBlockCache(node.id);
|
||||
}
|
||||
}}
|
||||
value={[
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
type: 'paragraph',
|
||||
children: [{ text: block.data.text }],
|
||||
children: node.data.content,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<HoveringToolbar blockId={block.id} />
|
||||
<HoveringToolbar blockId={node.id} />
|
||||
<Editable
|
||||
onKeyDownCapture={(event) => {
|
||||
switch (event.key) {
|
||||
@ -38,15 +59,25 @@ export default function TextBlock({ block }: { block: Block }) {
|
||||
|
||||
triggerHotkey(event, editor);
|
||||
}}
|
||||
onDOMBeforeInput={(e) => {
|
||||
// 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();
|
||||
}
|
||||
}}
|
||||
renderLeaf={(props) => <Leaf {...props} />}
|
||||
placeholder='Enter some text...'
|
||||
/>
|
||||
</Slate>
|
||||
<div className='pl-[1.5em]'>
|
||||
{block.children?.map((item: Block) => (
|
||||
<BlockComponent key={item.id} block={item} />
|
||||
))}
|
||||
</div>
|
||||
{node.children && node.children.length > 0 ? (
|
||||
<div className='pl-[1.5em]'>
|
||||
{node.children.map((item) => (
|
||||
<BlockComponent key={item.id} node={item} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,51 +1,64 @@
|
||||
import { Descendant } from "slate";
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
export enum BlockType {
|
||||
PageBlock = 0,
|
||||
HeadingBlock = 1,
|
||||
ListBlock = 2,
|
||||
TextBlock = 3,
|
||||
CodeBlock = 4,
|
||||
EmbedBlock = 5,
|
||||
QuoteBlock = 6,
|
||||
DividerBlock = 7,
|
||||
MediaBlock = 8,
|
||||
TableBlock = 9,
|
||||
PageBlock = 'page',
|
||||
HeadingBlock = 'heading',
|
||||
ListBlock = 'list',
|
||||
TextBlock = 'text',
|
||||
CodeBlock = 'code',
|
||||
EmbedBlock = 'embed',
|
||||
QuoteBlock = 'quote',
|
||||
DividerBlock = 'divider',
|
||||
MediaBlock = 'media',
|
||||
TableBlock = 'table',
|
||||
ColumnBlock = 'column'
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type BlockData<T> = T extends BlockType.TextBlock ? TextBlockData :
|
||||
T extends BlockType.PageBlock ? PageBlockData :
|
||||
T extends BlockType.HeadingBlock ? HeadingBlockData:
|
||||
T extends BlockType.ListBlock ? ListBlockData : any;
|
||||
T extends BlockType.HeadingBlock ? HeadingBlockData :
|
||||
T extends BlockType.ListBlock ? ListBlockData :
|
||||
T extends BlockType.ColumnBlock ? ColumnBlockData : any;
|
||||
|
||||
export interface Block {
|
||||
|
||||
export interface BlockInterface<T = BlockType> {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
data: BlockData<BlockType>;
|
||||
parent: string | null;
|
||||
prev: string | null;
|
||||
data: BlockData<T>;
|
||||
next: string | null;
|
||||
firstChild: string | null;
|
||||
lastChild: string | null;
|
||||
children?: Block[];
|
||||
}
|
||||
|
||||
|
||||
interface TextBlockData {
|
||||
text: string;
|
||||
attr: string;
|
||||
content: Descendant[];
|
||||
}
|
||||
|
||||
interface PageBlockData {
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface ListBlockData {
|
||||
type: 'ul' | 'ol';
|
||||
interface ListBlockData extends TextBlockData {
|
||||
type: 'numbered' | 'bulleted' | 'column';
|
||||
}
|
||||
|
||||
interface HeadingBlockData {
|
||||
interface HeadingBlockData extends TextBlockData {
|
||||
level: number;
|
||||
}
|
||||
|
||||
interface ColumnBlockData {
|
||||
ratio: string;
|
||||
}
|
||||
|
||||
|
||||
export interface TreeNodeInterface {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
parent: TreeNodeInterface | null;
|
||||
children: TreeNodeInterface[];
|
||||
data: BlockData<BlockType>;
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
|
||||
import { createContext } from 'react';
|
||||
import { Block, BlockType } from '../interfaces';
|
||||
|
||||
export const BlockContext = createContext<{
|
||||
id?: string;
|
||||
blocksMap?: Record<string, Block>;
|
||||
} | null>(null);
|
||||
}>({});
|
||||
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
import {
|
||||
Editor,
|
||||
Transforms,
|
||||
Text,
|
||||
Node
|
||||
} from 'slate';
|
||||
|
||||
export function toggleFormat(editor: Editor, format: string) {
|
||||
const isActive = isFormatActive(editor, format)
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ [format]: isActive ? null : true },
|
||||
{ match: Text.isText, split: true }
|
||||
)
|
||||
}
|
||||
|
||||
export const isFormatActive = (editor: Editor, format: string) => {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
match: (n: Node) => n[format] === true,
|
||||
mode: 'all',
|
||||
})
|
||||
return !!match
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
import { toggleFormat } from './format';
|
||||
import { Editor } from 'slate';
|
||||
|
||||
const HOTKEYS: Record<string, string> = {
|
||||
'mod+b': 'bold',
|
||||
'mod+i': 'italic',
|
||||
'mod+u': 'underline',
|
||||
'mod+e': 'code',
|
||||
'mod+shift+X': 'strikethrough',
|
||||
'mod+shift+S': 'strikethrough',
|
||||
};
|
||||
|
||||
export function triggerHotkey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
||||
for (const hotkey in HOTKEYS) {
|
||||
if (isHotkey(hotkey, event)) {
|
||||
event.preventDefault()
|
||||
const format = HOTKEYS[hotkey]
|
||||
toggleFormat(editor, format)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { getBlockEditor } from '@/appflowy_app/block_editor';
|
||||
import { Editor, Range } from 'slate';
|
||||
export function calcToolbarPosition(editor: Editor, toolbarDom: HTMLDivElement, blockId: string) {
|
||||
const { selection } = editor;
|
||||
|
||||
const scrollContainer = document.querySelector('.doc-scroller-container');
|
||||
if (!scrollContainer) return;
|
||||
|
||||
if (!selection || Range.isCollapsed(selection) || Editor.string(editor, selection) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const blockEditor = getBlockEditor();
|
||||
const blockRect = blockEditor?.renderTree.getNodeRect(blockId);
|
||||
const blockDom = document.querySelector(`[data-block-id=${blockId}]`);
|
||||
|
||||
if (!blockDom || !blockRect) return;
|
||||
|
||||
const domSelection = window.getSelection();
|
||||
let domRange;
|
||||
if (domSelection?.rangeCount === 0) {
|
||||
return;
|
||||
} else {
|
||||
domRange = domSelection?.getRangeAt(0);
|
||||
}
|
||||
|
||||
const rect = domRange?.getBoundingClientRect() || { top: 0, left: 0, width: 0, height: 0 };
|
||||
|
||||
const top = `${-toolbarDom.offsetHeight - 5 + (rect.top + scrollContainer.scrollTop - blockRect.top)}px`;
|
||||
const left = `${rect.left - blockRect.left - toolbarDom.offsetWidth / 2 + rect.width / 2}px`;
|
||||
|
||||
return {
|
||||
top,
|
||||
left,
|
||||
}
|
||||
|
||||
}
|
10
frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts
Normal file
10
frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export function debounce(fn: (...args: any[]) => void, delay: number) {
|
||||
let timeout: NodeJS.Timeout;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(()=>{
|
||||
// eslint-disable-next-line prefer-spread
|
||||
fn.apply(undefined, args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import { Block } from "../interfaces";
|
||||
|
||||
export function buildTree(id: string, blocksMap: Record<string, Block>) {
|
||||
const head = blocksMap[id];
|
||||
let node: Block | null = head;
|
||||
while(node) {
|
||||
|
||||
if (node.parent) {
|
||||
const parent = blocksMap[node.parent];
|
||||
!parent.children && (parent.children = []);
|
||||
parent.children.push(node);
|
||||
}
|
||||
|
||||
if (node.firstChild) {
|
||||
node = blocksMap[node.firstChild];
|
||||
} else if (node.next) {
|
||||
node = blocksMap[node.next];
|
||||
} else {
|
||||
while(node && node?.parent) {
|
||||
const parent: Block | null = blocksMap[node.parent];
|
||||
if (parent?.next) {
|
||||
node = blocksMap[parent.next];
|
||||
break;
|
||||
} else {
|
||||
node = parent;
|
||||
}
|
||||
}
|
||||
if (node.id === head.id) {
|
||||
node = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return head;
|
||||
}
|
@ -4,12 +4,209 @@ import {
|
||||
DocumentVersionPB,
|
||||
OpenDocumentPayloadPB,
|
||||
} from '../../services/backend/events/flowy-document';
|
||||
import { Block, BlockType } from '../interfaces';
|
||||
import { BlockInterface, BlockType } from '../interfaces';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { getBlockEditor, createBlockEditor } from '../block_editor';
|
||||
|
||||
const loadBlockData = async (id: string): Promise<Record<string, BlockInterface>> => {
|
||||
return {
|
||||
[id]: {
|
||||
id: id,
|
||||
type: BlockType.PageBlock,
|
||||
data: { title: 'Document Title' },
|
||||
next: null,
|
||||
firstChild: "L1-1",
|
||||
},
|
||||
"L1-1": {
|
||||
id: "L1-1",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 1, content: [{ text: 'Heading 1' }] },
|
||||
next: "L1-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-2": {
|
||||
id: "L1-2",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 2, content: [{ text: 'Heading 2' }] },
|
||||
next: "L1-3",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-3": {
|
||||
id: "L1-3",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 3, content: [{ text: 'Heading 3' }] },
|
||||
next: "L1-4",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-4": {
|
||||
id: "L1-4",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{
|
||||
text:
|
||||
'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{ text: ', ' },
|
||||
{ text: 'italic', italic: true },
|
||||
{ text: ', or anything else you might want to do!' },
|
||||
] },
|
||||
next: "L1-5",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-5": {
|
||||
id: "L1-5",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
{ text: 'select any piece of text and the menu will appear', bold: true },
|
||||
{ text: '.' },
|
||||
] },
|
||||
next: "L1-6",
|
||||
firstChild: "L1-5-1",
|
||||
},
|
||||
"L1-5-1": {
|
||||
id: "L1-5-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L1-5-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-5-2": {
|
||||
id: "L1-5-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-6": {
|
||||
id: "L1-6",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'bulleted', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{
|
||||
text:
|
||||
', or add a semantically rendered block quote in the middle of the page, like this:',
|
||||
},
|
||||
] },
|
||||
next: "L1-7",
|
||||
firstChild: "L1-6-1",
|
||||
},
|
||||
"L1-6-1": {
|
||||
id: "L1-6-1",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'numbered', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
|
||||
] },
|
||||
|
||||
next: "L1-6-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-6-2": {
|
||||
id: "L1-6-2",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'numbered', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
|
||||
] },
|
||||
|
||||
next: "L1-6-3",
|
||||
firstChild: null,
|
||||
},
|
||||
|
||||
"L1-6-3": {
|
||||
id: "L1-6-3",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [{ text: 'A wise quote.' }] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
|
||||
"L1-7": {
|
||||
id: "L1-7",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'column' },
|
||||
|
||||
next: "L1-8",
|
||||
firstChild: "L1-7-1",
|
||||
},
|
||||
"L1-7-1": {
|
||||
id: "L1-7-1",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: "L1-7-2",
|
||||
firstChild: "L1-7-1-1",
|
||||
},
|
||||
"L1-7-1-1": {
|
||||
id: "L1-7-1-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-7-2": {
|
||||
id: "L1-7-2",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: "L1-7-3",
|
||||
firstChild: "L1-7-2-1",
|
||||
},
|
||||
"L1-7-2-1": {
|
||||
id: "L1-7-2-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L1-7-2-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-7-2-2": {
|
||||
id: "L1-7-2-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-7-3": {
|
||||
id: "L1-7-3",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: null,
|
||||
firstChild: "L1-7-3-1",
|
||||
},
|
||||
"L1-7-3-1": {
|
||||
id: "L1-7-3-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const useDocument = () => {
|
||||
const params = useParams();
|
||||
const [blocksMap, setBlocksMap] = useState<Record<string, Block>>();
|
||||
const [blockId, setBlockId] = useState<string>();
|
||||
const loadDocument = async (id: string): Promise<any> => {
|
||||
const getDocumentResult = await DocumentEventGetDocument(
|
||||
OpenDocumentPayloadPB.fromObject({
|
||||
@ -26,148 +223,24 @@ export const useDocument = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadBlockData = async (blockId: string): Promise<Record<string, Block>> => {
|
||||
return {
|
||||
[blockId]: {
|
||||
id: blockId,
|
||||
type: BlockType.PageBlock,
|
||||
data: { title: 'Document Title' },
|
||||
parent: null,
|
||||
next: null,
|
||||
prev: null,
|
||||
firstChild: "A",
|
||||
lastChild: "E"
|
||||
},
|
||||
"A": {
|
||||
id: "A",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 1, text: 'A Heading-1' },
|
||||
parent: blockId,
|
||||
prev: null,
|
||||
next: "B",
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"B": {
|
||||
id: "B",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'Hello', attr: '' },
|
||||
parent: blockId,
|
||||
prev: "A",
|
||||
next: "C",
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"C": {
|
||||
id: "C",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'block c' },
|
||||
prev: null,
|
||||
parent: blockId,
|
||||
next: "D",
|
||||
firstChild: "F",
|
||||
lastChild: null,
|
||||
},
|
||||
"D": {
|
||||
id: "D",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'number_list', text: 'D List' },
|
||||
prev: "C",
|
||||
parent: blockId,
|
||||
next: null,
|
||||
firstChild: "G",
|
||||
lastChild: "H",
|
||||
},
|
||||
"E": {
|
||||
id: "E",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'World', attr: '' },
|
||||
prev: "D",
|
||||
parent: blockId,
|
||||
next: null,
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"F": {
|
||||
id: "F",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'Heading', attr: '' },
|
||||
prev: null,
|
||||
parent: "C",
|
||||
next: null,
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"G": {
|
||||
id: "G",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'Item 1', attr: '' },
|
||||
prev: null,
|
||||
parent: "D",
|
||||
next: "H",
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"H": {
|
||||
id: "H",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: 'Item 2', attr: '' },
|
||||
prev: "G",
|
||||
parent: "D",
|
||||
next: "I",
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"I": {
|
||||
id: "I",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 2, text: 'B Heading-1' },
|
||||
parent: blockId,
|
||||
prev: "H",
|
||||
next: 'L',
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"L": {
|
||||
id: "L",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: '456' },
|
||||
parent: blockId,
|
||||
prev: "I",
|
||||
next: 'J',
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"J": {
|
||||
id: "J",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 3, text: 'C Heading-1' },
|
||||
parent: blockId,
|
||||
prev: "L",
|
||||
next: "K",
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
"K": {
|
||||
id: "K",
|
||||
type: BlockType.TextBlock,
|
||||
data: { text: '123' },
|
||||
parent: blockId,
|
||||
prev: "J",
|
||||
next: null,
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
if (!params?.id) return;
|
||||
const data = await loadBlockData(params.id);
|
||||
console.log(data);
|
||||
setBlocksMap(data);
|
||||
console.log('==== enter ====', params?.id, data);
|
||||
|
||||
const blockEditor = getBlockEditor();
|
||||
if (blockEditor) {
|
||||
blockEditor.changeDoc(params?.id, data);
|
||||
} else {
|
||||
createBlockEditor(params?.id, data);
|
||||
}
|
||||
|
||||
setBlockId(params.id)
|
||||
})();
|
||||
}, [params]);
|
||||
return { blocksMap, blockId: params.id };
|
||||
return () => {
|
||||
console.log('==== leave ====', params?.id)
|
||||
}
|
||||
}, [params.id]);
|
||||
return { blockId };
|
||||
};
|
||||
|
@ -9,18 +9,18 @@ const theme = createTheme({
|
||||
},
|
||||
});
|
||||
export const DocumentPage = () => {
|
||||
const { blocksMap, blockId } = useDocument();
|
||||
const { blockId } = useDocument();
|
||||
|
||||
if (!blockId) return <div className='error-page'></div>;
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<div id='appflowy-block-doc' className='flex flex-col items-center'>
|
||||
<div id='appflowy-block-doc' className='doc-scroller-container flex h-[100%] flex-col items-center overflow-auto'>
|
||||
<BlockContext.Provider
|
||||
value={{
|
||||
id: blockId,
|
||||
blocksMap,
|
||||
}}
|
||||
>
|
||||
<BlockList />
|
||||
<BlockList blockId={blockId} />
|
||||
</BlockContext.Provider>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
|
Loading…
Reference in New Issue
Block a user