mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
parent
1f187a3917
commit
cf97c8ba9c
@ -12,8 +12,10 @@ module.exports = {
|
|||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
},
|
},
|
||||||
plugins: ['@typescript-eslint'],
|
plugins: ['@typescript-eslint', "react-hooks"],
|
||||||
rules: {
|
rules: {
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
||||||
'@typescript-eslint/no-empty-function': 'error',
|
'@typescript-eslint/no-empty-function': 'error',
|
||||||
'@typescript-eslint/no-empty-interface': 'warn',
|
'@typescript-eslint/no-empty-interface': 'warn',
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "2.8.4",
|
"prettier": "2.8.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||||
|
11
frontend/appflowy_tauri/pnpm-lock.yaml
generated
11
frontend/appflowy_tauri/pnpm-lock.yaml
generated
@ -25,6 +25,7 @@ specifiers:
|
|||||||
dayjs: ^1.11.7
|
dayjs: ^1.11.7
|
||||||
eslint: ^8.34.0
|
eslint: ^8.34.0
|
||||||
eslint-plugin-react: ^7.32.2
|
eslint-plugin-react: ^7.32.2
|
||||||
|
eslint-plugin-react-hooks: ^4.6.0
|
||||||
events: ^3.3.0
|
events: ^3.3.0
|
||||||
google-protobuf: ^3.21.2
|
google-protobuf: ^3.21.2
|
||||||
i18next: ^22.4.10
|
i18next: ^22.4.10
|
||||||
@ -110,6 +111,7 @@ devDependencies:
|
|||||||
autoprefixer: 10.4.13_postcss@8.4.21
|
autoprefixer: 10.4.13_postcss@8.4.21
|
||||||
eslint: 8.35.0
|
eslint: 8.35.0
|
||||||
eslint-plugin-react: 7.32.2_eslint@8.35.0
|
eslint-plugin-react: 7.32.2_eslint@8.35.0
|
||||||
|
eslint-plugin-react-hooks: 4.6.0_eslint@8.35.0
|
||||||
postcss: 8.4.21
|
postcss: 8.4.21
|
||||||
prettier: 2.8.4
|
prettier: 2.8.4
|
||||||
prettier-plugin-tailwindcss: 0.2.4_prettier@2.8.4
|
prettier-plugin-tailwindcss: 0.2.4_prettier@2.8.4
|
||||||
@ -2426,6 +2428,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
/eslint-plugin-react-hooks/4.6.0_eslint@8.35.0:
|
||||||
|
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.35.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-plugin-react/7.32.2_eslint@8.35.0:
|
/eslint-plugin-react/7.32.2_eslint@8.35.0:
|
||||||
resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==}
|
resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -1,31 +1,6 @@
|
|||||||
import { useCallback, useContext, useMemo } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Editor } from 'slate';
|
|
||||||
import { TextBlockKeyEventHandlerParams, TextDelta, TextSelection } from '$app/interfaces/document';
|
|
||||||
import { useTextInput } from '../_shared/TextInput.hooks';
|
import { useTextInput } from '../_shared/TextInput.hooks';
|
||||||
import { useAppDispatch } from '@/appflowy_app/stores/store';
|
import { useTextBlockKeyEvent } from '$app/components/document/TextBlock/events/Events.hooks';
|
||||||
import { DocumentControllerContext } from '@/appflowy_app/stores/effects/document/document_controller';
|
|
||||||
import {
|
|
||||||
backspaceNodeThunk,
|
|
||||||
indentNodeThunk,
|
|
||||||
splitNodeThunk,
|
|
||||||
setCursorNextLineThunk,
|
|
||||||
setCursorPreLineThunk,
|
|
||||||
} from '@/appflowy_app/stores/reducers/document/async-actions';
|
|
||||||
import { documentActions } from '@/appflowy_app/stores/reducers/document/slice';
|
|
||||||
import {
|
|
||||||
canHandleBackspaceKey,
|
|
||||||
canHandleDownKey,
|
|
||||||
canHandleEnterKey,
|
|
||||||
canHandleLeftKey,
|
|
||||||
canHandleRightKey,
|
|
||||||
canHandleTabKey,
|
|
||||||
canHandleUpKey,
|
|
||||||
onHandleEnterKey,
|
|
||||||
triggerHotkey,
|
|
||||||
} from '$app/utils/document/slate/hotkey';
|
|
||||||
import { updateNodeDeltaThunk } from '$app_reducers/document/async-actions/blocks/text/update';
|
|
||||||
import { useMarkDown } from './useMarkDown.hooks';
|
|
||||||
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
|
|
||||||
|
|
||||||
export function useTextBlock(id: string) {
|
export function useTextBlock(id: string) {
|
||||||
const { editor, onChange, value } = useTextInput(id);
|
const { editor, onChange, value } = useTextInput(id);
|
||||||
@ -55,177 +30,3 @@ export function useTextBlock(id: string) {
|
|||||||
value,
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useTextBlockKeyEvent(id: string, editor: Editor) {
|
|
||||||
const { indentAction, backSpaceAction, splitAction, wrapAction, focusPreLineAction, focusNextLineAction } =
|
|
||||||
useActions(id);
|
|
||||||
|
|
||||||
const { markdownEvents } = useMarkDown(id);
|
|
||||||
|
|
||||||
const enterEvent = useMemo(() => {
|
|
||||||
return {
|
|
||||||
triggerEventKey: keyBoardEventKeyMap.Enter,
|
|
||||||
canHandle: canHandleEnterKey,
|
|
||||||
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
|
||||||
onHandleEnterKey(...args, {
|
|
||||||
onSplit: splitAction,
|
|
||||||
onWrap: wrapAction,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [splitAction, wrapAction]);
|
|
||||||
|
|
||||||
const tabEvent = useMemo(() => {
|
|
||||||
return {
|
|
||||||
triggerEventKey: keyBoardEventKeyMap.Tab,
|
|
||||||
canHandle: canHandleTabKey,
|
|
||||||
handler: (..._args: TextBlockKeyEventHandlerParams) => {
|
|
||||||
void indentAction();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [indentAction]);
|
|
||||||
|
|
||||||
const backSpaceEvent = useMemo(() => {
|
|
||||||
return {
|
|
||||||
triggerEventKey: keyBoardEventKeyMap.Backspace,
|
|
||||||
canHandle: canHandleBackspaceKey,
|
|
||||||
handler: (..._args: TextBlockKeyEventHandlerParams) => {
|
|
||||||
void backSpaceAction();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [backSpaceAction]);
|
|
||||||
|
|
||||||
const upEvent = useMemo(() => {
|
|
||||||
return {
|
|
||||||
triggerEventKey: keyBoardEventKeyMap.Up,
|
|
||||||
canHandle: canHandleUpKey,
|
|
||||||
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
|
||||||
void focusPreLineAction({
|
|
||||||
editor: args[1],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [focusPreLineAction]);
|
|
||||||
|
|
||||||
const downEvent = useMemo(() => {
|
|
||||||
return {
|
|
||||||
triggerEventKey: keyBoardEventKeyMap.Down,
|
|
||||||
canHandle: canHandleDownKey,
|
|
||||||
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
|
||||||
void focusNextLineAction({
|
|
||||||
editor: args[1],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [focusNextLineAction]);
|
|
||||||
|
|
||||||
const leftEvent = useMemo(() => {
|
|
||||||
return {
|
|
||||||
triggerEventKey: keyBoardEventKeyMap.Left,
|
|
||||||
canHandle: canHandleLeftKey,
|
|
||||||
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
|
||||||
void focusPreLineAction({
|
|
||||||
editor: args[1],
|
|
||||||
focusEnd: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [focusPreLineAction]);
|
|
||||||
|
|
||||||
const rightEvent = useMemo(() => {
|
|
||||||
return {
|
|
||||||
triggerEventKey: keyBoardEventKeyMap.Right,
|
|
||||||
canHandle: canHandleRightKey,
|
|
||||||
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
|
||||||
void focusNextLineAction({
|
|
||||||
editor: args[1],
|
|
||||||
focusStart: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [focusNextLineAction]);
|
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
// This is list of key events that can be handled by TextBlock
|
|
||||||
const keyEvents = [enterEvent, backSpaceEvent, tabEvent, upEvent, downEvent, leftEvent, rightEvent];
|
|
||||||
|
|
||||||
keyEvents.push(...markdownEvents);
|
|
||||||
const matchKeys = keyEvents.filter((keyEvent) => keyEvent.canHandle(event, editor));
|
|
||||||
if (matchKeys.length === 0) {
|
|
||||||
triggerHotkey(event, editor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
matchKeys.forEach((matchKey) => matchKey.handler(event, editor));
|
|
||||||
},
|
|
||||||
[editor, enterEvent, backSpaceEvent, tabEvent, upEvent, downEvent, leftEvent, rightEvent, markdownEvents]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
onKeyDown,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function useActions(id: string) {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const controller = useContext(DocumentControllerContext);
|
|
||||||
|
|
||||||
const indentAction = useCallback(async () => {
|
|
||||||
if (!controller) return;
|
|
||||||
await dispatch(
|
|
||||||
indentNodeThunk({
|
|
||||||
id,
|
|
||||||
controller,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [id, controller]);
|
|
||||||
|
|
||||||
const backSpaceAction = useCallback(async () => {
|
|
||||||
if (!controller) return;
|
|
||||||
await dispatch(backspaceNodeThunk({ id, controller }));
|
|
||||||
}, [controller, id]);
|
|
||||||
|
|
||||||
const splitAction = useCallback(
|
|
||||||
async (retain: TextDelta[], insert: TextDelta[]) => {
|
|
||||||
if (!controller) return;
|
|
||||||
await dispatch(splitNodeThunk({ id, retain, insert, controller }));
|
|
||||||
},
|
|
||||||
[controller, id]
|
|
||||||
);
|
|
||||||
|
|
||||||
const wrapAction = useCallback(
|
|
||||||
async (delta: TextDelta[], selection: TextSelection) => {
|
|
||||||
if (!controller) return;
|
|
||||||
await dispatch(updateNodeDeltaThunk({ id, delta, controller }));
|
|
||||||
// This is a hack to make sure the selection is updated after next render
|
|
||||||
dispatch(documentActions.setTextSelection({ blockId: id, selection }));
|
|
||||||
},
|
|
||||||
[controller, id]
|
|
||||||
);
|
|
||||||
|
|
||||||
const focusPreLineAction = useCallback(
|
|
||||||
async (params: { editor: Editor; focusEnd?: boolean }) => {
|
|
||||||
await dispatch(setCursorPreLineThunk({ id, ...params }));
|
|
||||||
},
|
|
||||||
[id]
|
|
||||||
);
|
|
||||||
|
|
||||||
const focusNextLineAction = useCallback(
|
|
||||||
async (params: { editor: Editor; focusStart?: boolean }) => {
|
|
||||||
await dispatch(setCursorNextLineThunk({ id, ...params }));
|
|
||||||
},
|
|
||||||
[id]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
indentAction,
|
|
||||||
backSpaceAction,
|
|
||||||
splitAction,
|
|
||||||
wrapAction,
|
|
||||||
focusPreLineAction,
|
|
||||||
focusNextLineAction,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
|
import { useCallback, useContext } from 'react';
|
||||||
|
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||||
|
import {
|
||||||
|
backspaceNodeThunk,
|
||||||
|
indentNodeThunk,
|
||||||
|
setCursorNextLineThunk,
|
||||||
|
setCursorPreLineThunk,
|
||||||
|
splitNodeThunk,
|
||||||
|
updateNodeDeltaThunk,
|
||||||
|
} from '$app_reducers/document/async-actions';
|
||||||
|
import { TextDelta, TextSelection } from '$app/interfaces/document';
|
||||||
|
import { documentActions } from '$app_reducers/document/slice';
|
||||||
|
import { Editor } from 'slate';
|
||||||
|
|
||||||
|
export function useActions(id: string) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const controller = useContext(DocumentControllerContext);
|
||||||
|
|
||||||
|
const indentAction = useCallback(async () => {
|
||||||
|
if (!controller) return;
|
||||||
|
await dispatch(
|
||||||
|
indentNodeThunk({
|
||||||
|
id,
|
||||||
|
controller,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [id, controller]);
|
||||||
|
|
||||||
|
const backSpaceAction = useCallback(async () => {
|
||||||
|
if (!controller) return;
|
||||||
|
await dispatch(backspaceNodeThunk({ id, controller }));
|
||||||
|
}, [controller, id]);
|
||||||
|
|
||||||
|
const splitAction = useCallback(
|
||||||
|
async (retain: TextDelta[], insert: TextDelta[]) => {
|
||||||
|
if (!controller) return;
|
||||||
|
await dispatch(splitNodeThunk({ id, retain, insert, controller }));
|
||||||
|
},
|
||||||
|
[controller, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const wrapAction = useCallback(
|
||||||
|
async (delta: TextDelta[], selection: TextSelection) => {
|
||||||
|
if (!controller) return;
|
||||||
|
await dispatch(updateNodeDeltaThunk({ id, delta, controller }));
|
||||||
|
// This is a hack to make sure the selection is updated after next render
|
||||||
|
dispatch(documentActions.setTextSelection({ blockId: id, selection }));
|
||||||
|
},
|
||||||
|
[controller, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const focusPreLineAction = useCallback(
|
||||||
|
async (params: { editor: Editor; focusEnd?: boolean }) => {
|
||||||
|
await dispatch(setCursorPreLineThunk({ id, ...params }));
|
||||||
|
},
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const focusNextLineAction = useCallback(
|
||||||
|
async (params: { editor: Editor; focusStart?: boolean }) => {
|
||||||
|
await dispatch(setCursorNextLineThunk({ id, ...params }));
|
||||||
|
},
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
indentAction,
|
||||||
|
backSpaceAction,
|
||||||
|
splitAction,
|
||||||
|
wrapAction,
|
||||||
|
focusPreLineAction,
|
||||||
|
focusNextLineAction,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
import { Editor } from 'slate';
|
||||||
|
import { useTurnIntoBlock } from './TurnIntoEvents.hooks';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
|
||||||
|
import {
|
||||||
|
canHandleBackspaceKey,
|
||||||
|
canHandleDownKey,
|
||||||
|
canHandleEnterKey,
|
||||||
|
canHandleLeftKey,
|
||||||
|
canHandleRightKey,
|
||||||
|
canHandleTabKey,
|
||||||
|
canHandleUpKey,
|
||||||
|
onHandleEnterKey,
|
||||||
|
triggerHotkey,
|
||||||
|
} from '$app/utils/document/slate/hotkey';
|
||||||
|
import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document';
|
||||||
|
import { useActions } from './Actions.hooks';
|
||||||
|
|
||||||
|
export function useTextBlockKeyEvent(id: string, editor: Editor) {
|
||||||
|
const { indentAction, backSpaceAction, splitAction, wrapAction, focusPreLineAction, focusNextLineAction } =
|
||||||
|
useActions(id);
|
||||||
|
|
||||||
|
const { turnIntoBlockEvents } = useTurnIntoBlock(id);
|
||||||
|
|
||||||
|
const events = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
triggerEventKey: keyBoardEventKeyMap.Enter,
|
||||||
|
canHandle: canHandleEnterKey,
|
||||||
|
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||||
|
onHandleEnterKey(...args, {
|
||||||
|
onSplit: splitAction,
|
||||||
|
onWrap: wrapAction,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
triggerEventKey: keyBoardEventKeyMap.Tab,
|
||||||
|
canHandle: canHandleTabKey,
|
||||||
|
handler: (..._args: TextBlockKeyEventHandlerParams) => {
|
||||||
|
void indentAction();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
triggerEventKey: keyBoardEventKeyMap.Backspace,
|
||||||
|
canHandle: canHandleBackspaceKey,
|
||||||
|
handler: (..._args: TextBlockKeyEventHandlerParams) => {
|
||||||
|
void backSpaceAction();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
triggerEventKey: keyBoardEventKeyMap.Up,
|
||||||
|
canHandle: canHandleUpKey,
|
||||||
|
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||||
|
void focusPreLineAction({
|
||||||
|
editor: args[1],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
triggerEventKey: keyBoardEventKeyMap.Down,
|
||||||
|
canHandle: canHandleDownKey,
|
||||||
|
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||||
|
void focusNextLineAction({
|
||||||
|
editor: args[1],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
triggerEventKey: keyBoardEventKeyMap.Left,
|
||||||
|
canHandle: canHandleLeftKey,
|
||||||
|
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||||
|
void focusPreLineAction({
|
||||||
|
editor: args[1],
|
||||||
|
focusEnd: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
triggerEventKey: keyBoardEventKeyMap.Right,
|
||||||
|
canHandle: canHandleRightKey,
|
||||||
|
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||||
|
void focusNextLineAction({
|
||||||
|
editor: args[1],
|
||||||
|
focusStart: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [splitAction, wrapAction, indentAction, backSpaceAction, focusPreLineAction, focusNextLineAction]);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
// This is list of key events that can be handled by TextBlock
|
||||||
|
const keyEvents = [...events, ...turnIntoBlockEvents];
|
||||||
|
|
||||||
|
const matchKeys = keyEvents.filter((keyEvent) => keyEvent.canHandle(event, editor));
|
||||||
|
if (matchKeys.length === 0) {
|
||||||
|
triggerHotkey(event, editor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
matchKeys.forEach((matchKey) => matchKey.handler(event, editor));
|
||||||
|
},
|
||||||
|
[editor, events, turnIntoBlockEvents]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onKeyDown,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
import { useContext, useMemo } from 'react';
|
||||||
|
import { BlockData, BlockType, TextBlockKeyEventHandlerParams } from '$app/interfaces/document';
|
||||||
|
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
|
||||||
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
|
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { turnToBlockThunk } from '$app_reducers/document/async-actions';
|
||||||
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
|
import { Editor } from 'slate';
|
||||||
|
import { getBeforeRangeAt } from '$app/utils/document/slate/text';
|
||||||
|
import { getHeadingDataFromEditor, getQuoteDataFromEditor, getTodoListDataFromEditor } from '$app/utils/document/blocks';
|
||||||
|
|
||||||
|
const blockDataFactoryMap: Record<string, (editor: Editor) => BlockData<any> | undefined> = {
|
||||||
|
[BlockType.HeadingBlock]: getHeadingDataFromEditor,
|
||||||
|
[BlockType.TodoListBlock]: getTodoListDataFromEditor,
|
||||||
|
[BlockType.QuoteBlock]: getQuoteDataFromEditor,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useTurnIntoBlock(id: string) {
|
||||||
|
const controller = useContext(DocumentControllerContext);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const turnIntoBlockEvents = useMemo(() => {
|
||||||
|
return Object.entries(blockDataFactoryMap).map(([type, getData]) => {
|
||||||
|
const blockType = type as BlockType;
|
||||||
|
return {
|
||||||
|
triggerEventKey: keyBoardEventKeyMap.Space,
|
||||||
|
canHandle: canHandle(blockType),
|
||||||
|
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||||
|
if (!controller) return;
|
||||||
|
const [_event, editor] = args;
|
||||||
|
const data = getData(editor);
|
||||||
|
if (!data) return;
|
||||||
|
dispatch(turnToBlockThunk({ id, data, type: blockType, controller }));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}, [controller, dispatch, id]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
turnIntoBlockEvents,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function canHandle(type: BlockType) {
|
||||||
|
const config = blockConfig[type];
|
||||||
|
|
||||||
|
const regex = config.markdownRegexps;
|
||||||
|
// This error will be thrown if the block type is not in the config, and it will happen in development environment
|
||||||
|
if (!regex) {
|
||||||
|
throw new Error(`canHandle: block type ${type} is not supported`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (...args: TextBlockKeyEventHandlerParams) => {
|
||||||
|
const [event, editor] = args;
|
||||||
|
const isSpaceKey = event.key === keyBoardEventKeyMap.Space;
|
||||||
|
const selection = editor.selection;
|
||||||
|
|
||||||
|
if (!isSpaceKey || !selection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flag = Editor.string(editor, getBeforeRangeAt(editor, selection)).trim();
|
||||||
|
if (flag === null) return false;
|
||||||
|
|
||||||
|
return regex.some((r) => r.test(flag));
|
||||||
|
};
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import { Slate, Editable } from 'slate-react';
|
import { Slate, Editable } from 'slate-react';
|
||||||
import Leaf from './Leaf';
|
import Leaf from './Leaf';
|
||||||
import { useTextBlock } from './TextBlock.hooks';
|
import { useTextBlock } from './TextBlock.hooks';
|
||||||
import NodeComponent from '../Node';
|
|
||||||
import BlockHorizontalToolbar from '../BlockHorizontalToolbar';
|
import BlockHorizontalToolbar from '../BlockHorizontalToolbar';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NestedBlock } from '$app/interfaces/document';
|
import { NestedBlock } from '$app/interfaces/document';
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
import { useCallback, useContext, useMemo } from 'react';
|
|
||||||
import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document';
|
|
||||||
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
|
|
||||||
import {
|
|
||||||
canHandleToHeadingBlock,
|
|
||||||
canHandleToCheckboxBlock,
|
|
||||||
canHandleToQuoteBlock,
|
|
||||||
} from '$app/utils/document/slate/markdown';
|
|
||||||
import { useAppDispatch } from '$app/stores/store';
|
|
||||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
|
||||||
import { turnToHeadingBlockThunk } from '$app_reducers/document/async-actions/blocks/heading';
|
|
||||||
import { turnToTodoListBlockThunk } from '$app_reducers/document/async-actions/blocks/todo_list';
|
|
||||||
import { turnToQuoteBlockThunk } from '$app_reducers/document/async-actions/blocks/quote';
|
|
||||||
|
|
||||||
export function useMarkDown(id: string) {
|
|
||||||
const { toHeadingBlockAction, toCheckboxBlockAction, toQuoteBlockAction } = useActions(id);
|
|
||||||
const toHeadingBlockEvent = useMemo(() => {
|
|
||||||
return {
|
|
||||||
triggerEventKey: keyBoardEventKeyMap.Space,
|
|
||||||
canHandle: canHandleToHeadingBlock,
|
|
||||||
handler: toHeadingBlockAction,
|
|
||||||
};
|
|
||||||
}, [toHeadingBlockAction]);
|
|
||||||
|
|
||||||
const toCheckboxBlockEvent = useMemo(() => {
|
|
||||||
return {
|
|
||||||
triggerEventKey: keyBoardEventKeyMap.Space,
|
|
||||||
canHandle: canHandleToCheckboxBlock,
|
|
||||||
handler: toCheckboxBlockAction,
|
|
||||||
};
|
|
||||||
}, [toCheckboxBlockAction]);
|
|
||||||
|
|
||||||
const toQuoteBlockEvent = useMemo(() => {
|
|
||||||
return {
|
|
||||||
triggerEventKey: keyBoardEventKeyMap.Space,
|
|
||||||
canHandle: canHandleToQuoteBlock,
|
|
||||||
handler: toQuoteBlockAction,
|
|
||||||
};
|
|
||||||
}, [toQuoteBlockAction]);
|
|
||||||
|
|
||||||
const markdownEvents = useMemo(
|
|
||||||
() => [toHeadingBlockEvent, toCheckboxBlockEvent, toQuoteBlockEvent],
|
|
||||||
[toHeadingBlockEvent, toCheckboxBlockEvent, toQuoteBlockEvent]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
markdownEvents,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function useActions(id: string) {
|
|
||||||
const controller = useContext(DocumentControllerContext);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const toHeadingBlockAction = useCallback(
|
|
||||||
(...args: TextBlockKeyEventHandlerParams) => {
|
|
||||||
if (!controller) return;
|
|
||||||
const [_event, editor] = args;
|
|
||||||
dispatch(turnToHeadingBlockThunk({ id, editor, controller }));
|
|
||||||
},
|
|
||||||
[controller, dispatch, id]
|
|
||||||
);
|
|
||||||
|
|
||||||
const toCheckboxBlockAction = useCallback(
|
|
||||||
(...args: TextBlockKeyEventHandlerParams) => {
|
|
||||||
if (!controller) return;
|
|
||||||
const [_event, editor] = args;
|
|
||||||
dispatch(turnToTodoListBlockThunk({ id, controller, editor }));
|
|
||||||
},
|
|
||||||
[controller, dispatch, id]
|
|
||||||
);
|
|
||||||
|
|
||||||
const toQuoteBlockAction = useCallback(
|
|
||||||
(...args: TextBlockKeyEventHandlerParams) => {
|
|
||||||
if (!controller) return;
|
|
||||||
const [_event, editor] = args;
|
|
||||||
dispatch(turnToQuoteBlockThunk({ id, controller, editor }));
|
|
||||||
},
|
|
||||||
[controller, dispatch, id]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
toHeadingBlockAction,
|
|
||||||
toCheckboxBlockAction,
|
|
||||||
toQuoteBlockAction,
|
|
||||||
};
|
|
||||||
}
|
|
@ -11,6 +11,7 @@ import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
|
|||||||
import { updateNodeDeltaThunk } from '$app_reducers/document/async-actions/blocks/text/update';
|
import { updateNodeDeltaThunk } from '$app_reducers/document/async-actions/blocks/text/update';
|
||||||
import { deltaToSlateValue, getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
|
import { deltaToSlateValue, getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
|
||||||
import { documentActions } from '@/appflowy_app/stores/reducers/document/slice';
|
import { documentActions } from '@/appflowy_app/stores/reducers/document/slice';
|
||||||
|
import { isSameDelta } from '$app/utils/document/blocks/text';
|
||||||
|
|
||||||
export function useTextInput(id: string) {
|
export function useTextInput(id: string) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -25,27 +26,18 @@ export function useTextInput(id: string) {
|
|||||||
|
|
||||||
const { editor, yText } = useBindYjs(id, delta);
|
const { editor, yText } = useBindYjs(id, delta);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
dispatch(documentActions.removeTextSelection(id));
|
|
||||||
};
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
const [value, setValue] = useState<Descendant[]>([]);
|
const [value, setValue] = useState<Descendant[]>([]);
|
||||||
|
|
||||||
const storeSelection = useCallback(() => {
|
const storeSelection = useCallback(() => {
|
||||||
// This is a hack to make sure the selection is updated after next render
|
// This is a hack to make sure the selection is updated after next render
|
||||||
// It will save the selection to the store, and the selection will be restored
|
// It will save the selection to the store, and the selection will be restored
|
||||||
if (!ReactEditor.isFocused(editor) || !editor.selection || !editor.selection.anchor || !editor.selection.focus)
|
if (!ReactEditor.isFocused(editor)) return;
|
||||||
return;
|
const selection = editor.selection as TextSelection;
|
||||||
const { anchor, focus } = editor.selection;
|
|
||||||
const selection = { anchor, focus } as TextSelection;
|
|
||||||
dispatch(documentActions.setTextSelection({ blockId: id, selection }));
|
dispatch(documentActions.setTextSelection({ blockId: id, selection }));
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
const currentSelection = useAppSelector((state) => state.document.textSelections[id]);
|
const currentSelection = useAppSelector((state) => state.document.textSelections[id]);
|
||||||
const restoreSelection = useCallback(() => {
|
const restoreSelection = useCallback(() => {
|
||||||
if (editor.selection && JSON.stringify(currentSelection) === JSON.stringify(editor.selection)) return;
|
|
||||||
setSelection(editor, currentSelection);
|
setSelection(editor, currentSelection);
|
||||||
}, [editor, currentSelection]);
|
}, [editor, currentSelection]);
|
||||||
|
|
||||||
@ -54,13 +46,15 @@ export function useTextInput(id: string) {
|
|||||||
setValue(e);
|
setValue(e);
|
||||||
storeSelection();
|
storeSelection();
|
||||||
},
|
},
|
||||||
|
|
||||||
[storeSelection]
|
[storeSelection]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
restoreSelection();
|
restoreSelection();
|
||||||
}, [restoreSelection]);
|
return () => {
|
||||||
|
dispatch(documentActions.removeTextSelection(id));
|
||||||
|
};
|
||||||
|
}, [id, restoreSelection]);
|
||||||
|
|
||||||
if (editor.selection && ReactEditor.isFocused(editor)) {
|
if (editor.selection && ReactEditor.isFocused(editor)) {
|
||||||
const domSelection = window.getSelection();
|
const domSelection = window.getSelection();
|
||||||
@ -128,12 +122,13 @@ function useBindYjs(id: string, delta: TextDelta[]) {
|
|||||||
if (!yText) return;
|
if (!yText) return;
|
||||||
|
|
||||||
// If the delta is not equal to the current yText, then we need to update the yText
|
// If the delta is not equal to the current yText, then we need to update the yText
|
||||||
if (JSON.stringify(yText.toDelta()) !== JSON.stringify(delta)) {
|
const isSame = isSameDelta(delta, yText.toDelta());
|
||||||
|
if (isSame) return;
|
||||||
|
|
||||||
yText.delete(0, yText.length);
|
yText.delete(0, yText.length);
|
||||||
yText.applyDelta(delta);
|
yText.applyDelta(delta);
|
||||||
// It should be noted that the selection will be lost after the yText is updated
|
// It should be noted that the selection will be lost after the yText is updated
|
||||||
setSelection(editor, currentSelection);
|
setSelection(editor, currentSelection);
|
||||||
}
|
|
||||||
}, [delta, currentSelection, editor]);
|
}, [delta, currentSelection, editor]);
|
||||||
|
|
||||||
return { editor, yText: yTextRef.current };
|
return { editor, yText: yTextRef.current };
|
||||||
@ -167,7 +162,6 @@ function setSelection(editor: ReactEditor, currentSelection: TextSelection) {
|
|||||||
if (!currentSelection || !currentSelection.anchor || !currentSelection.focus) {
|
if (!currentSelection || !currentSelection.anchor || !currentSelection.focus) {
|
||||||
if (ReactEditor.isFocused(editor)) {
|
if (ReactEditor.isFocused(editor)) {
|
||||||
ReactEditor.blur(editor);
|
ReactEditor.blur(editor);
|
||||||
ReactEditor.deselect(editor);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -178,12 +172,12 @@ function setSelection(editor: ReactEditor, currentSelection: TextSelection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { path, offset } = currentSelection.focus;
|
const { path, offset } = currentSelection.focus;
|
||||||
// It is possible that the current selection is out of range
|
|
||||||
const children = getDeltaFromSlateNodes(editor.children);
|
const children = getDeltaFromSlateNodes(editor.children);
|
||||||
|
|
||||||
// the path always has 2 elements,
|
// the path always has 2 elements,
|
||||||
// because the slate node is a two-dimensional array
|
// because the slate node is a two-dimensional array
|
||||||
const index = path[1];
|
const index = path[1];
|
||||||
|
// It is possible that the current selection is out of range
|
||||||
if (children[index].insert.length < offset) {
|
if (children[index].insert.length < offset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,75 @@
|
|||||||
import { BlockType } from '$app/interfaces/document';
|
import { BlockType } from '$app/interfaces/document';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block types that are allowed to have children
|
* If the block type is not in the config, it will be thrown an error in development env
|
||||||
*/
|
*/
|
||||||
export const allowedChildrenBlockTypes = [
|
export const blockConfig: Record<
|
||||||
BlockType.TextBlock,
|
string,
|
||||||
BlockType.PageBlock,
|
{
|
||||||
BlockType.TodoListBlock,
|
|
||||||
BlockType.QuoteBlock,
|
|
||||||
BlockType.CalloutBlock,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block types that split node can extend to the next line
|
* Whether the block can have children
|
||||||
*/
|
*/
|
||||||
export const splitableBlockTypes = [BlockType.TextBlock, BlockType.TodoListBlock];
|
canAddChild: boolean;
|
||||||
|
/**
|
||||||
|
* the type of the block that will be split from the current block
|
||||||
|
*/
|
||||||
|
splitType: BlockType;
|
||||||
|
/**
|
||||||
|
* The regexps that will be used to match the markdown flag
|
||||||
|
*/
|
||||||
|
markdownRegexps?: RegExp[];
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
[BlockType.TextBlock]: {
|
||||||
|
canAddChild: true,
|
||||||
|
splitType: BlockType.TextBlock,
|
||||||
|
},
|
||||||
|
[BlockType.HeadingBlock]: {
|
||||||
|
canAddChild: false,
|
||||||
|
splitType: BlockType.TextBlock,
|
||||||
|
/**
|
||||||
|
* # or ## or ###
|
||||||
|
*/
|
||||||
|
markdownRegexps: [/^(#{1,3})$/],
|
||||||
|
},
|
||||||
|
[BlockType.TodoListBlock]: {
|
||||||
|
canAddChild: true,
|
||||||
|
splitType: BlockType.TodoListBlock,
|
||||||
|
/**
|
||||||
|
* -[] or -[x] or -[ ] or [] or [x] or [ ]
|
||||||
|
*/
|
||||||
|
markdownRegexps: [/^((-)?\[(x|\s)?\])$/],
|
||||||
|
},
|
||||||
|
[BlockType.BulletedListBlock]: {
|
||||||
|
canAddChild: true,
|
||||||
|
splitType: BlockType.BulletedListBlock,
|
||||||
|
/**
|
||||||
|
* - or + or *
|
||||||
|
*/
|
||||||
|
markdownRegexps: [/^(\s*[-+*])$/],
|
||||||
|
},
|
||||||
|
[BlockType.NumberedListBlock]: {
|
||||||
|
canAddChild: true,
|
||||||
|
splitType: BlockType.NumberedListBlock,
|
||||||
|
/**
|
||||||
|
* 1. or 2. or 3.
|
||||||
|
*/
|
||||||
|
markdownRegexps: [/^(\s*\d+\.)$/],
|
||||||
|
},
|
||||||
|
[BlockType.QuoteBlock]: {
|
||||||
|
canAddChild: true,
|
||||||
|
splitType: BlockType.TextBlock,
|
||||||
|
/**
|
||||||
|
* " or “ or ”
|
||||||
|
*/
|
||||||
|
markdownRegexps: [/^("|“|”)$/],
|
||||||
|
},
|
||||||
|
[BlockType.CodeBlock]: {
|
||||||
|
canAddChild: false,
|
||||||
|
splitType: BlockType.TextBlock,
|
||||||
|
/**
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
markdownRegexps: [/^(```)$/],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -5,6 +5,8 @@ export enum BlockType {
|
|||||||
HeadingBlock = 'heading',
|
HeadingBlock = 'heading',
|
||||||
TextBlock = 'text',
|
TextBlock = 'text',
|
||||||
TodoListBlock = 'todo_list',
|
TodoListBlock = 'todo_list',
|
||||||
|
BulletedListBlock = 'bulleted_list',
|
||||||
|
NumberedListBlock = 'numbered_list',
|
||||||
CodeBlock = 'code',
|
CodeBlock = 'code',
|
||||||
EmbedBlock = 'embed',
|
EmbedBlock = 'embed',
|
||||||
QuoteBlock = 'quote',
|
QuoteBlock = 'quote',
|
||||||
|
@ -15,6 +15,7 @@ import * as Y from 'yjs';
|
|||||||
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '$app/constants/document/block';
|
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '$app/constants/document/block';
|
||||||
import { get } from '@/appflowy_app/utils/tool';
|
import { get } from '@/appflowy_app/utils/tool';
|
||||||
import { blockPB2Node } from '$app/utils/document/blocks/common';
|
import { blockPB2Node } from '$app/utils/document/blocks/common';
|
||||||
|
import { Log } from '$app/utils/log';
|
||||||
|
|
||||||
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
||||||
|
|
||||||
@ -65,6 +66,7 @@ export class DocumentController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
applyActions = async (actions: ReturnType<typeof BlockActionPB.prototype.toObject>[]) => {
|
applyActions = async (actions: ReturnType<typeof BlockActionPB.prototype.toObject>[]) => {
|
||||||
|
Log.debug('applyActions', actions);
|
||||||
await this.backendService.applyActions(actions);
|
await this.backendService.applyActions(actions);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -153,6 +155,7 @@ export class DocumentController {
|
|||||||
if (!this.onDocChange) return;
|
if (!this.onDocChange) return;
|
||||||
const { events, is_remote } = DocEventPB.deserializeBinary(payload);
|
const { events, is_remote } = DocEventPB.deserializeBinary(payload);
|
||||||
|
|
||||||
|
Log.debug('DocumentController', 'updated', { events, is_remote });
|
||||||
events.forEach((blockEvent) => {
|
events.forEach((blockEvent) => {
|
||||||
blockEvent.event.forEach((_payload) => {
|
blockEvent.event.forEach((_payload) => {
|
||||||
this.onDocChange?.({
|
this.onDocChange?.({
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import { Ok, Result } from 'ts-results';
|
|
||||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
|
||||||
import { FolderNotificationObserver } from '../folder/notifications/observer';
|
|
||||||
import { DocumentNotification } from '@/services/backend';
|
import { DocumentNotification } from '@/services/backend';
|
||||||
import { DocumentNotificationObserver } from './notifications/observer';
|
import { DocumentNotificationObserver } from './notifications/observer';
|
||||||
|
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import { Editor } from 'slate';
|
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
|
||||||
import { BlockType } from '$app/interfaces/document';
|
|
||||||
import { turnToBlockThunk } from '$app_reducers/document/async-actions/turn_to';
|
|
||||||
import { getHeadingDataFromEditor } from '$app/utils/document/blocks/heading';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* transform to heading block
|
|
||||||
* 1. insert heading block after current block
|
|
||||||
* 2. move all children to parent after heading block, because heading block can't have children
|
|
||||||
* 3. delete current block
|
|
||||||
*/
|
|
||||||
export const turnToHeadingBlockThunk = createAsyncThunk(
|
|
||||||
'document/turnToHeadingBlock',
|
|
||||||
async (payload: { id: string; editor: Editor; controller: DocumentController }, thunkAPI) => {
|
|
||||||
const { id, editor, controller } = payload;
|
|
||||||
const { dispatch } = thunkAPI;
|
|
||||||
|
|
||||||
const data = getHeadingDataFromEditor(editor);
|
|
||||||
if (!data) return;
|
|
||||||
await dispatch(
|
|
||||||
turnToBlockThunk({
|
|
||||||
id,
|
|
||||||
controller,
|
|
||||||
type: BlockType.HeadingBlock,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
@ -0,0 +1 @@
|
|||||||
|
export * from './text';
|
@ -1,31 +0,0 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
|
||||||
import { BlockType } from '$app/interfaces/document';
|
|
||||||
import { turnToBlockThunk } from '$app_reducers/document/async-actions/turn_to';
|
|
||||||
import { Editor } from 'slate';
|
|
||||||
import { getQuoteDataFromEditor } from '$app/utils/document/blocks/quote';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* transform to quote block
|
|
||||||
* 1. insert quote block after current block
|
|
||||||
* 2. move children to quote block
|
|
||||||
* 3. delete current block
|
|
||||||
*/
|
|
||||||
export const turnToQuoteBlockThunk = createAsyncThunk(
|
|
||||||
'document/turnToQuoteBlock',
|
|
||||||
async (payload: { id: string; editor: Editor; controller: DocumentController }, thunkAPI) => {
|
|
||||||
const { id, controller, editor } = payload;
|
|
||||||
const { dispatch } = thunkAPI;
|
|
||||||
const data = getQuoteDataFromEditor(editor);
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
await dispatch(
|
|
||||||
turnToBlockThunk({
|
|
||||||
id,
|
|
||||||
controller,
|
|
||||||
type: BlockType.QuoteBlock,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
@ -4,8 +4,8 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
|||||||
import { documentActions } from '$app_reducers/document/slice';
|
import { documentActions } from '$app_reducers/document/slice';
|
||||||
import { outdentNodeThunk } from './outdent';
|
import { outdentNodeThunk } from './outdent';
|
||||||
import { setCursorAfterThunk } from '../../cursor';
|
import { setCursorAfterThunk } from '../../cursor';
|
||||||
import { turnToTextBlockThunk } from '$app_reducers/document/async-actions/blocks/text/index';
|
|
||||||
import { getPrevLineId } from '$app/utils/document/blocks/common';
|
import { getPrevLineId } from '$app/utils/document/blocks/common';
|
||||||
|
import { turnToTextBlockThunk } from '$app_reducers/document/async-actions/blocks/text/turn_to';
|
||||||
|
|
||||||
const composeNodeThunk = createAsyncThunk(
|
const composeNodeThunk = createAsyncThunk(
|
||||||
'document/composeNode',
|
'document/composeNode',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BlockType, DocumentState } from '$app/interfaces/document';
|
import { DocumentState } from '$app/interfaces/document';
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { allowedChildrenBlockTypes } from '$app/constants/document/config';
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
|
|
||||||
export const indentNodeThunk = createAsyncThunk(
|
export const indentNodeThunk = createAsyncThunk(
|
||||||
'document/indentNode',
|
'document/indentNode',
|
||||||
@ -20,7 +20,8 @@ export const indentNodeThunk = createAsyncThunk(
|
|||||||
const newParentId = children[index - 1];
|
const newParentId = children[index - 1];
|
||||||
const prevNode = state.nodes[newParentId];
|
const prevNode = state.nodes[newParentId];
|
||||||
// check if prev node is allowed to have children
|
// check if prev node is allowed to have children
|
||||||
if (!allowedChildrenBlockTypes.includes(prevNode.type)) return;
|
const config = blockConfig[prevNode.type];
|
||||||
|
if (!config.canAddChild) return;
|
||||||
// check if prev node has children and get last child for new prev node
|
// check if prev node has children and get last child for new prev node
|
||||||
const prevNodeChildren = state.children[prevNode.children];
|
const prevNodeChildren = state.children[prevNode.children];
|
||||||
const newPrevId = prevNodeChildren[prevNodeChildren.length - 1];
|
const newPrevId = prevNodeChildren[prevNodeChildren.length - 1];
|
||||||
|
@ -1,32 +1,8 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
export * from './delete';
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
export * from './indent';
|
||||||
import { BlockType, DocumentState } from '$app/interfaces/document';
|
export * from './insert';
|
||||||
import { turnToBlockThunk } from '$app_reducers/document/async-actions/turn_to';
|
export * from './backspace';
|
||||||
|
export * from './outdent';
|
||||||
/**
|
export * from './split';
|
||||||
* transform to text block
|
export * from './turn_to';
|
||||||
* 1. insert text block after current block
|
export * from './update';
|
||||||
* 2. move children to text block
|
|
||||||
* 3. delete current block
|
|
||||||
*/
|
|
||||||
export const turnToTextBlockThunk = createAsyncThunk(
|
|
||||||
'document/turnToTextBlock',
|
|
||||||
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
|
||||||
const { id, controller } = payload;
|
|
||||||
const { dispatch, getState } = thunkAPI;
|
|
||||||
const state = (getState() as { document: DocumentState }).document;
|
|
||||||
const node = state.nodes[id];
|
|
||||||
const data = {
|
|
||||||
delta: node.data.delta,
|
|
||||||
};
|
|
||||||
|
|
||||||
await dispatch(
|
|
||||||
turnToBlockThunk({
|
|
||||||
id,
|
|
||||||
controller,
|
|
||||||
type: BlockType.TextBlock,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { BlockType, DocumentState, TextDelta } from '$app/interfaces/document';
|
import { DocumentState, TextDelta } from '$app/interfaces/document';
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { documentActions } from '$app_reducers/document/slice';
|
import { documentActions } from '$app_reducers/document/slice';
|
||||||
import { setCursorBeforeThunk } from '../../cursor';
|
import { setCursorBeforeThunk } from '../../cursor';
|
||||||
import { getDefaultBlockData, newBlock } from '$app/utils/document/blocks/common';
|
import { getDefaultBlockData, newBlock } from '$app/utils/document/blocks/common';
|
||||||
import { splitableBlockTypes } from '$app/constants/document/config';
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
|
|
||||||
export const splitNodeThunk = createAsyncThunk(
|
export const splitNodeThunk = createAsyncThunk(
|
||||||
'document/splitNode',
|
'document/splitNode',
|
||||||
@ -18,10 +18,11 @@ export const splitNodeThunk = createAsyncThunk(
|
|||||||
const node = state.nodes[id];
|
const node = state.nodes[id];
|
||||||
if (!node.parent) return;
|
if (!node.parent) return;
|
||||||
const children = state.children[node.children];
|
const children = state.children[node.children];
|
||||||
const prevId = children.length > 0 ? null : node.id;
|
const prevId = node.id;
|
||||||
const parent = children.length > 0 ? node : state.nodes[node.parent];
|
const parent = state.nodes[node.parent];
|
||||||
|
|
||||||
const newNodeType = splitableBlockTypes.includes(node.type) ? node.type : BlockType.TextBlock;
|
const config = blockConfig[node.type];
|
||||||
|
const newNodeType = config.splitType;
|
||||||
const defaultData = getDefaultBlockData(newNodeType);
|
const defaultData = getDefaultBlockData(newNodeType);
|
||||||
const newNode = newBlock<any>(newNodeType, parent.id, {
|
const newNode = newBlock<any>(newNodeType, parent.id, {
|
||||||
...defaultData,
|
...defaultData,
|
||||||
@ -34,7 +35,14 @@ export const splitNodeThunk = createAsyncThunk(
|
|||||||
delta: retain,
|
delta: retain,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await controller.applyActions([controller.getInsertAction(newNode, prevId), controller.getUpdateAction(retainNode)]);
|
const insertAction = controller.getInsertAction(newNode, prevId);
|
||||||
|
const updateAction = controller.getUpdateAction(retainNode);
|
||||||
|
const moveChildrenAction = controller.getMoveChildrenAction(
|
||||||
|
children.map((id) => state.nodes[id]),
|
||||||
|
newNode.id,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
await controller.applyActions([insertAction, ...moveChildrenAction, updateAction]);
|
||||||
// update local node data
|
// update local node data
|
||||||
dispatch(documentActions.updateNodeData({ id: retainNode.id, data: { delta: retain } }));
|
dispatch(documentActions.updateNodeData({ id: retainNode.id, data: { delta: retain } }));
|
||||||
// set cursor
|
// set cursor
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { BlockType, DocumentState } from '$app/interfaces/document';
|
||||||
|
import { turnToBlockThunk } from '$app_reducers/document/async-actions/turn_to';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* transform to text block
|
||||||
|
* 1. insert text block after current block
|
||||||
|
* 2. move children to text block
|
||||||
|
* 3. delete current block
|
||||||
|
*/
|
||||||
|
export const turnToTextBlockThunk = createAsyncThunk(
|
||||||
|
'document/turnToTextBlock',
|
||||||
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
|
const { id, controller } = payload;
|
||||||
|
const { dispatch, getState } = thunkAPI;
|
||||||
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
|
const node = state.nodes[id];
|
||||||
|
const data = {
|
||||||
|
delta: node.data.delta,
|
||||||
|
};
|
||||||
|
|
||||||
|
await dispatch(
|
||||||
|
turnToBlockThunk({
|
||||||
|
id,
|
||||||
|
controller,
|
||||||
|
type: BlockType.TextBlock,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -9,11 +9,11 @@ export const updateNodeDeltaThunk = createAsyncThunk(
|
|||||||
const { id, delta, controller } = payload;
|
const { id, delta, controller } = payload;
|
||||||
const { dispatch, getState } = thunkAPI;
|
const { dispatch, getState } = thunkAPI;
|
||||||
const state = (getState() as { document: DocumentState }).document;
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
|
const node = state.nodes[id];
|
||||||
// The block map should be updated immediately
|
// The block map should be updated immediately
|
||||||
// or the component will use the old data to update the editor
|
// or the component will use the old data to update the editor
|
||||||
dispatch(documentActions.updateNodeData({ id, data: { delta } }));
|
dispatch(documentActions.updateNodeData({ id, data: { delta } }));
|
||||||
|
|
||||||
const node = state.nodes[id];
|
|
||||||
// the transaction is delayed to avoid too many updates
|
// the transaction is delayed to avoid too many updates
|
||||||
debounceApplyUpdate(controller, {
|
debounceApplyUpdate(controller, {
|
||||||
...node,
|
...node,
|
||||||
@ -47,17 +47,16 @@ export const updateNodeDataThunk = createAsyncThunk<
|
|||||||
const { id, data, controller } = payload;
|
const { id, data, controller } = payload;
|
||||||
const { dispatch, getState } = thunkAPI;
|
const { dispatch, getState } = thunkAPI;
|
||||||
const state = (getState() as { document: DocumentState }).document;
|
const state = (getState() as { document: DocumentState }).document;
|
||||||
|
|
||||||
dispatch(documentActions.updateNodeData({ id, data: { ...data } }));
|
|
||||||
|
|
||||||
const node = state.nodes[id];
|
const node = state.nodes[id];
|
||||||
|
|
||||||
|
const newData = { ...node.data, ...data };
|
||||||
|
|
||||||
|
dispatch(documentActions.updateNodeData({ id, data: newData }));
|
||||||
|
|
||||||
await controller.applyActions([
|
await controller.applyActions([
|
||||||
controller.getUpdateAction({
|
controller.getUpdateAction({
|
||||||
...node,
|
...node,
|
||||||
data: {
|
data: newData,
|
||||||
...node.data,
|
|
||||||
...data,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
|
||||||
import { BlockType } from '$app/interfaces/document';
|
|
||||||
import { turnToBlockThunk } from '$app_reducers/document/async-actions/turn_to';
|
|
||||||
import { Editor } from 'slate';
|
|
||||||
import { getTodoListDataFromEditor } from '$app/utils/document/blocks/todo_list';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* transform to todolist block
|
|
||||||
* 1. insert todolist block after current block
|
|
||||||
* 2. move children to todolist block
|
|
||||||
* 3. delete current block
|
|
||||||
*/
|
|
||||||
export const turnToTodoListBlockThunk = createAsyncThunk(
|
|
||||||
'document/turnToTodoListBlock',
|
|
||||||
async (payload: { id: string; editor: Editor; controller: DocumentController }, thunkAPI) => {
|
|
||||||
const { id, controller, editor } = payload;
|
|
||||||
const { dispatch } = thunkAPI;
|
|
||||||
const data = getTodoListDataFromEditor(editor);
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
await dispatch(
|
|
||||||
turnToBlockThunk({
|
|
||||||
id,
|
|
||||||
controller,
|
|
||||||
type: BlockType.TodoListBlock,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
@ -95,6 +95,7 @@ export const setCursorNextLineThunk = createAsyncThunk(
|
|||||||
|
|
||||||
// set the cursor to next line with the relative offset
|
// set the cursor to next line with the relative offset
|
||||||
const newSelection = getStartLineSelectionByOffset(delta, textOffset);
|
const newSelection = getStartLineSelectionByOffset(delta, textOffset);
|
||||||
|
|
||||||
dispatch(documentActions.setTextSelection({ blockId: nextLineNode.id, selection: newSelection }));
|
dispatch(documentActions.setTextSelection({ blockId: nextLineNode.id, selection: newSelection }));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
export * from './blocks/text/delete';
|
|
||||||
export * from './blocks/text/indent';
|
|
||||||
export * from './blocks/text/insert';
|
|
||||||
export * from './blocks/text/backspace';
|
|
||||||
export * from './blocks/text/outdent';
|
|
||||||
export * from './blocks/text/split';
|
|
||||||
export * from './cursor';
|
export * from './cursor';
|
||||||
|
export * from './blocks';
|
||||||
|
export * from './turn_to';
|
||||||
|
@ -2,9 +2,17 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
|||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { BlockData, BlockType, DocumentState, NestedBlock } from '$app/interfaces/document';
|
import { BlockData, BlockType, DocumentState, NestedBlock } from '$app/interfaces/document';
|
||||||
import { setCursorBeforeThunk } from '$app_reducers/document/async-actions/cursor';
|
import { setCursorBeforeThunk } from '$app_reducers/document/async-actions/cursor';
|
||||||
import { allowedChildrenBlockTypes } from '$app/constants/document/config';
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
import { newBlock } from '$app/utils/document/blocks/common';
|
import { newBlock } from '$app/utils/document/blocks/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* transform to block
|
||||||
|
* 1. insert block after current block
|
||||||
|
* 2. move all children
|
||||||
|
* - if new block is not allowed to have children, move children to parent
|
||||||
|
* - otherwise, move children to new block
|
||||||
|
* 3. delete current block
|
||||||
|
*/
|
||||||
export const turnToBlockThunk = createAsyncThunk(
|
export const turnToBlockThunk = createAsyncThunk(
|
||||||
'document/turnToBlock',
|
'document/turnToBlock',
|
||||||
async (payload: { id: string; controller: DocumentController; type: BlockType; data: BlockData<any> }, thunkAPI) => {
|
async (payload: { id: string; controller: DocumentController; type: BlockType; data: BlockData<any> }, thunkAPI) => {
|
||||||
@ -18,19 +26,14 @@ export const turnToBlockThunk = createAsyncThunk(
|
|||||||
const parent = state.nodes[node.parent];
|
const parent = state.nodes[node.parent];
|
||||||
const children = state.children[node.children].map((id) => state.nodes[id]);
|
const children = state.children[node.children].map((id) => state.nodes[id]);
|
||||||
|
|
||||||
/**
|
|
||||||
* transform to block
|
|
||||||
* 1. insert block after current block
|
|
||||||
* 2. move all children
|
|
||||||
* 3. delete current block
|
|
||||||
*/
|
|
||||||
|
|
||||||
const block = newBlock<any>(type, parent.id, data);
|
const block = newBlock<any>(type, parent.id, data);
|
||||||
// insert new block after current block
|
// insert new block after current block
|
||||||
const insertHeadingAction = controller.getInsertAction(block, node.id);
|
const insertHeadingAction = controller.getInsertAction(block, node.id);
|
||||||
|
|
||||||
|
// check if prev node is allowed to have children
|
||||||
|
const config = blockConfig[block.type];
|
||||||
// if new block is not allowed to have children, move children to parent
|
// if new block is not allowed to have children, move children to parent
|
||||||
const newParent = allowedChildrenBlockTypes.includes(block.type) ? block : parent;
|
const newParent = config.canAddChild ? block : parent;
|
||||||
// if move children to parent, set prev to current block, otherwise the prev is empty
|
// if move children to parent, set prev to current block, otherwise the prev is empty
|
||||||
const newPrev = newParent.id === parent.id ? block.id : '';
|
const newPrev = newParent.id === parent.id ? block.id : '';
|
||||||
const moveChildrenActions = controller.getMoveChildrenAction(children, newParent.id, newPrev);
|
const moveChildrenActions = controller.getMoveChildrenAction(children, newParent.id, newPrev);
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import { Editor } from 'slate';
|
|
||||||
import { getAfterRangeAt, getBeforeRangeAt } from '$app/utils/document/slate/text';
|
|
||||||
import { HeadingBlockData } from '$app/interfaces/document';
|
|
||||||
import { getDeltaAfterSelection, getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get heading data from editor, only support markdown
|
|
||||||
* @param editor
|
|
||||||
*/
|
|
||||||
export function getHeadingDataFromEditor(editor: Editor): HeadingBlockData | undefined {
|
|
||||||
const selection = editor.selection;
|
|
||||||
if (!selection) return;
|
|
||||||
const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection));
|
|
||||||
const level = hashTags.match(/#/g)?.length;
|
|
||||||
if (!level) return;
|
|
||||||
const delta = getDeltaAfterSelection(editor);
|
|
||||||
if (!delta) return;
|
|
||||||
return {
|
|
||||||
level,
|
|
||||||
delta,
|
|
||||||
};
|
|
||||||
}
|
|
@ -0,0 +1,52 @@
|
|||||||
|
import { Editor } from 'slate';
|
||||||
|
import { HeadingBlockData, TodoListBlockData } from '$app/interfaces/document';
|
||||||
|
import { getAfterRangeAt, getBeforeRangeAt } from '$app/utils/document/slate/text';
|
||||||
|
import { getDeltaAfterSelection, getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get heading data from editor, only support markdown
|
||||||
|
* @param editor
|
||||||
|
*/
|
||||||
|
export function getHeadingDataFromEditor(editor: Editor): HeadingBlockData | undefined {
|
||||||
|
const selection = editor.selection;
|
||||||
|
if (!selection) return;
|
||||||
|
const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection));
|
||||||
|
const level = hashTags.match(/#/g)?.length;
|
||||||
|
if (!level) return;
|
||||||
|
const delta = getDeltaAfterSelection(editor);
|
||||||
|
if (!delta) return;
|
||||||
|
return {
|
||||||
|
level,
|
||||||
|
delta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get quote data from editor, only support markdown
|
||||||
|
* @param editor
|
||||||
|
*/
|
||||||
|
export function getQuoteDataFromEditor(editor: Editor) {
|
||||||
|
const delta = getDeltaAfterSelection(editor);
|
||||||
|
if (!delta) return;
|
||||||
|
return {
|
||||||
|
delta,
|
||||||
|
size: 'default',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get todo_list data from editor, only support markdown
|
||||||
|
* @param editor
|
||||||
|
*/
|
||||||
|
export function getTodoListDataFromEditor(editor: Editor): TodoListBlockData | undefined {
|
||||||
|
const selection = editor.selection;
|
||||||
|
if (!selection) return;
|
||||||
|
const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection));
|
||||||
|
const checked = hashTags.match(/x/g)?.length;
|
||||||
|
const slateNodes = Editor.fragment(editor, getAfterRangeAt(editor, selection));
|
||||||
|
const delta = getDeltaFromSlateNodes(slateNodes);
|
||||||
|
return {
|
||||||
|
delta,
|
||||||
|
checked: !!checked,
|
||||||
|
};
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
import { Editor } from 'slate';
|
|
||||||
import { getDeltaAfterSelection } from '$app/utils/document/blocks/common';
|
|
||||||
|
|
||||||
export function getQuoteDataFromEditor(editor: Editor) {
|
|
||||||
const delta = getDeltaAfterSelection(editor);
|
|
||||||
if (!delta) return;
|
|
||||||
return {
|
|
||||||
delta,
|
|
||||||
size: 'default',
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,6 +1,16 @@
|
|||||||
import { BlockType, NestedBlock, TextBlockData } from '$app/interfaces/document';
|
import { BlockType, NestedBlock, TextBlockData, TextDelta } from '$app/interfaces/document';
|
||||||
import { newBlock } from '$app/utils/document/blocks/common';
|
import { newBlock } from '$app/utils/document/blocks/common';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
export function newTextBlock(parentId: string, data: TextBlockData): NestedBlock {
|
export function newTextBlock(parentId: string, data: TextBlockData): NestedBlock {
|
||||||
return newBlock<BlockType.TextBlock>(BlockType.TextBlock, parentId, data);
|
return newBlock<BlockType.TextBlock>(BlockType.TextBlock, parentId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSameDelta(referDelta: TextDelta[], delta: TextDelta[]) {
|
||||||
|
const ydoc = new Y.Doc();
|
||||||
|
const yText = ydoc.getText('1');
|
||||||
|
const yTextRefer = ydoc.getText('2');
|
||||||
|
yText.applyDelta(delta);
|
||||||
|
yTextRefer.applyDelta(referDelta);
|
||||||
|
return JSON.stringify(yText.toDelta()) === JSON.stringify(yTextRefer.toDelta());
|
||||||
|
}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { Editor } from 'slate';
|
|
||||||
import { TodoListBlockData } from '$app/interfaces/document';
|
|
||||||
import { getAfterRangeAt, getBeforeRangeAt } from '$app/utils/document/slate/text';
|
|
||||||
import { getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get todo_list data from editor, only support markdown
|
|
||||||
* @param editor
|
|
||||||
*/
|
|
||||||
export function getTodoListDataFromEditor(editor: Editor): TodoListBlockData | undefined {
|
|
||||||
const selection = editor.selection;
|
|
||||||
if (!selection) return;
|
|
||||||
const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection));
|
|
||||||
const checked = hashTags.match(/x/g)?.length;
|
|
||||||
const slateNodes = Editor.fragment(editor, getAfterRangeAt(editor, selection));
|
|
||||||
const delta = getDeltaFromSlateNodes(slateNodes);
|
|
||||||
return {
|
|
||||||
delta,
|
|
||||||
checked: !!checked,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
|
|
||||||
import { getBeforeRangeAt } from '$app/utils/document/slate/text';
|
|
||||||
import { Editor } from 'slate';
|
|
||||||
|
|
||||||
export function canHandleToHeadingBlock(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor): boolean {
|
|
||||||
const flag = getMarkdownFlag(event, editor);
|
|
||||||
if (!flag) return false;
|
|
||||||
const isHeadingMarkdown = /^(#{1,3})$/.test(flag);
|
|
||||||
|
|
||||||
return isHeadingMarkdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canHandleToCheckboxBlock(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
|
||||||
const flag = getMarkdownFlag(event, editor);
|
|
||||||
if (!flag) return false;
|
|
||||||
|
|
||||||
const isCheckboxMarkdown = /^((-)?\[(x|\s)?\])$/.test(flag);
|
|
||||||
return isCheckboxMarkdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canHandleToQuoteBlock(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
|
||||||
const flag = getMarkdownFlag(event, editor);
|
|
||||||
if (!flag) return false;
|
|
||||||
|
|
||||||
const isQuoteMarkdown = /^("|“|”)$/.test(flag);
|
|
||||||
|
|
||||||
return isQuoteMarkdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMarkdownFlag(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
|
||||||
const isSpaceKey = event.key === keyBoardEventKeyMap.Space;
|
|
||||||
const selection = editor.selection;
|
|
||||||
|
|
||||||
if (!isSpaceKey || !selection) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Editor.string(editor, getBeforeRangeAt(editor, selection)).trim();
|
|
||||||
}
|
|
@ -41,8 +41,7 @@ export const useDocument = () => {
|
|||||||
}
|
}
|
||||||
Log.debug('close document', params.id);
|
Log.debug('close document', params.id);
|
||||||
};
|
};
|
||||||
// dispose controller before unload
|
|
||||||
window.addEventListener('beforeunload', closeDocument);
|
|
||||||
return closeDocument;
|
return closeDocument;
|
||||||
}, [params.id]);
|
}, [params.id]);
|
||||||
|
|
||||||
|
@ -14,7 +14,10 @@ lazy_static! {
|
|||||||
pub fn register_notification_sender<T: NotificationSender>(sender: T) {
|
pub fn register_notification_sender<T: NotificationSender>(sender: T) {
|
||||||
let box_sender = Box::new(sender);
|
let box_sender = Box::new(sender);
|
||||||
match NOTIFICATION_SENDER.write() {
|
match NOTIFICATION_SENDER.write() {
|
||||||
Ok(mut write_guard) => write_guard.push(box_sender),
|
Ok(mut write_guard) => {
|
||||||
|
write_guard.pop();
|
||||||
|
write_guard.push(box_sender)
|
||||||
|
},
|
||||||
Err(err) => tracing::error!("Failed to push notification sender: {:?}", err),
|
Err(err) => tracing::error!("Failed to push notification sender: {:?}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user