mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #2258 from qinluhe/feat/refactor-tauri-document
Feat/refactor tauri document
This commit is contained in:
commit
0e5a03a282
File diff suppressed because it is too large
Load Diff
1203
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
1203
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -72,7 +72,7 @@ export function useBlockSelection({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const calcIntersectBlocks = useCallback(
|
||||
const updateSelctionsByPoint = useCallback(
|
||||
(clientX: number, clientY: number) => {
|
||||
if (!isDragging) return;
|
||||
const [startX, startY] = pointRef.current;
|
||||
@ -86,7 +86,7 @@ export function useBlockSelection({
|
||||
endY,
|
||||
});
|
||||
disaptch(
|
||||
documentActions.changeSelectionByIntersectRect({
|
||||
documentActions.setSelectionByRect({
|
||||
startX: Math.min(startX, endX),
|
||||
startY: Math.min(startY, endY),
|
||||
endX: Math.max(startX, endX),
|
||||
@ -102,7 +102,7 @@ export function useBlockSelection({
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
calcIntersectBlocks(e.clientX, e.clientY);
|
||||
updateSelctionsByPoint(e.clientX, e.clientY);
|
||||
|
||||
const { top, bottom } = container.getBoundingClientRect();
|
||||
if (e.clientY >= bottom) {
|
||||
@ -124,7 +124,7 @@ export function useBlockSelection({
|
||||
}
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
calcIntersectBlocks(e.clientX, e.clientY);
|
||||
updateSelctionsByPoint(e.clientX, e.clientY);
|
||||
setDragging(false);
|
||||
setRect(null);
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BlockType } from '@/appflowy_app/interfaces/document';
|
||||
import { BlockType, HeadingBlockData } 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';
|
||||
@ -43,9 +43,10 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
|
||||
el.style.zIndex = '1';
|
||||
el.style.top = '1px';
|
||||
if (node?.type === BlockType.HeadingBlock) {
|
||||
if (node.data.style?.level === 1) {
|
||||
const nodeData = node.data as HeadingBlockData;
|
||||
if (nodeData.level === 1) {
|
||||
el.style.top = '8px';
|
||||
} else if (node.data.style?.level === 2) {
|
||||
} else if (nodeData.level === 2) {
|
||||
el.style.top = '6px';
|
||||
} else {
|
||||
el.style.top = '5px';
|
||||
@ -80,16 +81,7 @@ function useController() {
|
||||
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 {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
|
||||
export function useDocumentTitle(id: string) {
|
||||
const { node, delta } = useSubscribeNode(id);
|
||||
const { node } = useSubscribeNode(id);
|
||||
return {
|
||||
node,
|
||||
delta
|
||||
}
|
||||
};
|
||||
}
|
@ -3,11 +3,18 @@ import { useDocumentTitle } from './DocumentTitle.hooks';
|
||||
import TextBlock from '../TextBlock';
|
||||
|
||||
export default function DocumentTitle({ id }: { id: string }) {
|
||||
const { node, delta } = useDocumentTitle(id);
|
||||
const { node } = useDocumentTitle(id);
|
||||
if (!node) return null;
|
||||
return (
|
||||
<div data-block-id={node.id} className='doc-title relative pt-[50px] text-4xl font-bold'>
|
||||
<TextBlock placeholder='Untitled' childIds={[]} delta={delta || []} node={node} />
|
||||
<TextBlock placeholder='Untitled' childIds={[]} node={{
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
delta: node.data.delta || [],
|
||||
}
|
||||
}} />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ const fontSize: Record<string, string> = {
|
||||
export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) {
|
||||
return (
|
||||
<div className={`${fontSize[node.data.style?.level]} font-semibold `}>
|
||||
<TextBlock node={node} childIds={[]} delta={delta} />
|
||||
{/*<TextBlock node={node} childIds={[]} delta={delta} />*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ export default function ListBlock({ node, delta }: { node: Node; delta: TextDelt
|
||||
if (node.data.style?.type === 'column') return <></>;
|
||||
return (
|
||||
<div className='flex-1'>
|
||||
<TextBlock delta={delta} node={node} childIds={[]} />
|
||||
{/*<TextBlock delta={delta} node={node} childIds={[]} />*/}
|
||||
</div>
|
||||
);
|
||||
}, [node, delta]);
|
||||
|
@ -1,11 +1,10 @@
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { documentActions } from '$app/stores/reducers/document/slice';
|
||||
|
||||
export function useNode(id: string) {
|
||||
const { node, childIds, delta, isSelected } = useSubscribeNode(id);
|
||||
const { node, childIds, isSelected } = useSubscribeNode(id);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@ -15,22 +14,23 @@ export function useNode(id: string) {
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
|
||||
const scrollContainer = document.querySelector('.doc-scroller-container') as HTMLDivElement;
|
||||
dispatch(documentActions.updateNodePosition({
|
||||
id,
|
||||
rect: {
|
||||
x: rect.x,
|
||||
y: rect.y + scrollContainer.scrollTop,
|
||||
height: rect.height,
|
||||
width: rect.width
|
||||
}
|
||||
}))
|
||||
}, [])
|
||||
dispatch(
|
||||
documentActions.updateNodePosition({
|
||||
id,
|
||||
rect: {
|
||||
x: rect.x,
|
||||
y: rect.y + scrollContainer.scrollTop,
|
||||
height: rect.height,
|
||||
width: rect.width,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
ref,
|
||||
node,
|
||||
childIds,
|
||||
delta,
|
||||
isSelected
|
||||
}
|
||||
isSelected,
|
||||
};
|
||||
}
|
@ -7,14 +7,26 @@ import TextBlock from '../TextBlock';
|
||||
import { TextDelta } from '@/appflowy_app/interfaces/document';
|
||||
|
||||
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const { node, childIds, delta, isSelected, ref } = useNode(id);
|
||||
const { node, childIds, isSelected, ref } = useNode(id);
|
||||
|
||||
console.log('=====', id);
|
||||
const renderBlock = useCallback((_props: { node: Node; childIds?: string[]; delta?: TextDelta[] }) => {
|
||||
const renderBlock = useCallback((_props: { node: Node; childIds?: string[] }) => {
|
||||
switch (_props.node.type) {
|
||||
case 'text':
|
||||
if (!_props.delta) return null;
|
||||
return <TextBlock {..._props} delta={_props.delta} />;
|
||||
case 'text': {
|
||||
const delta = _props.node.data.delta;
|
||||
if (!delta) return null;
|
||||
return (
|
||||
<TextBlock
|
||||
node={{
|
||||
..._props.node,
|
||||
data: {
|
||||
delta,
|
||||
},
|
||||
}}
|
||||
childIds={childIds}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -27,7 +39,6 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
|
||||
{renderBlock({
|
||||
node,
|
||||
childIds,
|
||||
delta,
|
||||
})}
|
||||
<div className='block-overlay' />
|
||||
{isSelected ? <div className='pointer-events-none absolute inset-0 z-[-1] rounded-[4px] bg-[#E0F8FF]' /> : null}
|
||||
|
@ -5,14 +5,13 @@ import { documentActions } from '$app/stores/reducers/document/slice';
|
||||
|
||||
export function useParseTree(documentData: DocumentData) {
|
||||
const dispatch = useAppDispatch();
|
||||
const { blocks, ytexts, yarrays } = documentData;
|
||||
const { blocks, meta } = documentData;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
documentActions.createTree({
|
||||
documentActions.create({
|
||||
nodes: blocks,
|
||||
delta: ytexts,
|
||||
children: yarrays,
|
||||
children: meta.childrenMap,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { useRoot } from './Root.hooks';
|
||||
import Node from '../Node';
|
||||
import { withErrorBoundary } from 'react-error-boundary';
|
||||
import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
|
||||
import VirtualizerList from '../VirtualizerList';
|
||||
import VirtualizedList from '../VirtualizedList';
|
||||
import { Skeleton } from '@mui/material';
|
||||
|
||||
function Root({ documentData }: { documentData: DocumentData }) {
|
||||
@ -20,7 +20,7 @@ function Root({ documentData }: { documentData: DocumentData }) {
|
||||
|
||||
return (
|
||||
<div id='appflowy-block-doc' className='h-[100%] overflow-hidden'>
|
||||
<VirtualizerList node={node} childIds={childIds} renderNode={renderNode} />
|
||||
<VirtualizedList node={node} childIds={childIds} renderNode={renderNode} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,61 +0,0 @@
|
||||
|
||||
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { createEditor } from "slate";
|
||||
import { withReact } from "slate-react";
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
|
||||
import { Delta } from '@slate-yjs/core/dist/model/types';
|
||||
import { TextDelta } from '@/appflowy_app/interfaces/document';
|
||||
|
||||
const initialValue = [{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }],
|
||||
}];
|
||||
|
||||
export function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
|
||||
const yTextRef = useRef<Y.XmlText>();
|
||||
// Create a yjs document and get the shared type
|
||||
const sharedType = useMemo(() => {
|
||||
const ydoc = new Y.Doc()
|
||||
const _sharedType = ydoc.get('content', Y.XmlText) as Y.XmlText;
|
||||
|
||||
const insertDelta = slateNodesToInsertDelta(initialValue);
|
||||
// Load the initial value into the yjs document
|
||||
_sharedType.applyDelta(insertDelta);
|
||||
|
||||
const yText = insertDelta[0].insert as Y.XmlText;
|
||||
yTextRef.current = yText;
|
||||
|
||||
return _sharedType;
|
||||
}, []);
|
||||
|
||||
const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []);
|
||||
|
||||
useEffect(() => {
|
||||
YjsEditor.connect(editor);
|
||||
return () => {
|
||||
yTextRef.current = undefined;
|
||||
YjsEditor.disconnect(editor);
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
const yText = yTextRef.current;
|
||||
if (!yText) return;
|
||||
|
||||
const textEventHandler = (event: Y.YTextEvent) => {
|
||||
update(event.changes.delta as TextDelta[]);
|
||||
}
|
||||
yText.applyDelta(delta);
|
||||
yText.observe(textEventHandler);
|
||||
|
||||
return () => {
|
||||
yText.unobserve(textEventHandler);
|
||||
}
|
||||
}, [delta])
|
||||
|
||||
|
||||
return { editor }
|
||||
}
|
@ -1,71 +1,18 @@
|
||||
import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey";
|
||||
import { useCallback, useContext, useMemo, useRef, useState } from "react";
|
||||
import { Descendant, Range } from "slate";
|
||||
import { useBindYjs } from "./BindYjs.hooks";
|
||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||
import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Descendant, Range } from 'slate';
|
||||
import { TextDelta } from '$app/interfaces/document';
|
||||
import { debounce } from "@/appflowy_app/utils/tool";
|
||||
import { useTextInput } from '../_shared/TextInput.hooks';
|
||||
|
||||
function useController(textId: string) {
|
||||
const docController = useContext(DocumentControllerContext);
|
||||
|
||||
const update = useCallback(
|
||||
(delta: TextDelta[]) => {
|
||||
docController?.yTextApply(textId, delta)
|
||||
},
|
||||
[textId],
|
||||
);
|
||||
const transact = useCallback(
|
||||
(actions: (() => void)[]) => {
|
||||
docController?.transact(actions)
|
||||
},
|
||||
[textId],
|
||||
)
|
||||
|
||||
return {
|
||||
update,
|
||||
transact
|
||||
}
|
||||
}
|
||||
|
||||
function useTransact(textId: string) {
|
||||
const pendingActions = useRef<(() => void)[]>([]);
|
||||
const { update, transact } = useController(textId);
|
||||
|
||||
const sendTransact = useCallback(
|
||||
() => {
|
||||
const actions = pendingActions.current;
|
||||
transact(actions);
|
||||
},
|
||||
[transact],
|
||||
)
|
||||
|
||||
const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]);
|
||||
|
||||
const sendDelta = useCallback(
|
||||
(delta: TextDelta[]) => {
|
||||
const action = () => update(delta);
|
||||
pendingActions.current.push(action);
|
||||
debounceSendTransact()
|
||||
},
|
||||
[update, debounceSendTransact],
|
||||
);
|
||||
return {
|
||||
sendDelta
|
||||
}
|
||||
}
|
||||
|
||||
export function useTextBlock(text: string, delta: TextDelta[]) {
|
||||
const { sendDelta } = useTransact(text);
|
||||
|
||||
const { editor } = useBindYjs(delta, sendDelta);
|
||||
export function useTextBlock(delta: TextDelta[]) {
|
||||
const { editor } = useTextInput(delta);
|
||||
const [value, setValue] = useState<Descendant[]>([]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: Descendant[]) => {
|
||||
setValue(e);
|
||||
},
|
||||
[editor],
|
||||
[editor]
|
||||
);
|
||||
|
||||
const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
@ -73,14 +20,13 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
|
||||
case 'Enter': {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
return;
|
||||
}
|
||||
case 'Backspace': {
|
||||
if (!editor.selection) return;
|
||||
const { anchor } = editor.selection;
|
||||
const isCollapase = Range.isCollapsed(editor.selection);
|
||||
if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') {
|
||||
const isCollapsed = Range.isCollapsed(editor.selection);
|
||||
if (isCollapsed && anchor.offset === 0 && anchor.path.toString() === '0,0') {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
@ -88,16 +34,15 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
|
||||
}
|
||||
}
|
||||
triggerHotkey(event, editor);
|
||||
}
|
||||
};
|
||||
|
||||
const onDOMBeforeInput = useCallback((e: InputEvent) => {
|
||||
// COMPAT: in Apple, `compositionend` is dispatched after the
|
||||
// `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese.
|
||||
// Here, prevent the beforeInput event and wait for the compositionend event to take effect
|
||||
// COMPAT: in Apple, `compositionend` is dispatched after the `beforeinput` for "insertFromComposition".
|
||||
// It will cause repeated characters when inputting Chinese.
|
||||
// Here, prevent the beforeInput event and wait for the compositionend event to take effect.
|
||||
if (e.inputType === 'insertFromComposition') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
}, []);
|
||||
|
||||
return {
|
||||
@ -105,6 +50,6 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
|
||||
onKeyDownCapture,
|
||||
onDOMBeforeInput,
|
||||
editor,
|
||||
value
|
||||
}
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
@ -3,23 +3,21 @@ import Leaf from './Leaf';
|
||||
import { useTextBlock } from './TextBlock.hooks';
|
||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||
import NodeComponent from '../Node';
|
||||
import HoveringToolbar from '../HoveringToolbar';
|
||||
import { TextDelta } from '@/appflowy_app/interfaces/document';
|
||||
import HoveringToolbar from '../_shared/HoveringToolbar';
|
||||
import React from 'react';
|
||||
import { TextDelta } from '@/appflowy_app/interfaces/document';
|
||||
|
||||
function TextBlock({
|
||||
node,
|
||||
childIds,
|
||||
placeholder,
|
||||
delta,
|
||||
...props
|
||||
}: {
|
||||
node: Node;
|
||||
delta: TextDelta[];
|
||||
node: Node & { data: { delta: TextDelta[] } };
|
||||
childIds?: string[];
|
||||
placeholder?: string;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, delta);
|
||||
const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.delta);
|
||||
|
||||
return (
|
||||
<div {...props} className={`py-[2px] ${props.className}`}>
|
||||
|
@ -3,10 +3,10 @@ import { useRef } from 'react';
|
||||
|
||||
const defaultSize = 60;
|
||||
|
||||
export function useVirtualizerList(count: number) {
|
||||
export function useVirtualizedList(count: number) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
const virtualize = useVirtualizer({
|
||||
count,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => {
|
||||
@ -15,7 +15,7 @@ export function useVirtualizerList(count: number) {
|
||||
});
|
||||
|
||||
return {
|
||||
rowVirtualizer,
|
||||
virtualize,
|
||||
parentRef,
|
||||
};
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useVirtualizerList } from './VirtualizerList.hooks';
|
||||
import { useVirtualizedList } from './VirtualizedList.hooks';
|
||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||
import DocumentTitle from '../DocumentTitle';
|
||||
import Overlay from '../Overlay';
|
||||
|
||||
export default function VirtualizerList({
|
||||
export default function VirtualizedList({
|
||||
childIds,
|
||||
node,
|
||||
renderNode,
|
||||
@ -13,9 +13,8 @@ export default function VirtualizerList({
|
||||
node: Node;
|
||||
renderNode: (nodeId: string) => JSX.Element;
|
||||
}) {
|
||||
const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length);
|
||||
|
||||
const virtualItems = rowVirtualizer.getVirtualItems();
|
||||
const { virtualize, parentRef } = useVirtualizedList(childIds.length);
|
||||
const virtualItems = virtualize.getVirtualItems();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -26,7 +25,7 @@ export default function VirtualizerList({
|
||||
<div
|
||||
className='doc-body max-w-screen w-[900px] min-w-0'
|
||||
style={{
|
||||
height: rowVirtualizer.getTotalSize(),
|
||||
height: virtualize.getTotalSize(),
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
@ -43,7 +42,7 @@ export default function VirtualizerList({
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const id = childIds[virtualRow.index];
|
||||
return (
|
||||
<div className='p-[1px]' key={id} data-index={virtualRow.index} ref={rowVirtualizer.measureElement}>
|
||||
<div className='p-[1px]' key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
|
||||
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
|
||||
{renderNode(id)}
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format';
|
||||
import { toggleFormat, isFormatActive } from '$app/utils/slate/format';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useFocused, useSlate } from 'slate-react';
|
||||
import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar';
|
||||
|
||||
|
||||
import { calcToolbarPosition } from '$app/utils/slate/toolbar';
|
||||
export function useHoveringToolbar(id: string) {
|
||||
const editor = useSlate();
|
||||
const inFocus = useFocused();
|
||||
@ -29,6 +27,6 @@ export function useHoveringToolbar(id: string) {
|
||||
return {
|
||||
ref,
|
||||
inFocus,
|
||||
editor
|
||||
}
|
||||
editor,
|
||||
};
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import FormatButton from './FormatButton';
|
||||
import Portal from '../BlockPortal';
|
||||
import Portal from '../../BlockPortal';
|
||||
import { useHoveringToolbar } from './index.hooks';
|
||||
|
||||
const HoveringToolbar = ({ id }: { id: string }) => {
|
@ -1,32 +1,38 @@
|
||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||
import { useAppSelector } from '@/appflowy_app/stores/store';
|
||||
import { useMemo } from 'react';
|
||||
import { TextDelta } from '@/appflowy_app/interfaces/document';
|
||||
|
||||
/**
|
||||
* Subscribe to a node and its children
|
||||
* It will be change when the node or its children is changed
|
||||
* And it will not be change when other node is changed
|
||||
* @param id
|
||||
*/
|
||||
export function useSubscribeNode(id: string) {
|
||||
const node = useAppSelector<Node>(state => state.document.nodes[id]);
|
||||
const childIds = useAppSelector<string[] | undefined>(state => {
|
||||
const node = useAppSelector<Node>((state) => state.document.nodes[id]);
|
||||
|
||||
const childIds = useAppSelector<string[] | undefined>((state) => {
|
||||
const childrenId = state.document.nodes[id]?.children;
|
||||
if (!childrenId) return;
|
||||
return state.document.children[childrenId];
|
||||
});
|
||||
const delta = useAppSelector<TextDelta[] | undefined>(state => {
|
||||
const deltaId = state.document.nodes[id]?.data?.text;
|
||||
if (!deltaId) return;
|
||||
return state.document.delta[deltaId];
|
||||
});
|
||||
const isSelected = useAppSelector<boolean>(state => {
|
||||
|
||||
const isSelected = useAppSelector<boolean>((state) => {
|
||||
return state.document.selections?.includes(id) || false;
|
||||
});
|
||||
|
||||
const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]);
|
||||
// Memoize the node and its children
|
||||
// So that the component will not be re-rendered when other node is changed
|
||||
// It very important for performance
|
||||
const memoizedNode = useMemo(
|
||||
() => node,
|
||||
[node?.id, JSON.stringify(node?.data), node?.parent, node?.type, node?.children]
|
||||
);
|
||||
const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]);
|
||||
const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]);
|
||||
|
||||
return {
|
||||
node: memoizedNode,
|
||||
childIds: memoizedChildIds,
|
||||
delta: memoizedDelta,
|
||||
isSelected
|
||||
isSelected,
|
||||
};
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
import { useCallback, useContext, useMemo, useRef, useEffect } from 'react';
|
||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||
import { TextDelta } from '$app/interfaces/document';
|
||||
import { debounce } from '@/appflowy_app/utils/tool';
|
||||
import { createEditor } from 'slate';
|
||||
import { withReact } from 'slate-react';
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
|
||||
|
||||
export function useTextInput(delta: TextDelta[]) {
|
||||
const { sendDelta } = useTransact();
|
||||
const { editor } = useBindYjs(delta, sendDelta);
|
||||
|
||||
return {
|
||||
editor,
|
||||
};
|
||||
}
|
||||
|
||||
function useController() {
|
||||
const docController = useContext(DocumentControllerContext);
|
||||
|
||||
const update = useCallback(
|
||||
(delta: TextDelta[]) => {
|
||||
docController?.applyActions([
|
||||
{
|
||||
type: 'update',
|
||||
payload: {
|
||||
block: {
|
||||
data: {
|
||||
delta,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
[docController]
|
||||
);
|
||||
|
||||
return {
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
function useTransact() {
|
||||
const { update } = useController();
|
||||
|
||||
const sendDelta = useCallback(
|
||||
(delta: TextDelta[]) => {
|
||||
update(delta);
|
||||
},
|
||||
[update]
|
||||
);
|
||||
const debounceSendDelta = useMemo(() => debounce(sendDelta, 300), [sendDelta]);
|
||||
|
||||
return {
|
||||
sendDelta: debounceSendDelta,
|
||||
};
|
||||
}
|
||||
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
];
|
||||
|
||||
function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
|
||||
const yTextRef = useRef<Y.XmlText>();
|
||||
// Create a yjs document and get the shared type
|
||||
const sharedType = useMemo(() => {
|
||||
const doc = new Y.Doc();
|
||||
const _sharedType = doc.get('content', Y.XmlText) as Y.XmlText;
|
||||
|
||||
const insertDelta = slateNodesToInsertDelta(initialValue);
|
||||
// Load the initial value into the yjs document
|
||||
_sharedType.applyDelta(insertDelta);
|
||||
|
||||
const yText = insertDelta[0].insert as Y.XmlText;
|
||||
yTextRef.current = yText;
|
||||
|
||||
return _sharedType;
|
||||
}, []);
|
||||
|
||||
const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []);
|
||||
|
||||
useEffect(() => {
|
||||
YjsEditor.connect(editor);
|
||||
return () => {
|
||||
yTextRef.current = undefined;
|
||||
YjsEditor.disconnect(editor);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
const yText = yTextRef.current;
|
||||
if (!yText) return;
|
||||
|
||||
const textEventHandler = (event: Y.YTextEvent) => {
|
||||
const textDelta = event.target.toDelta();
|
||||
console.log('delta', textDelta);
|
||||
update(textDelta);
|
||||
};
|
||||
yText.applyDelta(delta);
|
||||
yText.observe(textEventHandler);
|
||||
|
||||
return () => {
|
||||
yText.unobserve(textEventHandler);
|
||||
};
|
||||
}, [delta]);
|
||||
|
||||
return { editor };
|
||||
}
|
@ -8,7 +8,7 @@ async function testCreateDocument() {
|
||||
const document = await svc.open().then((result) => result.unwrap());
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const content = JSON.parse(document.content);
|
||||
// const content = JSON.parse(document.content);
|
||||
// The initial document content:
|
||||
// {
|
||||
// "document": {
|
||||
|
@ -10,8 +10,19 @@ export enum BlockType {
|
||||
DividerBlock = 'divider',
|
||||
MediaBlock = 'media',
|
||||
TableBlock = 'table',
|
||||
ColumnBlock = 'column'
|
||||
ColumnBlock = 'column',
|
||||
}
|
||||
|
||||
export interface HeadingBlockData {
|
||||
level: number;
|
||||
}
|
||||
|
||||
export interface TextBlockData {
|
||||
delta: TextDelta[];
|
||||
}
|
||||
|
||||
export interface PageBlockData extends TextBlockData {}
|
||||
|
||||
export interface NestedBlock {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
@ -26,6 +37,7 @@ export interface TextDelta {
|
||||
export interface DocumentData {
|
||||
rootId: string;
|
||||
blocks: Record<string, NestedBlock>;
|
||||
ytexts: Record<string, TextDelta[]>;
|
||||
yarrays: Record<string, string[]>;
|
||||
meta: {
|
||||
childrenMap: Record<string, string[]>;
|
||||
};
|
||||
}
|
||||
|
@ -24,22 +24,7 @@ import {
|
||||
export class DocumentBackendService {
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
|
||||
const payload = OpenDocumentPayloadPB.fromObject({ document_id: this.viewId, version: DocumentVersionPB.V1 });
|
||||
return DocumentEventGetDocument(payload);
|
||||
};
|
||||
|
||||
applyEdit = (operations: string) => {
|
||||
const payload = EditPayloadPB.fromObject({ doc_id: this.viewId, operations: operations });
|
||||
return DocumentEventApplyEdit(payload);
|
||||
};
|
||||
|
||||
close = () => {
|
||||
const payload = ViewIdPB.fromObject({ value: this.viewId });
|
||||
return FolderEventCloseView(payload);
|
||||
};
|
||||
|
||||
openV2 = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
|
||||
open = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
|
||||
const payload = OpenDocumentPayloadPBV2.fromObject({
|
||||
document_id: this.viewId,
|
||||
});
|
||||
@ -54,7 +39,7 @@ export class DocumentBackendService {
|
||||
return DocumentEvent2ApplyAction(payload);
|
||||
};
|
||||
|
||||
closeV2 = (): Promise<Result<void, FlowyError>> => {
|
||||
close = (): Promise<Result<void, FlowyError>> => {
|
||||
const payload = CloseDocumentPayloadPBV2.fromObject({
|
||||
document_id: this.viewId,
|
||||
});
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document';
|
||||
import { DocumentData, BlockType } from '@/appflowy_app/interfaces/document';
|
||||
import { createContext } from 'react';
|
||||
import { DocumentBackendService } from './document_bd_svc';
|
||||
import { Err } from 'ts-results';
|
||||
import { BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockPB, FlowyError } from '@/services/backend';
|
||||
import { FlowyError } from '@/services/backend';
|
||||
import { DocumentObserver } from './document_observer';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
||||
|
||||
@ -17,7 +15,7 @@ export class DocumentController {
|
||||
this.observer = new DocumentObserver(viewId);
|
||||
}
|
||||
|
||||
open = async (): Promise<DocumentData | null> => {
|
||||
open = async (): Promise<DocumentData | FlowyError> => {
|
||||
// example:
|
||||
await this.observer.subscribe({
|
||||
didReceiveUpdate: () => {
|
||||
@ -25,55 +23,39 @@ export class DocumentController {
|
||||
},
|
||||
});
|
||||
|
||||
const document = await this.backendService.openV2();
|
||||
let root_id = '';
|
||||
const document = await this.backendService.open();
|
||||
if (document.ok) {
|
||||
root_id = document.val.page_id;
|
||||
console.log(document.val.blocks);
|
||||
}
|
||||
await this.backendService.applyActions([
|
||||
BlockActionPB.fromObject({
|
||||
action: BlockActionTypePB.Insert,
|
||||
payload: BlockActionPayloadPB.fromObject({
|
||||
block: BlockPB.fromObject({
|
||||
id: nanoid(10),
|
||||
ty: 'text',
|
||||
parent_id: root_id,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
const openDocumentResult = await this.backendService.open();
|
||||
if (openDocumentResult.ok) {
|
||||
console.log(document.val);
|
||||
const blocks: DocumentData["blocks"] = {};
|
||||
document.val.blocks.forEach((block) => {
|
||||
blocks[block.id] = {
|
||||
id: block.id,
|
||||
type: block.ty as BlockType,
|
||||
parent: block.parent_id,
|
||||
children: block.children_id,
|
||||
data: JSON.parse(block.data),
|
||||
};
|
||||
});
|
||||
const childrenMap: Record<string, string[]> = {};
|
||||
document.val.meta.children_map.forEach((child, key) => { childrenMap[key] = child.children; });
|
||||
return {
|
||||
rootId: '',
|
||||
blocks: {},
|
||||
ytexts: {},
|
||||
yarrays: {},
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
rootId: document.val.page_id,
|
||||
blocks,
|
||||
meta: {
|
||||
childrenMap
|
||||
}
|
||||
}
|
||||
}
|
||||
return document.val;
|
||||
|
||||
};
|
||||
|
||||
insert(
|
||||
node: {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
delta?: TextDelta[];
|
||||
},
|
||||
parentId: string,
|
||||
prevId: string
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
transact(actions: (() => void)[]) {
|
||||
//
|
||||
}
|
||||
|
||||
yTextApply = (yTextId: string, delta: TextDelta[]) => {
|
||||
applyActions = (
|
||||
actions: {
|
||||
type: string;
|
||||
payload: any;
|
||||
}[]
|
||||
) => {
|
||||
//
|
||||
};
|
||||
|
||||
|
@ -29,8 +29,8 @@ export class DocumentObserver {
|
||||
};
|
||||
|
||||
unsubscribe = async () => {
|
||||
this.appListNotifier.unsubscribe();
|
||||
this.workspaceNotifier.unsubscribe();
|
||||
// this.appListNotifier.unsubscribe();
|
||||
// this.workspaceNotifier.unsubscribe();
|
||||
await this.listener?.stop();
|
||||
};
|
||||
}
|
||||
|
@ -1,22 +1,12 @@
|
||||
import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document";
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { RegionGrid } from "./region_grid";
|
||||
import { BlockType, NestedBlock, TextDelta } from '@/appflowy_app/interfaces/document';
|
||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { RegionGrid } from './region_grid';
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
data: {
|
||||
text?: string;
|
||||
style?: Record<string, any>
|
||||
};
|
||||
parent: string | null;
|
||||
children: string;
|
||||
}
|
||||
export type Node = NestedBlock;
|
||||
|
||||
export interface NodeState {
|
||||
nodes: Record<string, Node>;
|
||||
children: Record<string, string[]>;
|
||||
delta: Record<string, TextDelta[]>;
|
||||
selections: string[];
|
||||
}
|
||||
|
||||
@ -25,7 +15,6 @@ const regionGrid = new RegionGrid(50);
|
||||
const initialState: NodeState = {
|
||||
nodes: {},
|
||||
children: {},
|
||||
delta: {},
|
||||
selections: [],
|
||||
};
|
||||
|
||||
@ -33,46 +22,56 @@ export const documentSlice = createSlice({
|
||||
name: 'document',
|
||||
initialState: initialState,
|
||||
reducers: {
|
||||
clear: (state, action: PayloadAction) => {
|
||||
clear: () => {
|
||||
return initialState;
|
||||
},
|
||||
|
||||
createTree: (state, action: PayloadAction<{
|
||||
nodes: Record<string, Node>;
|
||||
children: Record<string, string[]>;
|
||||
delta: Record<string, TextDelta[]>;
|
||||
}>) => {
|
||||
const { nodes, children, delta } = action.payload;
|
||||
create: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
nodes: Record<string, Node>;
|
||||
children: Record<string, string[]>;
|
||||
}>
|
||||
) => {
|
||||
const { nodes, children } = action.payload;
|
||||
state.nodes = nodes;
|
||||
state.children = children;
|
||||
state.delta = delta;
|
||||
},
|
||||
|
||||
updateSelections: (state, action: PayloadAction<string[]>) => {
|
||||
state.selections = action.payload;
|
||||
},
|
||||
|
||||
changeSelectionByIntersectRect: (state, action: PayloadAction<{
|
||||
startX: number;
|
||||
startY: number;
|
||||
endX: number;
|
||||
endY: number
|
||||
}>) => {
|
||||
setSelectionByRect: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
startX: number;
|
||||
startY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
}>
|
||||
) => {
|
||||
const { startX, startY, endX, endY } = action.payload;
|
||||
const blocks = regionGrid.getIntersectBlocks(startX, startY, endX, endY);
|
||||
state.selections = blocks.map(block => block.id);
|
||||
state.selections = blocks.map((block) => block.id);
|
||||
},
|
||||
|
||||
updateNodePosition: (state, action: PayloadAction<{id: string; rect: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}}>) => {
|
||||
updateNodePosition: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
rect: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}>
|
||||
) => {
|
||||
const { id, rect } = action.payload;
|
||||
const position = {
|
||||
id,
|
||||
...rect
|
||||
...rect,
|
||||
};
|
||||
regionGrid.updateBlock(id, position);
|
||||
},
|
||||
@ -81,13 +80,13 @@ export const documentSlice = createSlice({
|
||||
state.nodes[action.payload.id] = action.payload;
|
||||
},
|
||||
|
||||
addChild: (state, action: PayloadAction<{ parentId: string, childId: string, prevId: string }>) => {
|
||||
addChild: (state, action: PayloadAction<{ parentId: string; childId: string; prevId: string }>) => {
|
||||
const { parentId, childId, prevId } = action.payload;
|
||||
const parentChildrenId = state.nodes[parentId].children;
|
||||
const children = state.children[parentChildrenId];
|
||||
const prevIndex = children.indexOf(prevId);
|
||||
if (prevIndex === -1) {
|
||||
children.push(childId)
|
||||
children.push(childId);
|
||||
} else {
|
||||
children.splice(prevIndex + 1, 0, childId);
|
||||
}
|
||||
@ -98,32 +97,28 @@ export const documentSlice = createSlice({
|
||||
state.children[id] = childIds;
|
||||
},
|
||||
|
||||
updateDelta: (state, action: PayloadAction<{ id: string; delta: TextDelta[] }>) => {
|
||||
const { id, delta } = action.payload;
|
||||
state.delta[id] = delta;
|
||||
},
|
||||
|
||||
updateNode: (state, action: PayloadAction<{id: string; type?: BlockType; data?: any }>) => {
|
||||
updateNode: (state, action: PayloadAction<{ id: string; data: any }>) => {
|
||||
state.nodes[action.payload.id] = {
|
||||
...state.nodes[action.payload.id],
|
||||
...action.payload
|
||||
}
|
||||
...action.payload,
|
||||
};
|
||||
},
|
||||
|
||||
removeNode: (state, action: PayloadAction<string>) => {
|
||||
const { children, data, parent } = state.nodes[action.payload];
|
||||
// remove from parent
|
||||
if (parent) {
|
||||
const index = state.children[state.nodes[parent].children].indexOf(action.payload);
|
||||
if (index > -1) {
|
||||
state.children[state.nodes[parent].children].splice(index, 1);
|
||||
}
|
||||
}
|
||||
// remove children
|
||||
if (children) {
|
||||
delete state.children[children];
|
||||
}
|
||||
if (data && data.text) {
|
||||
delete state.delta[data.text];
|
||||
}
|
||||
|
||||
// remove node
|
||||
delete state.nodes[action.payload];
|
||||
},
|
||||
},
|
||||
|
@ -23,7 +23,7 @@ export const useDocument = () => {
|
||||
const res = await c.open();
|
||||
console.log(res)
|
||||
if (!res) return;
|
||||
setDocumentData(res)
|
||||
// setDocumentData(res)
|
||||
setDocumentId(params.id)
|
||||
|
||||
})();
|
||||
|
@ -14,7 +14,7 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use nanoid::nanoid;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::entities::{BlockMapPB, BlockPB, ChildrenPB, DocumentDataPB2, MetaPB};
|
||||
use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB2, MetaPB};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Document(Arc<Mutex<InnerDocument>>);
|
||||
@ -96,7 +96,7 @@ impl From<DocumentDataWrapper> for DocumentDataPB2 {
|
||||
.collect::<HashMap<String, ChildrenPB>>();
|
||||
Self {
|
||||
page_id: data.0.page_id,
|
||||
blocks: BlockMapPB { blocks },
|
||||
blocks,
|
||||
meta: MetaPB { children_map },
|
||||
}
|
||||
}
|
||||
|
@ -31,18 +31,12 @@ pub struct DocumentDataPB2 {
|
||||
pub page_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub blocks: BlockMapPB,
|
||||
pub blocks: HashMap<String, BlockPB>,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub meta: MetaPB,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct BlockMapPB {
|
||||
#[pb(index = 1)]
|
||||
pub blocks: HashMap<String, BlockPB>,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct BlockPB {
|
||||
#[pb(index = 1)]
|
||||
|
Loading…
Reference in New Issue
Block a user