mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
refactor: document controller
This commit is contained in:
parent
35c21c0d84
commit
886766c887
@ -72,7 +72,7 @@ export function useBlockSelection({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const calcIntersectBlocks = useCallback(
|
const updateSelctionsByPoint = useCallback(
|
||||||
(clientX: number, clientY: number) => {
|
(clientX: number, clientY: number) => {
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
const [startX, startY] = pointRef.current;
|
const [startX, startY] = pointRef.current;
|
||||||
@ -86,7 +86,7 @@ export function useBlockSelection({
|
|||||||
endY,
|
endY,
|
||||||
});
|
});
|
||||||
disaptch(
|
disaptch(
|
||||||
documentActions.changeSelectionByIntersectRect({
|
documentActions.setSelectionByRect({
|
||||||
startX: Math.min(startX, endX),
|
startX: Math.min(startX, endX),
|
||||||
startY: Math.min(startY, endY),
|
startY: Math.min(startY, endY),
|
||||||
endX: Math.max(startX, endX),
|
endX: Math.max(startX, endX),
|
||||||
@ -102,7 +102,7 @@ export function useBlockSelection({
|
|||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
calcIntersectBlocks(e.clientX, e.clientY);
|
updateSelctionsByPoint(e.clientX, e.clientY);
|
||||||
|
|
||||||
const { top, bottom } = container.getBoundingClientRect();
|
const { top, bottom } = container.getBoundingClientRect();
|
||||||
if (e.clientY >= bottom) {
|
if (e.clientY >= bottom) {
|
||||||
@ -124,7 +124,7 @@ export function useBlockSelection({
|
|||||||
}
|
}
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
calcIntersectBlocks(e.clientX, e.clientY);
|
updateSelctionsByPoint(e.clientX, e.clientY);
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
setRect(null);
|
setRect(null);
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@ import { BlockType } from '@/appflowy_app/interfaces/document';
|
|||||||
import { useAppSelector } from '@/appflowy_app/stores/store';
|
import { useAppSelector } from '@/appflowy_app/stores/store';
|
||||||
import { debounce } from '@/appflowy_app/utils/tool';
|
import { debounce } from '@/appflowy_app/utils/tool';
|
||||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { YDocControllerContext } from '../../../stores/effects/document/document_controller';
|
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useController() {
|
function useController() {
|
||||||
const controller = useContext(YDocControllerContext);
|
const controller = useContext(DocumentControllerContext);
|
||||||
|
|
||||||
const insertAfter = useCallback((node: Node) => {
|
const insertAfter = useCallback((node: Node) => {
|
||||||
const parentId = node.parent;
|
const parentId = node.parent;
|
||||||
|
@ -5,14 +5,14 @@ import { documentActions } from '$app/stores/reducers/document/slice';
|
|||||||
|
|
||||||
export function useParseTree(documentData: DocumentData) {
|
export function useParseTree(documentData: DocumentData) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { blocks, ytexts, yarrays } = documentData;
|
const { blocks, meta } = documentData;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
documentActions.createTree({
|
documentActions.create({
|
||||||
nodes: blocks,
|
nodes: blocks,
|
||||||
delta: ytexts,
|
delta: meta.text_map,
|
||||||
children: yarrays,
|
children: meta.children_map,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { useRoot } from './Root.hooks';
|
|||||||
import Node from '../Node';
|
import Node from '../Node';
|
||||||
import { withErrorBoundary } from 'react-error-boundary';
|
import { withErrorBoundary } from 'react-error-boundary';
|
||||||
import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
|
import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
|
||||||
import VirtualizerList from '../VirtualizerList';
|
import VirtualizedList from '../VirtualizerList';
|
||||||
import { Skeleton } from '@mui/material';
|
import { Skeleton } from '@mui/material';
|
||||||
|
|
||||||
function Root({ documentData }: { documentData: DocumentData }) {
|
function Root({ documentData }: { documentData: DocumentData }) {
|
||||||
@ -20,7 +20,7 @@ function Root({ documentData }: { documentData: DocumentData }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id='appflowy-block-doc' className='h-[100%] overflow-hidden'>
|
<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>
|
</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: Delta) => 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 Delta);
|
|
||||||
}
|
|
||||||
yText.applyDelta(delta);
|
|
||||||
yText.observe(textEventHandler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
yText.unobserve(textEventHandler);
|
|
||||||
}
|
|
||||||
}, [delta])
|
|
||||||
|
|
||||||
|
|
||||||
return { editor }
|
|
||||||
}
|
|
@ -1,72 +1,18 @@
|
|||||||
import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey";
|
import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey';
|
||||||
import { useCallback, useContext, useMemo, useRef, useState } from "react";
|
import { useCallback, useState } from 'react';
|
||||||
import { Descendant, Range } from "slate";
|
import { Descendant, Range } from 'slate';
|
||||||
import { useBindYjs } from "./BindYjs.hooks";
|
import { TextDelta } from '$app/interfaces/document';
|
||||||
import { YDocControllerContext } from '../../../stores/effects/document/document_controller';
|
import { useTextInput } from '../_shared/TextInput.hooks';
|
||||||
import { Delta } from "@slate-yjs/core/dist/model/types";
|
|
||||||
import { TextDelta } from '../../../interfaces/document';
|
|
||||||
import { debounce } from "@/appflowy_app/utils/tool";
|
|
||||||
|
|
||||||
function useController(textId: string) {
|
|
||||||
const docController = useContext(YDocControllerContext);
|
|
||||||
|
|
||||||
const update = useCallback(
|
|
||||||
(delta: Delta) => {
|
|
||||||
docController?.yTextApply(textId, delta)
|
|
||||||
},
|
|
||||||
[textId],
|
|
||||||
);
|
|
||||||
const transact = useCallback(
|
|
||||||
(actions: (() => void)[]) => {
|
|
||||||
docController?.transact(actions)
|
|
||||||
},
|
|
||||||
[textId],
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
update,
|
|
||||||
transact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useTransact(textId: string) {
|
|
||||||
const pendingActions = useRef<(() => void)[]>([]);
|
|
||||||
const { update, transact } = useController(textId);
|
|
||||||
|
|
||||||
const sendTransact = useCallback(
|
|
||||||
() => {
|
|
||||||
const actions = pendingActions.current;
|
|
||||||
transact(actions);
|
|
||||||
},
|
|
||||||
[transact],
|
|
||||||
)
|
|
||||||
|
|
||||||
const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]);
|
|
||||||
|
|
||||||
const sendDelta = useCallback(
|
|
||||||
(delta: Delta) => {
|
|
||||||
const action = () => update(delta);
|
|
||||||
pendingActions.current.push(action);
|
|
||||||
debounceSendTransact()
|
|
||||||
},
|
|
||||||
[update, debounceSendTransact],
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
sendDelta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTextBlock(text: string, delta: TextDelta[]) {
|
export function useTextBlock(text: string, delta: TextDelta[]) {
|
||||||
const { sendDelta } = useTransact(text);
|
const { editor } = useTextInput(text, delta);
|
||||||
|
|
||||||
const { editor } = useBindYjs(delta, sendDelta);
|
|
||||||
const [value, setValue] = useState<Descendant[]>([]);
|
const [value, setValue] = useState<Descendant[]>([]);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(e: Descendant[]) => {
|
(e: Descendant[]) => {
|
||||||
setValue(e);
|
setValue(e);
|
||||||
},
|
},
|
||||||
[editor],
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
@ -74,14 +20,13 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
|
|||||||
case 'Enter': {
|
case 'Enter': {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'Backspace': {
|
case 'Backspace': {
|
||||||
if (!editor.selection) return;
|
if (!editor.selection) return;
|
||||||
const { anchor } = editor.selection;
|
const { anchor } = editor.selection;
|
||||||
const isCollapase = Range.isCollapsed(editor.selection);
|
const isCollapsed = Range.isCollapsed(editor.selection);
|
||||||
if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') {
|
if (isCollapsed && anchor.offset === 0 && anchor.path.toString() === '0,0') {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
@ -89,16 +34,15 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
triggerHotkey(event, editor);
|
triggerHotkey(event, editor);
|
||||||
}
|
};
|
||||||
|
|
||||||
const onDOMBeforeInput = useCallback((e: InputEvent) => {
|
const onDOMBeforeInput = useCallback((e: InputEvent) => {
|
||||||
// COMPAT: in Apple, `compositionend` is dispatched after the
|
// COMPAT: in Apple, `compositionend` is dispatched after the `beforeinput` for "insertFromComposition".
|
||||||
// `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese.
|
// It will cause repeated characters when inputting Chinese.
|
||||||
// Here, prevent the beforeInput event and wait for the compositionend event to take effect
|
// Here, prevent the beforeInput event and wait for the compositionend event to take effect.
|
||||||
if (e.inputType === 'insertFromComposition') {
|
if (e.inputType === 'insertFromComposition') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -106,6 +50,6 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
|
|||||||
onKeyDownCapture,
|
onKeyDownCapture,
|
||||||
onDOMBeforeInput,
|
onDOMBeforeInput,
|
||||||
editor,
|
editor,
|
||||||
value
|
value,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import Leaf from './Leaf';
|
|||||||
import { useTextBlock } from './TextBlock.hooks';
|
import { useTextBlock } from './TextBlock.hooks';
|
||||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||||
import NodeComponent from '../Node';
|
import NodeComponent from '../Node';
|
||||||
import HoveringToolbar from '../HoveringToolbar';
|
import HoveringToolbar from '../_shared/HoveringToolbar';
|
||||||
import { TextDelta } from '@/appflowy_app/interfaces/document';
|
import { TextDelta } from '@/appflowy_app/interfaces/document';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
@ -3,10 +3,10 @@ import { useRef } from 'react';
|
|||||||
|
|
||||||
const defaultSize = 60;
|
const defaultSize = 60;
|
||||||
|
|
||||||
export function useVirtualizerList(count: number) {
|
export function useVirtualizedList(count: number) {
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const rowVirtualizer = useVirtualizer({
|
const Virtualize = useVirtualizer({
|
||||||
count,
|
count,
|
||||||
getScrollElement: () => parentRef.current,
|
getScrollElement: () => parentRef.current,
|
||||||
estimateSize: () => {
|
estimateSize: () => {
|
||||||
@ -15,7 +15,7 @@ export function useVirtualizerList(count: number) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rowVirtualizer,
|
Virtualize: Virtualize,
|
||||||
parentRef,
|
parentRef,
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useVirtualizerList } from './VirtualizerList.hooks';
|
import { useVirtualizedList } from './VirtualizedList.hooks';
|
||||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||||
import DocumentTitle from '../DocumentTitle';
|
import DocumentTitle from '../DocumentTitle';
|
||||||
import Overlay from '../Overlay';
|
import Overlay from '../Overlay';
|
||||||
|
|
||||||
export default function VirtualizerList({
|
export default function VirtualizedList({
|
||||||
childIds,
|
childIds,
|
||||||
node,
|
node,
|
||||||
renderNode,
|
renderNode,
|
||||||
@ -13,9 +13,8 @@ export default function VirtualizerList({
|
|||||||
node: Node;
|
node: Node;
|
||||||
renderNode: (nodeId: string) => JSX.Element;
|
renderNode: (nodeId: string) => JSX.Element;
|
||||||
}) {
|
}) {
|
||||||
const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length);
|
const { Virtualize, parentRef } = useVirtualizedList(childIds.length);
|
||||||
|
const virtualItems = Virtualize.getVirtualItems();
|
||||||
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -26,7 +25,7 @@ export default function VirtualizerList({
|
|||||||
<div
|
<div
|
||||||
className='doc-body max-w-screen w-[900px] min-w-0'
|
className='doc-body max-w-screen w-[900px] min-w-0'
|
||||||
style={{
|
style={{
|
||||||
height: rowVirtualizer.getTotalSize(),
|
height: Virtualize.getTotalSize(),
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -43,7 +42,7 @@ export default function VirtualizerList({
|
|||||||
{virtualItems.map((virtualRow) => {
|
{virtualItems.map((virtualRow) => {
|
||||||
const id = childIds[virtualRow.index];
|
const id = childIds[virtualRow.index];
|
||||||
return (
|
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}
|
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
|
||||||
{renderNode(id)}
|
{renderNode(id)}
|
||||||
</div>
|
</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 IconButton from '@mui/material/IconButton';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
|
@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useFocused, useSlate } from 'slate-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) {
|
export function useHoveringToolbar(id: string) {
|
||||||
const editor = useSlate();
|
const editor = useSlate();
|
||||||
@ -29,6 +28,6 @@ export function useHoveringToolbar(id: string) {
|
|||||||
return {
|
return {
|
||||||
ref,
|
ref,
|
||||||
inFocus,
|
inFocus,
|
||||||
editor
|
editor,
|
||||||
}
|
};
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import FormatButton from './FormatButton';
|
import FormatButton from './FormatButton';
|
||||||
import Portal from '../BlockPortal';
|
import Portal from '../../BlockPortal';
|
||||||
import { useHoveringToolbar } from './index.hooks';
|
import { useHoveringToolbar } from './index.hooks';
|
||||||
|
|
||||||
const HoveringToolbar = ({ id }: { id: string }) => {
|
const HoveringToolbar = ({ id }: { id: string }) => {
|
@ -3,22 +3,36 @@ import { useAppSelector } from '@/appflowy_app/stores/store';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { TextDelta } from '@/appflowy_app/interfaces/document';
|
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) {
|
export function useSubscribeNode(id: string) {
|
||||||
const node = useAppSelector<Node>(state => state.document.nodes[id]);
|
const node = useAppSelector<Node>(state => state.document.nodes[id]);
|
||||||
|
|
||||||
const childIds = useAppSelector<string[] | undefined>(state => {
|
const childIds = useAppSelector<string[] | undefined>(state => {
|
||||||
const childrenId = state.document.nodes[id]?.children;
|
const childrenId = state.document.nodes[id]?.children;
|
||||||
if (!childrenId) return;
|
if (!childrenId) return;
|
||||||
return state.document.children[childrenId];
|
return state.document.children[childrenId];
|
||||||
});
|
});
|
||||||
|
|
||||||
const delta = useAppSelector<TextDelta[] | undefined>(state => {
|
const delta = useAppSelector<TextDelta[] | undefined>(state => {
|
||||||
const deltaId = state.document.nodes[id]?.data?.text;
|
const externalType = state.document.nodes[id]?.externalType;
|
||||||
|
if (externalType !== 'text') return;
|
||||||
|
const deltaId = state.document.nodes[id]?.externalId;
|
||||||
if (!deltaId) return;
|
if (!deltaId) return;
|
||||||
return state.document.delta[deltaId];
|
return state.document.delta[deltaId];
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSelected = useAppSelector<boolean>(state => {
|
const isSelected = useAppSelector<boolean>(state => {
|
||||||
return state.document.selections?.includes(id) || false;
|
return state.document.selections?.includes(id) || false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Memoize the node and its children
|
||||||
|
// So that the component will not be re-rendered when other node is changed
|
||||||
|
// It very important for performance
|
||||||
const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]);
|
const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]);
|
||||||
const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]);
|
const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]);
|
||||||
const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]);
|
const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]);
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
import { useCallback, useContext, useMemo, useRef, useEffect } from 'react';
|
||||||
|
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { TextDelta } from '$app/interfaces/document';
|
||||||
|
import { debounce } from '@/appflowy_app/utils/tool';
|
||||||
|
import { createEditor } from 'slate';
|
||||||
|
import { withReact } from 'slate-react';
|
||||||
|
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
|
||||||
|
|
||||||
|
export function useTextInput(text: string, delta: TextDelta[]) {
|
||||||
|
const { sendDelta } = useTransact(text);
|
||||||
|
const { editor } = useBindYjs(delta, sendDelta);
|
||||||
|
|
||||||
|
return {
|
||||||
|
editor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useController(textId: string) {
|
||||||
|
const docController = useContext(DocumentControllerContext);
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
(delta: TextDelta[]) => {
|
||||||
|
docController?.yTextApply(textId, delta);
|
||||||
|
},
|
||||||
|
[textId]
|
||||||
|
);
|
||||||
|
const transact = useCallback(
|
||||||
|
(actions: (() => void)[]) => {
|
||||||
|
docController?.transact(actions);
|
||||||
|
},
|
||||||
|
[textId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
update,
|
||||||
|
transact,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTransact(textId: string) {
|
||||||
|
const pendingActions = useRef<(() => void)[]>([]);
|
||||||
|
const { update, transact } = useController(textId);
|
||||||
|
|
||||||
|
const sendTransact = useCallback(() => {
|
||||||
|
const actions = pendingActions.current;
|
||||||
|
transact(actions);
|
||||||
|
}, [transact]);
|
||||||
|
|
||||||
|
const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]);
|
||||||
|
|
||||||
|
const sendDelta = useCallback(
|
||||||
|
(delta: TextDelta[]) => {
|
||||||
|
const action = () => update(delta);
|
||||||
|
pendingActions.current.push(action);
|
||||||
|
debounceSendTransact();
|
||||||
|
},
|
||||||
|
[update, debounceSendTransact]
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
sendDelta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValue = [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [{ text: '' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
|
||||||
|
const yTextRef = useRef<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) => {
|
||||||
|
update(event.changes.delta as TextDelta[]);
|
||||||
|
};
|
||||||
|
yText.applyDelta(delta);
|
||||||
|
yText.observe(textEventHandler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
yText.unobserve(textEventHandler);
|
||||||
|
};
|
||||||
|
}, [delta]);
|
||||||
|
|
||||||
|
return { editor };
|
||||||
|
}
|
@ -9,7 +9,6 @@ import { useError } from '../../error/Error.hooks';
|
|||||||
import { AppObserver } from '../../../stores/effects/folder/app/app_observer';
|
import { AppObserver } from '../../../stores/effects/folder/app/app_observer';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||||
import { YDocController } from '$app/stores/effects/document/document_controller';
|
|
||||||
|
|
||||||
export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||||
const appDispatch = useAppDispatch();
|
const appDispatch = useAppDispatch();
|
||||||
@ -133,10 +132,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
|||||||
layoutType: ViewLayoutTypePB.Document,
|
layoutType: ViewLayoutTypePB.Document,
|
||||||
});
|
});
|
||||||
|
|
||||||
// temp: let me try it by yjs
|
|
||||||
const ydocController = new YDocController(newView.id);
|
|
||||||
await ydocController.createDocument();
|
|
||||||
|
|
||||||
appDispatch(
|
appDispatch(
|
||||||
pagesActions.addPage({
|
pagesActions.addPage({
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { TextBlockToolbarGroup } from "../interfaces";
|
|
||||||
|
|
||||||
export const iconSize = { width: 18, height: 18 };
|
export const iconSize = { width: 18, height: 18 };
|
||||||
|
|
||||||
@ -24,16 +23,3 @@ export const command: Record<string, { title: string; key: string }> = {
|
|||||||
key: '⌘ + Shift + S or ⌘ + Shift + X',
|
key: '⌘ + Shift + S or ⌘ + Shift + X',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toolbarDefaultProps = {
|
|
||||||
showGroups: [
|
|
||||||
TextBlockToolbarGroup.ASK_AI,
|
|
||||||
TextBlockToolbarGroup.BLOCK_SELECT,
|
|
||||||
TextBlockToolbarGroup.ADD_LINK,
|
|
||||||
TextBlockToolbarGroup.COMMENT,
|
|
||||||
TextBlockToolbarGroup.TEXT_FORMAT,
|
|
||||||
TextBlockToolbarGroup.TEXT_COLOR,
|
|
||||||
TextBlockToolbarGroup.MENTION,
|
|
||||||
TextBlockToolbarGroup.MORE,
|
|
||||||
],
|
|
||||||
};
|
|
@ -16,6 +16,8 @@ export interface NestedBlock {
|
|||||||
id: string;
|
id: string;
|
||||||
type: BlockType;
|
type: BlockType;
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
|
externalId: string;
|
||||||
|
externalType: 'text' | 'array' | 'map';
|
||||||
parent: string | null;
|
parent: string | null;
|
||||||
children: string;
|
children: string;
|
||||||
}
|
}
|
||||||
@ -26,6 +28,8 @@ export interface TextDelta {
|
|||||||
export interface DocumentData {
|
export interface DocumentData {
|
||||||
rootId: string;
|
rootId: string;
|
||||||
blocks: Record<string, NestedBlock>;
|
blocks: Record<string, NestedBlock>;
|
||||||
ytexts: Record<string, TextDelta[]>;
|
meta: {
|
||||||
yarrays: Record<string, string[]>;
|
text_map: Record<string, TextDelta[]>;
|
||||||
|
children_map: Record<string, string[]>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,112 +1 @@
|
|||||||
import { Descendant } from "slate";
|
export interface Document {}
|
||||||
|
|
||||||
// eslint-disable-next-line no-shadow
|
|
||||||
export enum BlockType {
|
|
||||||
PageBlock = 'page',
|
|
||||||
HeadingBlock = 'heading',
|
|
||||||
ListBlock = 'list',
|
|
||||||
TextBlock = 'text',
|
|
||||||
CodeBlock = 'code',
|
|
||||||
EmbedBlock = 'embed',
|
|
||||||
QuoteBlock = 'quote',
|
|
||||||
DividerBlock = 'divider',
|
|
||||||
MediaBlock = 'media',
|
|
||||||
TableBlock = 'table',
|
|
||||||
ColumnBlock = 'column'
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BlockData<T = BlockType> = T extends BlockType.TextBlock ? TextBlockData :
|
|
||||||
T extends BlockType.PageBlock ? PageBlockData :
|
|
||||||
T extends BlockType.HeadingBlock ? HeadingBlockData :
|
|
||||||
T extends BlockType.ListBlock ? ListBlockData :
|
|
||||||
T extends BlockType.ColumnBlock ? ColumnBlockData : any;
|
|
||||||
|
|
||||||
|
|
||||||
export interface BlockInterface<T = BlockType> {
|
|
||||||
id: string;
|
|
||||||
type: BlockType;
|
|
||||||
data: BlockData<T>;
|
|
||||||
next: string | null;
|
|
||||||
firstChild: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface TextBlockData {
|
|
||||||
content: Descendant[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageBlockData {
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ListBlockData extends TextBlockData {
|
|
||||||
type: 'numbered' | 'bulleted' | 'column';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HeadingBlockData extends TextBlockData {
|
|
||||||
level: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ColumnBlockData {
|
|
||||||
ratio: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-shadow
|
|
||||||
export enum TextBlockToolbarGroup {
|
|
||||||
ASK_AI,
|
|
||||||
BLOCK_SELECT,
|
|
||||||
ADD_LINK,
|
|
||||||
COMMENT,
|
|
||||||
TEXT_FORMAT,
|
|
||||||
TEXT_COLOR,
|
|
||||||
MENTION,
|
|
||||||
MORE
|
|
||||||
}
|
|
||||||
export interface TextBlockToolbarProps {
|
|
||||||
showGroups: TextBlockToolbarGroup[]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface BlockCommonProps<T> {
|
|
||||||
version: number;
|
|
||||||
node: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackendOp {
|
|
||||||
type: 'update' | 'insert' | 'remove' | 'move' | 'move_range';
|
|
||||||
version: number;
|
|
||||||
data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData;
|
|
||||||
}
|
|
||||||
export interface LocalOp {
|
|
||||||
type: 'update' | 'insert' | 'remove' | 'move' | 'move_range';
|
|
||||||
version: number;
|
|
||||||
data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateOpData {
|
|
||||||
blockId: string;
|
|
||||||
value: BlockData;
|
|
||||||
path: string[];
|
|
||||||
}
|
|
||||||
export interface InsertOpData {
|
|
||||||
block: BlockInterface;
|
|
||||||
parentId: string;
|
|
||||||
prevId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface moveRangeOpData {
|
|
||||||
range: [string, string];
|
|
||||||
newParentId: string;
|
|
||||||
newPrevId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface moveOpData {
|
|
||||||
blockId: string;
|
|
||||||
newParentId: string;
|
|
||||||
newPrevId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface removeOpData {
|
|
||||||
blockId: string
|
|
||||||
}
|
|
@ -1,194 +1,48 @@
|
|||||||
import * as Y from 'yjs';
|
import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document';
|
||||||
import { IndexeddbPersistence } from 'y-indexeddb';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import { DocumentData, NestedBlock } from '@/appflowy_app/interfaces/document';
|
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import { BlockType } from '@/appflowy_app/interfaces';
|
import { DocumentBackendService } from './document_bd_svc';
|
||||||
|
|
||||||
export type DeltaAttributes = {
|
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
||||||
retain: number;
|
|
||||||
attributes: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeltaRetain = { retain: number };
|
export class DocumentController {
|
||||||
export type DeltaDelete = { delete: number };
|
private readonly backendService: DocumentBackendService;
|
||||||
export type DeltaInsert = {
|
|
||||||
insert: string | Y.XmlText;
|
|
||||||
attributes?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InsertDelta = Array<DeltaInsert>;
|
constructor(public readonly viewId: string) {
|
||||||
export type Delta = Array<
|
this.backendService = new DocumentBackendService(viewId);
|
||||||
DeltaRetain | DeltaDelete | DeltaInsert | DeltaAttributes
|
|
||||||
>;
|
|
||||||
|
|
||||||
|
|
||||||
export const YDocControllerContext = createContext<YDocController | null>(null);
|
|
||||||
|
|
||||||
export class YDocController {
|
|
||||||
private _ydoc: Y.Doc;
|
|
||||||
private readonly provider: IndexeddbPersistence;
|
|
||||||
|
|
||||||
constructor(private id: string) {
|
|
||||||
this._ydoc = new Y.Doc();
|
|
||||||
this.provider = new IndexeddbPersistence(`document-${this.id}`, this._ydoc);
|
|
||||||
this._ydoc.on('update', this.handleUpdate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdate = (update: Uint8Array, origin: any) => {
|
open = async (): Promise<DocumentData | null> => {
|
||||||
const isLocal = origin === null;
|
const openDocumentResult = await this.backendService.open();
|
||||||
Y.logUpdate(update);
|
if (openDocumentResult.ok) {
|
||||||
}
|
return {
|
||||||
|
rootId: '',
|
||||||
|
blocks: {},
|
||||||
createDocument = async () => {
|
|
||||||
await this.provider.whenSynced;
|
|
||||||
const ydoc = this._ydoc;
|
|
||||||
const blocks = ydoc.getMap('blocks');
|
|
||||||
const rootNode = ydoc.getArray("root");
|
|
||||||
|
|
||||||
// create page block for root node
|
|
||||||
const rootId = v4();
|
|
||||||
rootNode.push([rootId])
|
|
||||||
const rootChildrenId = v4();
|
|
||||||
const rootChildren = ydoc.getArray(rootChildrenId);
|
|
||||||
const rootTitleId = v4();
|
|
||||||
const yTitle = ydoc.getText(rootTitleId);
|
|
||||||
yTitle.insert(0, "");
|
|
||||||
const root = {
|
|
||||||
id: rootId,
|
|
||||||
type: 'page',
|
|
||||||
data: {
|
|
||||||
text: rootTitleId
|
|
||||||
},
|
|
||||||
parent: null,
|
|
||||||
children: rootChildrenId
|
|
||||||
};
|
|
||||||
blocks.set(root.id, root);
|
|
||||||
|
|
||||||
// create text block for first line
|
|
||||||
const textId = v4();
|
|
||||||
const yTextId = v4();
|
|
||||||
const ytext = ydoc.getText(yTextId);
|
|
||||||
ytext.insert(0, "");
|
|
||||||
const textChildrenId = v4();
|
|
||||||
ydoc.getArray(textChildrenId);
|
|
||||||
const text = {
|
|
||||||
id: textId,
|
|
||||||
type: 'text',
|
|
||||||
data: {
|
|
||||||
text: yTextId,
|
|
||||||
},
|
|
||||||
parent: rootId,
|
|
||||||
children: textChildrenId,
|
|
||||||
}
|
|
||||||
|
|
||||||
// add text block to root children
|
|
||||||
rootChildren.push([textId]);
|
|
||||||
blocks.set(text.id, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
open = async (): Promise<DocumentData> => {
|
|
||||||
await this.provider.whenSynced;
|
|
||||||
const ydoc = this._ydoc;
|
|
||||||
|
|
||||||
const blocks = ydoc.getMap('blocks');
|
|
||||||
const obj: DocumentData = {
|
|
||||||
rootId: ydoc.getArray<string>('root').toArray()[0] || '',
|
|
||||||
blocks: blocks.toJSON(),
|
|
||||||
ytexts: {},
|
ytexts: {},
|
||||||
yarrays: {}
|
yarrays: {}
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Object.keys(obj.blocks).forEach(key => {
|
|
||||||
const value = obj.blocks[key];
|
|
||||||
if (value.children) {
|
|
||||||
const yarray = ydoc.getArray<string>(value.children);
|
|
||||||
Object.assign(obj.yarrays, {
|
|
||||||
[value.children]: yarray.toArray()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (value.data.text) {
|
|
||||||
const ytext = ydoc.getText(value.data.text);
|
|
||||||
Object.assign(obj.ytexts, {
|
|
||||||
[value.data.text]: ytext.toDelta()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
blocks.observe(this.handleBlocksEvent);
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
insert(node: {
|
insert(node: {
|
||||||
id: string,
|
id: string,
|
||||||
type: BlockType,
|
type: BlockType,
|
||||||
delta?: Delta
|
delta?: TextDelta[]
|
||||||
}, parentId: string, prevId: string) {
|
}, parentId: string, prevId: string) {
|
||||||
const blocks = this._ydoc.getMap<NestedBlock>('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)[]) {
|
transact(actions: (() => void)[]) {
|
||||||
const ydoc = this._ydoc;
|
//
|
||||||
console.log('====transact')
|
|
||||||
ydoc.transact(() => {
|
|
||||||
actions.forEach(action => {
|
|
||||||
action();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yTextApply = (yTextId: string, delta: Delta) => {
|
yTextApply = (yTextId: string, delta: TextDelta[]) => {
|
||||||
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<unknown>) => {
|
|
||||||
console.log(mapEvent.changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleTextEvent = (textEvent: Y.YTextEvent) => {
|
|
||||||
console.log(textEvent.changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleArrayEvent = (arrayEvent: Y.YArrayEvent<string>) => {
|
|
||||||
console.log(arrayEvent.changes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose = async () => {
|
||||||
|
await this.backendService.close();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,8 @@
|
|||||||
import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document";
|
import { BlockType, NestedBlock, TextDelta } from "@/appflowy_app/interfaces/document";
|
||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
import { RegionGrid } from "./region_grid";
|
import { RegionGrid } from "./region_grid";
|
||||||
|
|
||||||
export interface Node {
|
export type Node = NestedBlock;
|
||||||
id: string;
|
|
||||||
type: BlockType;
|
|
||||||
data: {
|
|
||||||
text?: string;
|
|
||||||
style?: Record<string, any>
|
|
||||||
};
|
|
||||||
parent: string | null;
|
|
||||||
children: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeState {
|
export interface NodeState {
|
||||||
nodes: Record<string, Node>;
|
nodes: Record<string, Node>;
|
||||||
@ -33,11 +24,11 @@ export const documentSlice = createSlice({
|
|||||||
name: 'document',
|
name: 'document',
|
||||||
initialState: initialState,
|
initialState: initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
clear: (state, action: PayloadAction) => {
|
clear: () => {
|
||||||
return initialState;
|
return initialState;
|
||||||
},
|
},
|
||||||
|
|
||||||
createTree: (state, action: PayloadAction<{
|
create: (state, action: PayloadAction<{
|
||||||
nodes: Record<string, Node>;
|
nodes: Record<string, Node>;
|
||||||
children: Record<string, string[]>;
|
children: Record<string, string[]>;
|
||||||
delta: Record<string, TextDelta[]>;
|
delta: Record<string, TextDelta[]>;
|
||||||
@ -52,7 +43,7 @@ export const documentSlice = createSlice({
|
|||||||
state.selections = action.payload;
|
state.selections = action.payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
changeSelectionByIntersectRect: (state, action: PayloadAction<{
|
setSelectionByRect: (state, action: PayloadAction<{
|
||||||
startX: number;
|
startX: number;
|
||||||
startY: number;
|
startY: number;
|
||||||
endX: number;
|
endX: number;
|
||||||
@ -77,26 +68,57 @@ export const documentSlice = createSlice({
|
|||||||
regionGrid.updateBlock(id, position);
|
regionGrid.updateBlock(id, position);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addNode: (state, action: PayloadAction<Node>) => {
|
||||||
|
state.nodes[action.payload.id] = action.payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
addChild: (state, action: PayloadAction<{ parentId: string, childId: string, prevId: string }>) => {
|
||||||
|
const { parentId, childId, prevId } = action.payload;
|
||||||
|
const parentChildrenId = state.nodes[parentId].children;
|
||||||
|
const children = state.children[parentChildrenId];
|
||||||
|
const prevIndex = children.indexOf(prevId);
|
||||||
|
if (prevIndex === -1) {
|
||||||
|
children.push(childId)
|
||||||
|
} else {
|
||||||
|
children.splice(prevIndex + 1, 0, childId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateChildren: (state, action: PayloadAction<{ id: string; childIds: string[] }>) => {
|
||||||
|
const { id, childIds } = action.payload;
|
||||||
|
state.children[id] = childIds;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateDelta: (state, action: PayloadAction<{ id: string; delta: TextDelta[] }>) => {
|
||||||
|
const { id, delta } = action.payload;
|
||||||
|
state.delta[id] = delta;
|
||||||
|
},
|
||||||
|
|
||||||
updateNode: (state, action: PayloadAction<{id: string; type?: BlockType; data?: any }>) => {
|
updateNode: (state, action: PayloadAction<{id: string; type?: BlockType; data?: any }>) => {
|
||||||
state.nodes[action.payload.id] = {
|
state.nodes[action.payload.id] = {
|
||||||
...state.nodes[action.payload.id],
|
...state.nodes[action.payload.id],
|
||||||
...action.payload
|
...action.payload
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeNode: (state, action: PayloadAction<string>) => {
|
removeNode: (state, action: PayloadAction<string>) => {
|
||||||
const { children, data, parent } = state.nodes[action.payload];
|
const { children, data, parent } = state.nodes[action.payload];
|
||||||
|
// remove from parent
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const index = state.children[state.nodes[parent].children].indexOf(action.payload);
|
const index = state.children[state.nodes[parent].children].indexOf(action.payload);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
state.children[state.nodes[parent].children].splice(index, 1);
|
state.children[state.nodes[parent].children].splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// remove children
|
||||||
if (children) {
|
if (children) {
|
||||||
delete state.children[children];
|
delete state.children[children];
|
||||||
}
|
}
|
||||||
|
// remove delta
|
||||||
if (data && data.text) {
|
if (data && data.text) {
|
||||||
delete state.delta[data.text];
|
delete state.delta[data.text];
|
||||||
}
|
}
|
||||||
|
// remove node
|
||||||
delete state.nodes[action.payload];
|
delete state.nodes[action.payload];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import { BlockData, BlockType } from "../interfaces";
|
|
||||||
|
|
||||||
|
|
||||||
export function filterSelections<TreeNode extends {
|
|
||||||
id: string;
|
|
||||||
children: TreeNode[];
|
|
||||||
parent: TreeNode | null;
|
|
||||||
type: BlockType;
|
|
||||||
data: BlockData;
|
|
||||||
}>(ids: string[], nodeMap: Map<string, TreeNode>): string[] {
|
|
||||||
const selected = new Set(ids);
|
|
||||||
const newSelected = new Set<string>();
|
|
||||||
ids.forEach(selectedId => {
|
|
||||||
const node = nodeMap.get(selectedId);
|
|
||||||
if (!node) return;
|
|
||||||
if (node.type === BlockType.ListBlock && node.data.type === 'column') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (node.children.length === 0) {
|
|
||||||
newSelected.add(selectedId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hasChildSelected = node.children.some(i => selected.has(i.id));
|
|
||||||
if (!hasChildSelected) {
|
|
||||||
newSelected.add(selectedId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hasSiblingSelected = node.parent?.children.filter(i => i.id !== selectedId).some(i => selected.has(i.id));
|
|
||||||
if (hasChildSelected && hasSiblingSelected) {
|
|
||||||
newSelected.add(selectedId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(newSelected);
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import { createContext } from "react";
|
|
||||||
import { TextBlockManager } from '../../block_editor/blocks/text_block';
|
|
||||||
|
|
||||||
export const TextBlockContext = createContext<{
|
|
||||||
textBlockManager?: TextBlockManager
|
|
||||||
}>({});
|
|
@ -6,24 +6,26 @@ import {
|
|||||||
} from '../../services/backend/events/flowy-document';
|
} from '../../services/backend/events/flowy-document';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { DocumentData } from '../interfaces/document';
|
import { DocumentData } from '../interfaces/document';
|
||||||
import { YDocController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
|
||||||
|
|
||||||
export const useDocument = () => {
|
export const useDocument = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [ documentId, setDocumentId ] = useState<string>();
|
const [ documentId, setDocumentId ] = useState<string>();
|
||||||
const [ documentData, setDocumentData ] = useState<DocumentData>();
|
const [ documentData, setDocumentData ] = useState<DocumentData>();
|
||||||
const [ controller, setController ] = useState<YDocController | null>(null);
|
const [ controller, setController ] = useState<DocumentController | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (!params?.id) return;
|
if (!params?.id) return;
|
||||||
const c = new YDocController(params.id);
|
const c = new DocumentController(params.id);
|
||||||
setController(c);
|
setController(c);
|
||||||
const res = await c.open();
|
const res = await c.open();
|
||||||
console.log(res)
|
console.log(res)
|
||||||
|
if (!res) return;
|
||||||
setDocumentData(res)
|
setDocumentData(res)
|
||||||
setDocumentId(params.id)
|
setDocumentId(params.id)
|
||||||
|
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
console.log('==== leave ====', params?.id)
|
console.log('==== leave ====', params?.id)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useDocument } from './DocumentPage.hooks';
|
import { useDocument } from './DocumentPage.hooks';
|
||||||
import { createTheme, ThemeProvider } from '@mui/material';
|
import { createTheme, ThemeProvider } from '@mui/material';
|
||||||
import Root from '../components/document/Root';
|
import Root from '../components/document/Root';
|
||||||
import { YDocControllerContext } from '../stores/effects/document/document_controller';
|
import { DocumentControllerContext } from '../stores/effects/document/document_controller';
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
typography: {
|
typography: {
|
||||||
@ -15,9 +15,9 @@ export const DocumentPage = () => {
|
|||||||
if (!documentId || !documentData || !controller) return null;
|
if (!documentId || !documentData || !controller) return null;
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<YDocControllerContext.Provider value={controller}>
|
<DocumentControllerContext.Provider value={controller}>
|
||||||
<Root documentData={documentData} />
|
<Root documentData={documentData} />
|
||||||
</YDocControllerContext.Provider>
|
</DocumentControllerContext.Provider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user