mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
refactor: document data and update
This commit is contained in:
parent
7781b912f0
commit
df66521f13
@ -20,6 +20,7 @@
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.12",
|
||||
"@reduxjs/toolkit": "^1.9.2",
|
||||
"@slate-yjs/core": "^0.3.1",
|
||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"events": "^3.3.0",
|
||||
@ -42,8 +43,9 @@
|
||||
"slate": "^0.91.4",
|
||||
"slate-react": "^0.91.9",
|
||||
"ts-results": "^3.3.0",
|
||||
"ulid": "^2.3.0",
|
||||
"utf8": "^3.0.0"
|
||||
"utf8": "^3.0.0",
|
||||
"yjs": "^13.5.51",
|
||||
"y-indexeddb": "^9.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.2.2",
|
||||
@ -53,6 +55,7 @@
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/utf8": "^3.0.1",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
@ -64,6 +67,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "^4.6.4",
|
||||
"uuid": "^9.0.0",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ specifiers:
|
||||
'@mui/icons-material': ^5.11.11
|
||||
'@mui/material': ^5.11.12
|
||||
'@reduxjs/toolkit': ^1.9.2
|
||||
'@slate-yjs/core': ^0.3.1
|
||||
'@tanstack/react-virtual': 3.0.0-beta.54
|
||||
'@tauri-apps/api': ^1.2.0
|
||||
'@tauri-apps/cli': ^1.2.2
|
||||
@ -15,6 +16,7 @@ specifiers:
|
||||
'@types/react': ^18.0.15
|
||||
'@types/react-dom': ^18.0.6
|
||||
'@types/utf8': ^3.0.1
|
||||
'@types/uuid': ^9.0.1
|
||||
'@typescript-eslint/eslint-plugin': ^5.51.0
|
||||
'@typescript-eslint/parser': ^5.51.0
|
||||
'@vitejs/plugin-react': ^3.0.0
|
||||
@ -31,6 +33,7 @@ specifiers:
|
||||
postcss: ^8.4.21
|
||||
prettier: 2.8.4
|
||||
prettier-plugin-tailwindcss: ^0.2.2
|
||||
protoc-gen-ts: ^0.8.5
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
react-error-boundary: ^3.1.4
|
||||
@ -45,9 +48,11 @@ specifiers:
|
||||
tailwindcss: ^3.2.7
|
||||
ts-results: ^3.3.0
|
||||
typescript: ^4.6.4
|
||||
ulid: ^2.3.0
|
||||
utf8: ^3.0.0
|
||||
uuid: ^9.0.0
|
||||
vite: ^4.0.0
|
||||
y-indexeddb: ^9.0.9
|
||||
yjs: ^13.5.51
|
||||
|
||||
dependencies:
|
||||
'@emotion/react': 11.10.6_pmekkgnqduwlme35zpnqhenc34
|
||||
@ -55,6 +60,7 @@ dependencies:
|
||||
'@mui/icons-material': 5.11.11_ao76n7r2cajsoyr3cbwrn7geoi
|
||||
'@mui/material': 5.11.12_xqeqsl5kvjjtyxwyi3jhw3yuli
|
||||
'@reduxjs/toolkit': 1.9.3_k4ae6lp43ej6mezo3ztvx6pykq
|
||||
'@slate-yjs/core': 0.3.1_slate@0.91.4+yjs@13.5.51
|
||||
'@tanstack/react-virtual': 3.0.0-beta.54_react@18.2.0
|
||||
'@tauri-apps/api': 1.2.0
|
||||
events: 3.3.0
|
||||
@ -64,6 +70,7 @@ dependencies:
|
||||
is-hotkey: 0.2.0
|
||||
jest: 29.5.0_@types+node@18.14.6
|
||||
nanoid: 4.0.1
|
||||
protoc-gen-ts: 0.8.6_ss7alqtodw6rv4lluxhr36xjoa
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
react-error-boundary: 3.1.4_react@18.2.0
|
||||
@ -76,8 +83,9 @@ dependencies:
|
||||
slate: 0.91.4
|
||||
slate-react: 0.91.9_6tgy34rvmll7duwkm4ydcekf3u
|
||||
ts-results: 3.3.0
|
||||
ulid: 2.3.0
|
||||
utf8: 3.0.0
|
||||
y-indexeddb: 9.0.9_yjs@13.5.51
|
||||
yjs: 13.5.51
|
||||
|
||||
devDependencies:
|
||||
'@tauri-apps/cli': 1.2.3
|
||||
@ -87,6 +95,7 @@ devDependencies:
|
||||
'@types/react': 18.0.28
|
||||
'@types/react-dom': 18.0.11
|
||||
'@types/utf8': 3.0.1
|
||||
'@types/uuid': 9.0.1
|
||||
'@typescript-eslint/eslint-plugin': 5.54.0_6mj2wypvdnknez7kws2nfdgupi
|
||||
'@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
|
||||
'@vitejs/plugin-react': 3.1.0_vite@4.1.4
|
||||
@ -98,6 +107,7 @@ devDependencies:
|
||||
prettier-plugin-tailwindcss: 0.2.4_prettier@2.8.4
|
||||
tailwindcss: 3.2.7_postcss@8.4.21
|
||||
typescript: 4.9.5
|
||||
uuid: 9.0.0
|
||||
vite: 4.1.4_@types+node@18.14.6
|
||||
|
||||
packages:
|
||||
@ -1308,6 +1318,17 @@ packages:
|
||||
'@sinonjs/commons': 2.0.0
|
||||
dev: false
|
||||
|
||||
/@slate-yjs/core/0.3.1_slate@0.91.4+yjs@13.5.51:
|
||||
resolution: {integrity: sha512-8nvS9m5FhMNONgydAfzwDCUhuoWbgzx5Bvw1/foSe+JO331UOT1xAKbUX5FzGCOunUcbRjMPXSdNyiPc0dodJg==}
|
||||
peerDependencies:
|
||||
slate: '>=0.70.0'
|
||||
yjs: ^13.5.29
|
||||
dependencies:
|
||||
slate: 0.91.4
|
||||
y-protocols: 1.0.5
|
||||
yjs: 13.5.51
|
||||
dev: false
|
||||
|
||||
/@tanstack/react-virtual/3.0.0-beta.54_react@18.2.0:
|
||||
resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==}
|
||||
peerDependencies:
|
||||
@ -1553,6 +1574,10 @@ packages:
|
||||
resolution: {integrity: sha512-1EkWuw7rT3BMz2HpmcEOr/HL61mWNA6Ulr/KdbXR9AI0A55wD4Qfv8hizd8Q1DnknSIzzDvQmvvY/guvX7jjZA==}
|
||||
dev: true
|
||||
|
||||
/@types/uuid/9.0.1:
|
||||
resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
|
||||
dev: true
|
||||
|
||||
/@types/yargs-parser/21.0.0:
|
||||
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
||||
dev: false
|
||||
@ -3050,6 +3075,10 @@ packages:
|
||||
/isexe/2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
/isomorphic.js/0.2.5:
|
||||
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
|
||||
dev: false
|
||||
|
||||
/istanbul-lib-coverage/3.2.0:
|
||||
resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==}
|
||||
engines: {node: '>=8'}
|
||||
@ -3575,6 +3604,14 @@ packages:
|
||||
type-check: 0.4.0
|
||||
dev: true
|
||||
|
||||
/lib0/0.2.73:
|
||||
resolution: {integrity: sha512-aJJIElCLWnHMcYZPtsM07QoSfHwpxCy4VUzBYGXFYEmh/h2QS5uZNbCCfL0CqnkOE30b7Tp9DVfjXag+3qzZjQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
isomorphic.js: 0.2.5
|
||||
dev: false
|
||||
|
||||
/lilconfig/2.1.0:
|
||||
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -4055,6 +4092,17 @@ packages:
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
/protoc-gen-ts/0.8.6_ss7alqtodw6rv4lluxhr36xjoa:
|
||||
resolution: {integrity: sha512-66oeorGy4QBvYjQGd/gaeOYyFqKyRmRgTpofmnw8buMG0P7A0jQjoKSvKJz5h5tNUaVkIzvGBUTRVGakrhhwpA==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
google-protobuf: ^3.13.0
|
||||
typescript: 4.x.x
|
||||
dependencies:
|
||||
google-protobuf: 3.21.2
|
||||
typescript: 4.9.5
|
||||
dev: false
|
||||
|
||||
/punycode/2.3.0:
|
||||
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||
engines: {node: '>=6'}
|
||||
@ -4678,12 +4726,6 @@ packages:
|
||||
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
|
||||
engines: {node: '>=4.2.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ulid/2.3.0:
|
||||
resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/unbox-primitive/1.0.2:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
@ -4726,6 +4768,11 @@ packages:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: true
|
||||
|
||||
/uuid/9.0.0:
|
||||
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/v8-to-istanbul/9.1.0:
|
||||
resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
@ -4839,6 +4886,21 @@ packages:
|
||||
engines: {node: '>=0.4'}
|
||||
dev: true
|
||||
|
||||
/y-indexeddb/9.0.9_yjs@13.5.51:
|
||||
resolution: {integrity: sha512-GcJbiJa2eD5hankj46Hea9z4hbDnDjvh1fT62E5SpZRsv8GcEemw34l1hwI2eknGcv5Ih9JfusT37JLx9q3LFg==}
|
||||
peerDependencies:
|
||||
yjs: ^13.0.0
|
||||
dependencies:
|
||||
lib0: 0.2.73
|
||||
yjs: 13.5.51
|
||||
dev: false
|
||||
|
||||
/y-protocols/1.0.5:
|
||||
resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==}
|
||||
dependencies:
|
||||
lib0: 0.2.73
|
||||
dev: false
|
||||
|
||||
/y18n/5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
@ -4872,6 +4934,13 @@ packages:
|
||||
yargs-parser: 21.1.1
|
||||
dev: false
|
||||
|
||||
/yjs/13.5.51:
|
||||
resolution: {integrity: sha512-F1Nb3z3TdandD80IAeQqgqy/2n9AhDLcXoBhZvCUX1dNVe0ef7fIwi6MjSYaGAYF2Ev8VcLcsGnmuGGOl7AWbw==}
|
||||
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
|
||||
dependencies:
|
||||
lib0: 0.2.73
|
||||
dev: false
|
||||
|
||||
/yocto-queue/0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import FormatButton from './FormatButton';
|
||||
import Portal from '../block/BlockPortal';
|
||||
import Portal from '../BlockPortal';
|
||||
import { TreeNode } from '$app/block_editor/view/tree_node';
|
||||
import { useHoveringToolbar } from './index.hooks';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import BlockComponent from '../BlockComponent';
|
||||
import { Slate, Editable } from 'slate-react';
|
||||
import Leaf from './Leaf';
|
||||
import HoveringToolbar from '$app/components/HoveringToolbar';
|
||||
import HoveringToolbar from '@/appflowy_app/components/block/HoveringToolbar';
|
||||
import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
|
||||
import { useTextBlock } from './index.hooks';
|
||||
import { BlockCommonProps, TextBlockToolbarProps } from '@/appflowy_app/interfaces';
|
||||
|
@ -0,0 +1,9 @@
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
const BlockPortal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => {
|
||||
const root = document.querySelectorAll(`[data-block-id=${blockId}] > .block-overlay`)[0];
|
||||
|
||||
return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null;
|
||||
};
|
||||
|
||||
export default BlockPortal;
|
@ -0,0 +1,7 @@
|
||||
import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
|
||||
export function useDocumentTitle(id: string) {
|
||||
const { node } = useSubscribeNode(id);
|
||||
return {
|
||||
node
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useDocumentTitle } from './DocumentTitle.hooks';
|
||||
import TextBlock from '../TextBlock';
|
||||
|
||||
export default function DocumentTitle({ id }: { id: string }) {
|
||||
const { node } = useDocumentTitle(id);
|
||||
if (!node) return null;
|
||||
|
||||
return (
|
||||
<div data-block-id={node.id} className='doc-title relative pt-[50px] text-4xl font-bold'>
|
||||
<TextBlock placeholder='Untitled' childIds={[]} node={node} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
import { command } from '$app/constants/toolbar';
|
||||
import FormatIcon from './FormatIcon';
|
||||
import { BaseEditor } from 'slate';
|
||||
|
||||
const FormatButton = ({ editor, format, icon }: { editor: BaseEditor; format: string; icon: string }) => {
|
||||
return (
|
||||
<Tooltip
|
||||
slotProps={{ tooltip: { style: { background: '#E0F8FF', borderRadius: 8 } } }}
|
||||
title={
|
||||
<div className='flex flex-col'>
|
||||
<span className='text-base font-medium text-black'>{command[format].title}</span>
|
||||
<span className='text-sm text-slate-400'>{command[format].key}</span>
|
||||
</div>
|
||||
}
|
||||
placement='top-start'
|
||||
>
|
||||
<IconButton
|
||||
size='small'
|
||||
sx={{ color: isFormatActive(editor, format) ? '#00BCF0' : 'white' }}
|
||||
onClick={() => toggleFormat(editor, format)}
|
||||
>
|
||||
<FormatIcon icon={icon} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormatButton;
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material';
|
||||
import { iconSize } from '$app/constants/toolbar';
|
||||
|
||||
export default function FormatIcon({ icon }: { icon: string }) {
|
||||
switch (icon) {
|
||||
case 'bold':
|
||||
return <FormatBold sx={iconSize} />;
|
||||
case 'underlined':
|
||||
return <FormatUnderlined sx={iconSize} />;
|
||||
case 'italic':
|
||||
return <FormatItalic sx={iconSize} />;
|
||||
case 'code':
|
||||
return <CodeOutlined sx={iconSize} />;
|
||||
case 'strikethrough':
|
||||
return <StrikethroughSOutlined sx={iconSize} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useFocused, useSlate } from 'slate-react';
|
||||
import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar';
|
||||
|
||||
|
||||
export function useHoveringToolbar(id: string) {
|
||||
const editor = useSlate();
|
||||
const inFocus = useFocused();
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const nodeRect = document.querySelector(`[data-block-id=${id}]`)?.getBoundingClientRect();
|
||||
|
||||
if (!nodeRect) return;
|
||||
const position = calcToolbarPosition(editor, el, nodeRect);
|
||||
|
||||
if (!position) {
|
||||
el.style.opacity = '0';
|
||||
el.style.zIndex = '-1';
|
||||
} else {
|
||||
el.style.opacity = '1';
|
||||
el.style.zIndex = '1';
|
||||
el.style.top = position.top;
|
||||
el.style.left = position.left;
|
||||
}
|
||||
});
|
||||
return {
|
||||
ref,
|
||||
inFocus,
|
||||
editor
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import FormatButton from './FormatButton';
|
||||
import Portal from '../BlockPortal';
|
||||
import { useHoveringToolbar } from './index.hooks';
|
||||
|
||||
const HoveringToolbar = ({ id }: { id: string }) => {
|
||||
const { inFocus, ref, editor } = useHoveringToolbar(id);
|
||||
if (!inFocus) return null;
|
||||
|
||||
return (
|
||||
<Portal blockId={id}>
|
||||
<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
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{['bold', 'italic', 'underlined', 'strikethrough', 'code'].map((format) => (
|
||||
<FormatButton key={format} editor={editor} format={format} icon={format} />
|
||||
))}
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default HoveringToolbar;
|
@ -0,0 +1,11 @@
|
||||
|
||||
import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
|
||||
|
||||
export function useNode(id: string) {
|
||||
const { node, childIds } = useSubscribeNode(id);
|
||||
|
||||
return {
|
||||
node,
|
||||
childIds,
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useNode } from './Node.hooks';
|
||||
import { withErrorBoundary } from 'react-error-boundary';
|
||||
import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
|
||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||
import TextBlock from '../TextBlock';
|
||||
|
||||
function NodeComponent({ id }: { id: string }) {
|
||||
const { node, childIds } = useNode(id);
|
||||
|
||||
const renderBlock = useCallback((props: { node: Node; childIds?: string[] }) => {
|
||||
switch (props.node.type) {
|
||||
case 'text':
|
||||
return <TextBlock {...props} />;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!node) return null;
|
||||
|
||||
return (
|
||||
<div data-block-id={node.id} className='relative my-[1px]'>
|
||||
{renderBlock({
|
||||
node,
|
||||
childIds,
|
||||
})}
|
||||
<div className='block-overlay' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, {
|
||||
FallbackComponent: ErrorBoundaryFallbackComponent,
|
||||
});
|
||||
|
||||
export default React.memo(NodeWithErrorBoundary);
|
@ -0,0 +1,16 @@
|
||||
import { DocumentData } from '$app/interfaces/document';
|
||||
import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
|
||||
import { useParseTree } from './Tree.hooks';
|
||||
|
||||
export function useRoot({ documentData }: { documentData: DocumentData }) {
|
||||
const { rootId } = documentData;
|
||||
|
||||
useParseTree(documentData);
|
||||
|
||||
const { node: rootNode, childIds: rootChildIds } = useSubscribeNode(rootId);
|
||||
|
||||
return {
|
||||
node: rootNode,
|
||||
childIds: rootChildIds,
|
||||
};
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import { useEffect } from 'react';
|
||||
import { DocumentData, NestedBlock } from '$app/interfaces/document';
|
||||
import { useAppDispatch } from '@/appflowy_app/stores/store';
|
||||
import { documentActions, Node } from '$app/stores/reducers/document/slice';
|
||||
|
||||
export function useParseTree(documentData: DocumentData) {
|
||||
const dispatch = useAppDispatch();
|
||||
const { blocks, ytexts, yarrays, rootId } = documentData;
|
||||
const flattenNestedBlocks = (
|
||||
block: NestedBlock
|
||||
): (Node & {
|
||||
children: string[];
|
||||
})[] => {
|
||||
const node: Node & {
|
||||
children: string[];
|
||||
} = {
|
||||
id: block.id,
|
||||
delta: ytexts[block.data.text],
|
||||
data: block.data,
|
||||
type: block.type,
|
||||
parent: block.parent,
|
||||
children: yarrays[block.children],
|
||||
};
|
||||
|
||||
const nodes = [node];
|
||||
node.children.forEach((child) => {
|
||||
const childBlock = blocks[child];
|
||||
nodes.push(...flattenNestedBlocks(childBlock));
|
||||
});
|
||||
return nodes;
|
||||
};
|
||||
|
||||
const initializeNodeHierarchy = (parentId: string, children: string[]) => {
|
||||
children.forEach((childId) => {
|
||||
dispatch(documentActions.addChild({ parentId, childId }));
|
||||
const child = blocks[childId];
|
||||
initializeNodeHierarchy(childId, yarrays[child.children]);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const root = documentData.blocks[rootId];
|
||||
|
||||
const initialNodes = flattenNestedBlocks(root);
|
||||
|
||||
initialNodes.forEach((node) => {
|
||||
const _node = {
|
||||
id: node.id,
|
||||
parent: node.parent,
|
||||
data: node.data,
|
||||
type: node.type,
|
||||
delta: node.delta,
|
||||
};
|
||||
dispatch(documentActions.addNode(_node));
|
||||
});
|
||||
|
||||
initializeNodeHierarchy(rootId, yarrays[root.children]);
|
||||
|
||||
return () => {
|
||||
dispatch(documentActions.clear());
|
||||
};
|
||||
}, [documentData]);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { DocumentData } from '@/appflowy_app/interfaces/document';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useRoot } from './Root.hooks';
|
||||
import Node from '../Node';
|
||||
import { withErrorBoundary } from 'react-error-boundary';
|
||||
import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
|
||||
import VirtualizerList from '../VirtualizerList';
|
||||
import { Skeleton } from '@mui/material';
|
||||
|
||||
function Root({ documentData }: { documentData: DocumentData }) {
|
||||
const { node, childIds } = useRoot({ documentData });
|
||||
|
||||
const renderNode = useCallback((nodeId: string) => {
|
||||
return <Node key={nodeId} id={nodeId} />;
|
||||
}, []);
|
||||
|
||||
if (!node || !childIds) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='appflowy-block-doc' className='h-[100%] overflow-hidden'>
|
||||
<VirtualizerList node={node} childIds={childIds} renderNode={renderNode} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const RootWithErrorBoundary = withErrorBoundary(Root, {
|
||||
FallbackComponent: ErrorBoundaryFallbackComponent,
|
||||
});
|
||||
|
||||
export default React.memo(RootWithErrorBoundary);
|
@ -0,0 +1,62 @@
|
||||
|
||||
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { createEditor } from "slate";
|
||||
import { withReact } from "slate-react";
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
|
||||
import { Delta } from '@slate-yjs/core/dist/model/types';
|
||||
import { TextDelta } from '@/appflowy_app/interfaces/document';
|
||||
|
||||
const initialValue = [{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }],
|
||||
}];
|
||||
|
||||
export function useBindYjs(delta: TextDelta[], update: (_delta: Delta) => void) {
|
||||
const yTextRef = useRef<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) => {
|
||||
console.log(event.delta, event.target.toDelta());
|
||||
update(event.delta as Delta);
|
||||
}
|
||||
yText.applyDelta(delta);
|
||||
yText.observe(textEventHandler);
|
||||
|
||||
return () => {
|
||||
yText.unobserve(textEventHandler);
|
||||
}
|
||||
}, [delta])
|
||||
|
||||
|
||||
return { editor }
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { BaseText } from 'slate';
|
||||
import { RenderLeafProps } from 'slate-react';
|
||||
|
||||
const Leaf = ({
|
||||
attributes,
|
||||
children,
|
||||
leaf,
|
||||
}: RenderLeafProps & {
|
||||
leaf: BaseText & {
|
||||
bold?: boolean;
|
||||
code?: boolean;
|
||||
italic?: boolean;
|
||||
underlined?: boolean;
|
||||
strikethrough?: boolean;
|
||||
};
|
||||
}) => {
|
||||
let newChildren = children;
|
||||
if (leaf.bold) {
|
||||
newChildren = <strong>{children}</strong>;
|
||||
}
|
||||
|
||||
if (leaf.code) {
|
||||
newChildren = <code className='rounded-sm bg-[#F2FCFF] p-1'>{newChildren}</code>;
|
||||
}
|
||||
|
||||
if (leaf.italic) {
|
||||
newChildren = <em>{newChildren}</em>;
|
||||
}
|
||||
|
||||
if (leaf.underlined) {
|
||||
newChildren = <u>{newChildren}</u>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span {...attributes} className={leaf.strikethrough ? `line-through` : ''}>
|
||||
{newChildren}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Leaf;
|
@ -0,0 +1,75 @@
|
||||
import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey";
|
||||
import { useCallback, useContext, useState } from "react";
|
||||
import { Descendant, Range } from "slate";
|
||||
import { useBindYjs } from "./BindYjs.hooks";
|
||||
import { YDocControllerContext } from '../../../stores/effects/document/document_controller';
|
||||
import { Delta } from "@slate-yjs/core/dist/model/types";
|
||||
import { TextDelta } from '../../../interfaces/document';
|
||||
|
||||
function useController(textId: string) {
|
||||
const docController = useContext(YDocControllerContext);
|
||||
|
||||
const update = useCallback(
|
||||
(delta: Delta) => {
|
||||
docController?.yTextApply(textId, delta)
|
||||
},
|
||||
[textId],
|
||||
)
|
||||
|
||||
return {
|
||||
update
|
||||
}
|
||||
}
|
||||
|
||||
export function useTextBlock(text: string, delta: TextDelta[]) {
|
||||
const { update } = useController(text);
|
||||
const { editor } = useBindYjs(delta, update);
|
||||
const [value, setValue] = useState<Descendant[]>([]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: Descendant[]) => {
|
||||
setValue(e);
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
switch (event.key) {
|
||||
case 'Enter': {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
return;
|
||||
}
|
||||
case 'Backspace': {
|
||||
if (!editor.selection) return;
|
||||
const { anchor } = editor.selection;
|
||||
const isCollapase = Range.isCollapsed(editor.selection);
|
||||
if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
triggerHotkey(event, editor);
|
||||
}
|
||||
|
||||
const onDOMBeforeInput = useCallback((e: InputEvent) => {
|
||||
// COMPAT: in Apple, `compositionend` is dispatched after the
|
||||
// `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese.
|
||||
// Here, prevent the beforeInput event and wait for the compositionend event to take effect
|
||||
if (e.inputType === 'insertFromComposition') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
}, []);
|
||||
|
||||
return {
|
||||
onChange,
|
||||
onKeyDownCapture,
|
||||
onDOMBeforeInput,
|
||||
editor,
|
||||
value
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { Slate, Editable } from 'slate-react';
|
||||
import Leaf from './Leaf';
|
||||
import { useTextBlock } from './TextBlock.hooks';
|
||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||
import NodeComponent from '../Node';
|
||||
import HoveringToolbar from '../HoveringToolbar';
|
||||
|
||||
export default function TextBlock({
|
||||
node,
|
||||
childIds,
|
||||
placeholder,
|
||||
...props
|
||||
}: {
|
||||
node: Node;
|
||||
childIds?: string[];
|
||||
placeholder?: string;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, node.delta);
|
||||
|
||||
return (
|
||||
<div {...props} className={props.className}>
|
||||
<Slate editor={editor} onChange={onChange} value={value}>
|
||||
<HoveringToolbar id={node.id} />
|
||||
<Editable
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onDOMBeforeInput={onDOMBeforeInput}
|
||||
renderLeaf={(leafProps) => <Leaf {...leafProps} />}
|
||||
placeholder={placeholder || 'Please enter some text...'}
|
||||
/>
|
||||
</Slate>
|
||||
{childIds && childIds.length > 0 ? (
|
||||
<div className='pl-[1.5em]'>
|
||||
{childIds.map((item) => (
|
||||
<NodeComponent key={item} id={item} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useRef } from 'react';
|
||||
|
||||
const defaultSize = 60;
|
||||
|
||||
export function useVirtualizerList(count: number) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => {
|
||||
return defaultSize;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
rowVirtualizer,
|
||||
parentRef,
|
||||
};
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { useVirtualizerList } from './VirtualizerList.hooks';
|
||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||
import DocumentTitle from '../DocumentTitle';
|
||||
|
||||
export default function VirtualizerList({
|
||||
childIds,
|
||||
node,
|
||||
renderNode,
|
||||
}: {
|
||||
childIds: string[];
|
||||
node: Node;
|
||||
renderNode: (nodeId: string) => JSX.Element;
|
||||
}) {
|
||||
const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length);
|
||||
|
||||
const virtualItems = rowVirtualizer.getVirtualItems();
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className={`doc-scroller-container flex h-[100%] flex-wrap justify-center overflow-auto px-20`}>
|
||||
<div
|
||||
className='doc-body max-w-screen w-[900px] min-w-0'
|
||||
style={{
|
||||
height: rowVirtualizer.getTotalSize(),
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{node && childIds && virtualItems.length ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${virtualItems[0].start || 0}px)`,
|
||||
}}
|
||||
>
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const id = childIds[virtualRow.index];
|
||||
return (
|
||||
<div className='p-[1px]' key={id} data-index={virtualRow.index} ref={rowVirtualizer.measureElement}>
|
||||
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
|
||||
{renderNode(id)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { Alert } from '@mui/material';
|
||||
import { FallbackProps } from 'react-error-boundary';
|
||||
|
||||
export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
||||
<Alert severity='error' className='mb-2'>
|
||||
<p>Something went wrong:</p>
|
||||
<pre>{error.message}</pre>
|
||||
<button onClick={resetErrorBoundary}>Try again</button>
|
||||
</Alert>
|
||||
);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { Node } from '@/appflowy_app/stores/reducers/document/slice';
|
||||
import { useAppSelector } from '@/appflowy_app/stores/store';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useSubscribeNode(id: string) {
|
||||
const node = useAppSelector<Node | undefined>(state => state.document.nodes[id]);
|
||||
const childIds = useAppSelector<string[] | undefined>(state => state.document.children[id]);
|
||||
|
||||
const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.type]);
|
||||
const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]);
|
||||
|
||||
return {
|
||||
node: memoizedNode,
|
||||
childIds: memoizedChildIds
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import { useError } from '../../error/Error.hooks';
|
||||
import { AppObserver } from '../../../stores/effects/folder/app/app_observer';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||
import { YDocController } from '$app/stores/effects/document/document_controller';
|
||||
|
||||
export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
const appDispatch = useAppDispatch();
|
||||
@ -132,6 +133,10 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
layoutType: ViewLayoutTypePB.Document,
|
||||
});
|
||||
|
||||
// temp: let me try it by yjs
|
||||
const ydocController = new YDocController(newView.id);
|
||||
await ydocController.createDocument();
|
||||
|
||||
appDispatch(
|
||||
pagesActions.addPage({
|
||||
folderId: folder.id,
|
||||
|
@ -0,0 +1,31 @@
|
||||
// eslint-disable-next-line no-shadow
|
||||
export enum BlockType {
|
||||
PageBlock = 'page',
|
||||
HeadingBlock = 'heading',
|
||||
ListBlock = 'list',
|
||||
TextBlock = 'text',
|
||||
CodeBlock = 'code',
|
||||
EmbedBlock = 'embed',
|
||||
QuoteBlock = 'quote',
|
||||
DividerBlock = 'divider',
|
||||
MediaBlock = 'media',
|
||||
TableBlock = 'table',
|
||||
ColumnBlock = 'column'
|
||||
}
|
||||
export interface NestedBlock {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
data: Record<string, any>;
|
||||
parent: string | null;
|
||||
children: string;
|
||||
}
|
||||
export interface TextDelta {
|
||||
insert: string;
|
||||
attributes?: Record<string, string | boolean>;
|
||||
}
|
||||
export interface DocumentData {
|
||||
rootId: string;
|
||||
blocks: Record<string, NestedBlock>;
|
||||
ytexts: Record<string, TextDelta[]>;
|
||||
yarrays: Record<string, string[]>;
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
import * as Y from 'yjs';
|
||||
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||
import { v4 } from 'uuid';
|
||||
import { DocumentData } from '@/appflowy_app/interfaces/document';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type DeltaAttributes = {
|
||||
retain: number;
|
||||
attributes: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DeltaRetain = { retain: number };
|
||||
export type DeltaDelete = { delete: number };
|
||||
export type DeltaInsert = {
|
||||
insert: string | Y.XmlText;
|
||||
attributes?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type InsertDelta = Array<DeltaInsert>;
|
||||
export type Delta = Array<
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
ydoc.on('updateV2', (update) => {
|
||||
console.log('======', update);
|
||||
})
|
||||
const blocks = ydoc.getMap('blocks');
|
||||
const obj: DocumentData = {
|
||||
rootId: ydoc.getArray<string>('root').toArray()[0] || '',
|
||||
blocks: blocks.toJSON(),
|
||||
ytexts: {},
|
||||
yarrays: {}
|
||||
};
|
||||
Object.keys(obj.blocks).forEach(key => {
|
||||
const value = obj.blocks[key];
|
||||
if (value.children) {
|
||||
Object.assign(obj.yarrays, {
|
||||
[value.children]: ydoc.getArray(value.children).toArray()
|
||||
});
|
||||
}
|
||||
if (value.data.text) {
|
||||
Object.assign(obj.ytexts, {
|
||||
[value.data.text]: ydoc.getText(value.data.text).toDelta()
|
||||
})
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
yTextApply = (yTextId: string, delta: Delta) => {
|
||||
console.log("====", yTextId, delta);
|
||||
const ydoc = this._ydoc;
|
||||
const ytext = ydoc.getText(yTextId);
|
||||
ytext.applyDelta(delta);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document";
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
parent: string | null;
|
||||
type: BlockType;
|
||||
selected?: boolean;
|
||||
delta: TextDelta[];
|
||||
data: {
|
||||
text?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type NodeState = {
|
||||
nodes: Record<string, Node>;
|
||||
children: Record<string, string[]>;
|
||||
};
|
||||
const initialState: NodeState = {
|
||||
nodes: {},
|
||||
children: {},
|
||||
};
|
||||
|
||||
export const documentSlice = createSlice({
|
||||
name: 'document',
|
||||
initialState: initialState,
|
||||
reducers: {
|
||||
clear: (state, action: PayloadAction) => {
|
||||
return initialState;
|
||||
},
|
||||
addNode: (state, action: PayloadAction<Node>) => {
|
||||
state.nodes[action.payload.id] = action.payload;
|
||||
},
|
||||
addChild: (state, action: PayloadAction<{ parentId: string, childId: string }>) => {
|
||||
const children = state.children[action.payload.parentId];
|
||||
if (children) {
|
||||
children.push(action.payload.childId);
|
||||
} else {
|
||||
state.children[action.payload.parentId] = [action.payload.childId]
|
||||
}
|
||||
},
|
||||
|
||||
updateNode: (state, action: PayloadAction<{id: string; parent?: string; type?: BlockType; data?: any }>) => {
|
||||
state.nodes[action.payload.id] = {
|
||||
...state.nodes[action.payload.id],
|
||||
...action.payload
|
||||
}
|
||||
},
|
||||
|
||||
removeNode: (state, action: PayloadAction<string>) => {
|
||||
const parentId = state.nodes[action.payload].parent;
|
||||
delete state.nodes[action.payload];
|
||||
if (parentId) {
|
||||
const index = state.children[parentId].indexOf(action.payload);
|
||||
if (index > -1) {
|
||||
state.children[parentId].splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const documentActions = documentSlice.actions;
|
@ -14,6 +14,7 @@ import { currentUserSlice } from './reducers/current-user/slice';
|
||||
import { gridSlice } from './reducers/grid/slice';
|
||||
import { workspaceSlice } from './reducers/workspace/slice';
|
||||
import { databaseSlice } from './reducers/database/slice';
|
||||
import { documentSlice } from './reducers/document/slice';
|
||||
import { boardSlice } from './reducers/board/slice';
|
||||
import { errorSlice } from './reducers/error/slice';
|
||||
import { activePageIdSlice } from './reducers/activePageId/slice';
|
||||
@ -32,6 +33,7 @@ const store = configureStore({
|
||||
[gridSlice.name]: gridSlice.reducer,
|
||||
[databaseSlice.name]: databaseSlice.reducer,
|
||||
[boardSlice.name]: boardSlice.reducer,
|
||||
[documentSlice.name]: documentSlice.reducer,
|
||||
[workspaceSlice.name]: workspaceSlice.reducer,
|
||||
[errorSlice.name]: errorSlice.reducer,
|
||||
},
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { createContext } from 'react';
|
||||
import { ulid } from "ulid";
|
||||
import { BlockEditor } from '../block_editor/index';
|
||||
import { BlockType } from '../interfaces';
|
||||
|
||||
export const BlockContext = createContext<{
|
||||
id?: string;
|
||||
@ -23,3 +24,11 @@ export function calculateViewportBlockMaxCount() {
|
||||
}
|
||||
|
||||
|
||||
export interface NestedNode {
|
||||
id: string;
|
||||
children: string;
|
||||
parent: string | null;
|
||||
type: BlockType;
|
||||
data: any;
|
||||
}
|
||||
|
||||
|
@ -49,3 +49,40 @@ export function set(obj: any, path: string[], value: any): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isEqual<T>(value1: T, value2: T): boolean {
|
||||
if (typeof value1 !== 'object' || value1 === null || typeof value2 !== 'object' || value2 === null) {
|
||||
return value1 === value2;
|
||||
}
|
||||
|
||||
|
||||
if (Array.isArray(value1)) {
|
||||
if (!Array.isArray(value2) || value1.length !== value2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < value1.length; i++) {
|
||||
if (!isEqual(value1[i], value2[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(value1);
|
||||
const keys2 = Object.keys(value2);
|
||||
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key of keys1) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
if (!isEqual(value1[key], value2[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -4,796 +4,30 @@ import {
|
||||
DocumentVersionPB,
|
||||
OpenDocumentPayloadPB,
|
||||
} from '../../services/backend/events/flowy-document';
|
||||
import { BlockInterface, BlockType } from '../interfaces';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { BlockEditor } from '../block_editor';
|
||||
import { DocumentData } from '../interfaces/document';
|
||||
import { YDocController } from '$app/stores/effects/document/document_controller';
|
||||
|
||||
const loadBlockData = async (id: string): Promise<Record<string, BlockInterface>> => {
|
||||
return {
|
||||
[id]: {
|
||||
id: id,
|
||||
type: BlockType.PageBlock,
|
||||
data: { content: [{ text: 'Document Title' }] },
|
||||
next: null,
|
||||
firstChild: "L1-1",
|
||||
},
|
||||
"L1-1": {
|
||||
id: "L1-1",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 1, content: [{ text: 'Heading 1' }] },
|
||||
next: "L1-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-2": {
|
||||
id: "L1-2",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 2, content: [{ text: 'Heading 2' }] },
|
||||
next: "L1-3",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-3": {
|
||||
id: "L1-3",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 3, content: [{ text: 'Heading 3' }] },
|
||||
next: "L1-4",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-4": {
|
||||
id: "L1-4",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{
|
||||
text:
|
||||
'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{ text: ', ' },
|
||||
{ text: 'italic', italic: true },
|
||||
{ text: ', or anything else you might want to do!' },
|
||||
] },
|
||||
next: "L1-5",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-5": {
|
||||
id: "L1-5",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
{ text: 'select any piece of text and the menu will appear', bold: true },
|
||||
{ text: '.' },
|
||||
] },
|
||||
next: "L1-6",
|
||||
firstChild: "L1-5-1",
|
||||
},
|
||||
"L1-5-1": {
|
||||
id: "L1-5-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L1-5-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-5-2": {
|
||||
id: "L1-5-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-6": {
|
||||
id: "L1-6",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'bulleted', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{
|
||||
text:
|
||||
', or add a semantically rendered block quote in the middle of the page, like this:',
|
||||
},
|
||||
] },
|
||||
next: "L1-7",
|
||||
firstChild: "L1-6-1",
|
||||
},
|
||||
"L1-6-1": {
|
||||
id: "L1-6-1",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'numbered', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
|
||||
] },
|
||||
|
||||
next: "L1-6-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-6-2": {
|
||||
id: "L1-6-2",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'numbered', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
|
||||
] },
|
||||
|
||||
next: "L1-6-3",
|
||||
firstChild: null,
|
||||
},
|
||||
|
||||
"L1-6-3": {
|
||||
id: "L1-6-3",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [{ text: 'A wise quote.' }] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
|
||||
"L1-7": {
|
||||
id: "L1-7",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'column' },
|
||||
|
||||
next: "L1-8",
|
||||
firstChild: "L1-7-1",
|
||||
},
|
||||
"L1-7-1": {
|
||||
id: "L1-7-1",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: "L1-7-2",
|
||||
firstChild: "L1-7-1-1",
|
||||
},
|
||||
"L1-7-1-1": {
|
||||
id: "L1-7-1-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-7-2": {
|
||||
id: "L1-7-2",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: "L1-7-3",
|
||||
firstChild: "L1-7-2-1",
|
||||
},
|
||||
"L1-7-2-1": {
|
||||
id: "L1-7-2-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L1-7-2-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-7-2-2": {
|
||||
id: "L1-7-2-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-7-3": {
|
||||
id: "L1-7-3",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: null,
|
||||
firstChild: "L1-7-3-1",
|
||||
},
|
||||
"L1-7-3-1": {
|
||||
id: "L1-7-3-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-8": {
|
||||
id: "L1-8",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 1, content: [{ text: 'Heading 1' }] },
|
||||
next: "L1-9",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-9": {
|
||||
id: "L1-9",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 2, content: [{ text: 'Heading 2' }] },
|
||||
next: "L1-10",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-10": {
|
||||
id: "L1-10",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 3, content: [{ text: 'Heading 3' }] },
|
||||
next: "L1-11",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-11": {
|
||||
id: "L1-11",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{
|
||||
text:
|
||||
'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{ text: ', ' },
|
||||
{ text: 'italic', italic: true },
|
||||
{ text: ', or anything else you might want to do!' },
|
||||
] },
|
||||
next: "L1-12",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-12": {
|
||||
id: "L1-12",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
{ text: 'select any piece of text and the menu will appear', bold: true },
|
||||
{ text: '.' },
|
||||
] },
|
||||
next: "L2-1",
|
||||
firstChild: "L1-12-1",
|
||||
},
|
||||
"L1-12-1": {
|
||||
id: "L1-12-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L1-12-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L1-12-2": {
|
||||
id: "L1-12-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-1": {
|
||||
id: "L2-1",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 1, content: [{ text: 'Heading 1' }] },
|
||||
next: "L2-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-2": {
|
||||
id: "L2-2",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 2, content: [{ text: 'Heading 2' }] },
|
||||
next: "L2-3",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-3": {
|
||||
id: "L2-3",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 3, content: [{ text: 'Heading 3' }] },
|
||||
next: "L2-4",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-4": {
|
||||
id: "L2-4",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{
|
||||
text:
|
||||
'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{ text: ', ' },
|
||||
{ text: 'italic', italic: true },
|
||||
{ text: ', or anything else you might want to do!' },
|
||||
] },
|
||||
next: "L2-5",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-5": {
|
||||
id: "L2-5",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
{ text: 'select any piece of text and the menu will appear', bold: true },
|
||||
{ text: '.' },
|
||||
] },
|
||||
next: "L2-6",
|
||||
firstChild: "L2-5-1",
|
||||
},
|
||||
"L2-5-1": {
|
||||
id: "L2-5-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L2-5-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-5-2": {
|
||||
id: "L2-5-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-6": {
|
||||
id: "L2-6",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'bulleted', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{
|
||||
text:
|
||||
', or add a semantically rendered block quote in the middle of the page, like this:',
|
||||
},
|
||||
] },
|
||||
next: "L2-7",
|
||||
firstChild: "L2-6-1",
|
||||
},
|
||||
"L2-6-1": {
|
||||
id: "L2-6-1",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'numbered', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
|
||||
] },
|
||||
|
||||
next: "L2-6-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-6-2": {
|
||||
id: "L2-6-2",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'numbered', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
|
||||
] },
|
||||
|
||||
next: "L2-6-3",
|
||||
firstChild: null,
|
||||
},
|
||||
|
||||
"L2-6-3": {
|
||||
id: "L2-6-3",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [{ text: 'A wise quote.' }] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
|
||||
"L2-7": {
|
||||
id: "L2-7",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'column' },
|
||||
|
||||
next: "L2-8",
|
||||
firstChild: "L2-7-1",
|
||||
},
|
||||
"L2-7-1": {
|
||||
id: "L2-7-1",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: "L2-7-2",
|
||||
firstChild: "L2-7-1-1",
|
||||
},
|
||||
"L2-7-1-1": {
|
||||
id: "L2-7-1-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-7-2": {
|
||||
id: "L2-7-2",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: "L2-7-3",
|
||||
firstChild: "L2-7-2-1",
|
||||
},
|
||||
"L2-7-2-1": {
|
||||
id: "L2-7-2-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L2-7-2-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-7-2-2": {
|
||||
id: "L2-7-2-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-7-3": {
|
||||
id: "L2-7-3",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: null,
|
||||
firstChild: "L2-7-3-1",
|
||||
},
|
||||
"L2-7-3-1": {
|
||||
id: "L2-7-3-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-8": {
|
||||
id: "L2-8",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 1, content: [{ text: 'Heading 1' }] },
|
||||
next: "L2-9",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-9": {
|
||||
id: "L2-9",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 2, content: [{ text: 'Heading 2' }] },
|
||||
next: "L2-10",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-10": {
|
||||
id: "L2-10",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 3, content: [{ text: 'Heading 3' }] },
|
||||
next: "L2-11",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-11": {
|
||||
id: "L2-11",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{
|
||||
text:
|
||||
'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{ text: ', ' },
|
||||
{ text: 'italic', italic: true },
|
||||
{ text: ', or anything else you might want to do!' },
|
||||
] },
|
||||
next: "L2-12",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-12": {
|
||||
id: "L2-12",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
{ text: 'select any piece of text and the menu will appear', bold: true },
|
||||
{ text: '.' },
|
||||
] },
|
||||
next: "L3-1",
|
||||
firstChild: "L2-12-1",
|
||||
},
|
||||
"L2-12-1": {
|
||||
id: "L2-12-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L2-12-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L2-12-2": {
|
||||
id: "L2-12-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},"L3-1": {
|
||||
id: "L3-1",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 1, content: [{ text: 'Heading 1' }] },
|
||||
next: "L3-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-2": {
|
||||
id: "L3-2",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 2, content: [{ text: 'Heading 2' }] },
|
||||
next: "L3-3",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-3": {
|
||||
id: "L3-3",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 3, content: [{ text: 'Heading 3' }] },
|
||||
next: "L3-4",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-4": {
|
||||
id: "L3-4",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{
|
||||
text:
|
||||
'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{ text: ', ' },
|
||||
{ text: 'italic', italic: true },
|
||||
{ text: ', or anything else you might want to do!' },
|
||||
] },
|
||||
next: "L3-5",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-5": {
|
||||
id: "L3-5",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
{ text: 'select any piece of text and the menu will appear', bold: true },
|
||||
{ text: '.' },
|
||||
] },
|
||||
next: "L3-6",
|
||||
firstChild: "L3-5-1",
|
||||
},
|
||||
"L3-5-1": {
|
||||
id: "L3-5-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L3-5-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-5-2": {
|
||||
id: "L3-5-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-6": {
|
||||
id: "L3-6",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'bulleted', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{
|
||||
text:
|
||||
', or add a semantically rendered block quote in the middle of the page, like this:',
|
||||
},
|
||||
] },
|
||||
next: "L3-7",
|
||||
firstChild: "L3-6-1",
|
||||
},
|
||||
"L3-6-1": {
|
||||
id: "L3-6-1",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'numbered', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
|
||||
] },
|
||||
|
||||
next: "L3-6-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-6-2": {
|
||||
id: "L3-6-2",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'numbered', content: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
|
||||
] },
|
||||
|
||||
next: "L3-6-3",
|
||||
firstChild: null,
|
||||
},
|
||||
|
||||
"L3-6-3": {
|
||||
id: "L3-6-3",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [{ text: 'A wise quote.' }] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
|
||||
"L3-7": {
|
||||
id: "L3-7",
|
||||
type: BlockType.ListBlock,
|
||||
data: { type: 'column' },
|
||||
|
||||
next: "L3-8",
|
||||
firstChild: "L3-7-1",
|
||||
},
|
||||
"L3-7-1": {
|
||||
id: "L3-7-1",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: "L3-7-2",
|
||||
firstChild: "L3-7-1-1",
|
||||
},
|
||||
"L3-7-1-1": {
|
||||
id: "L3-7-1-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-7-2": {
|
||||
id: "L3-7-2",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: "L3-7-3",
|
||||
firstChild: "L3-7-2-1",
|
||||
},
|
||||
"L3-7-2-1": {
|
||||
id: "L3-7-2-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L3-7-2-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-7-2-2": {
|
||||
id: "L3-7-2-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-7-3": {
|
||||
id: "L3-7-3",
|
||||
type: BlockType.ColumnBlock,
|
||||
data: { ratio: '0.33' },
|
||||
next: null,
|
||||
firstChild: "L3-7-3-1",
|
||||
},
|
||||
"L3-7-3-1": {
|
||||
id: "L3-7-3-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-8": {
|
||||
id: "L3-8",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 1, content: [{ text: 'Heading 1' }] },
|
||||
next: "L3-9",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-9": {
|
||||
id: "L3-9",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 2, content: [{ text: 'Heading 2' }] },
|
||||
next: "L3-10",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-10": {
|
||||
id: "L3-10",
|
||||
type: BlockType.HeadingBlock,
|
||||
data: { level: 3, content: [{ text: 'Heading 3' }] },
|
||||
next: "L3-11",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-11": {
|
||||
id: "L3-11",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{
|
||||
text:
|
||||
'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{ text: ', ' },
|
||||
{ text: 'italic', italic: true },
|
||||
{ text: ', or anything else you might want to do!' },
|
||||
] },
|
||||
next: "L3-12",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-12": {
|
||||
id: "L3-12",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
{ text: 'select any piece of text and the menu will appear', bold: true },
|
||||
{ text: '.' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: "L3-12-1",
|
||||
},
|
||||
"L3-12-1": {
|
||||
id: "L3-12-1",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: "L3-12-2",
|
||||
firstChild: null,
|
||||
},
|
||||
"L3-12-2": {
|
||||
id: "L3-12-2",
|
||||
type: BlockType.TextBlock,
|
||||
data: { content: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
] },
|
||||
next: null,
|
||||
firstChild: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const useDocument = () => {
|
||||
const params = useParams();
|
||||
const [blockId, setBlockId] = useState<string>();
|
||||
const blockEditorRef = useRef<BlockEditor | null>(null)
|
||||
|
||||
const [ documentId, setDocumentId ] = useState<string>();
|
||||
const [ documentData, setDocumentData ] = useState<DocumentData>();
|
||||
const [ controller, setController ] = useState<YDocController | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
if (!params?.id) return;
|
||||
const data = await loadBlockData(params.id);
|
||||
console.log('==== enter ====', params?.id, data);
|
||||
|
||||
if (!blockEditorRef.current) {
|
||||
blockEditorRef.current = new BlockEditor(params?.id, data);
|
||||
} else {
|
||||
blockEditorRef.current.changeDoc(params?.id, data);
|
||||
}
|
||||
|
||||
setBlockId(params.id)
|
||||
const c = new YDocController(params.id);
|
||||
setController(c);
|
||||
const res = await c.open();
|
||||
console.log(res)
|
||||
setDocumentData(res)
|
||||
setDocumentId(params.id)
|
||||
})();
|
||||
return () => {
|
||||
console.log('==== leave ====', params?.id)
|
||||
}
|
||||
}, [params.id]);
|
||||
return { blockId, blockEditor: blockEditorRef.current };
|
||||
return { documentId, documentData, controller };
|
||||
};
|
||||
|
@ -1,27 +1,23 @@
|
||||
import { useDocument } from './DocumentPage.hooks';
|
||||
import BlockList from '../components/block/BlockList';
|
||||
import { BlockContext } from '../utils/block';
|
||||
import { createTheme, ThemeProvider } from '@mui/material';
|
||||
import Root from '../components/document/Root';
|
||||
import { YDocControllerContext } from '../stores/effects/document/document_controller';
|
||||
|
||||
const theme = createTheme({
|
||||
typography: {
|
||||
fontFamily: ['Poppins'].join(','),
|
||||
},
|
||||
});
|
||||
export const DocumentPage = () => {
|
||||
const { blockId, blockEditor } = useDocument();
|
||||
|
||||
if (!blockId || !blockEditor) return <div className='error-page'></div>;
|
||||
export const DocumentPage = () => {
|
||||
const { documentId, documentData, controller } = useDocument();
|
||||
|
||||
if (!documentId || !documentData || !controller) return null;
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<BlockContext.Provider
|
||||
value={{
|
||||
id: blockId,
|
||||
blockEditor,
|
||||
}}
|
||||
>
|
||||
<BlockList blockEditor={blockEditor} blockId={blockId} />
|
||||
</BlockContext.Provider>
|
||||
<YDocControllerContext.Provider value={controller}>
|
||||
<Root documentData={documentData} />
|
||||
</YDocControllerContext.Provider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user