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',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
plugins: ['@typescript-eslint', "react-hooks"],
|
||||
rules: {
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
||||
'@typescript-eslint/no-empty-function': 'error',
|
||||
'@typescript-eslint/no-empty-interface': 'warn',
|
||||
|
@ -66,6 +66,7 @@
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "2.8.4",
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
|
@ -25,6 +25,7 @@ specifiers:
|
||||
dayjs: ^1.11.7
|
||||
eslint: ^8.34.0
|
||||
eslint-plugin-react: ^7.32.2
|
||||
eslint-plugin-react-hooks: ^4.6.0
|
||||
events: ^3.3.0
|
||||
google-protobuf: ^3.21.2
|
||||
i18next: ^22.4.10
|
||||
@ -110,6 +111,7 @@ devDependencies:
|
||||
autoprefixer: 10.4.13_postcss@8.4.21
|
||||
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
|
||||
prettier: 2.8.4
|
||||
prettier-plugin-tailwindcss: 0.2.4_prettier@2.8.4
|
||||
@ -2426,6 +2428,15 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -1,31 +1,6 @@
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { Editor } from 'slate';
|
||||
import { TextBlockKeyEventHandlerParams, TextDelta, TextSelection } from '$app/interfaces/document';
|
||||
import { useCallback } from 'react';
|
||||
import { useTextInput } from '../_shared/TextInput.hooks';
|
||||
import { useAppDispatch } from '@/appflowy_app/stores/store';
|
||||
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';
|
||||
import { useTextBlockKeyEvent } from '$app/components/document/TextBlock/events/Events.hooks';
|
||||
|
||||
export function useTextBlock(id: string) {
|
||||
const { editor, onChange, value } = useTextInput(id);
|
||||
@ -55,177 +30,3 @@ export function useTextBlock(id: string) {
|
||||
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 Leaf from './Leaf';
|
||||
import { useTextBlock } from './TextBlock.hooks';
|
||||
import NodeComponent from '../Node';
|
||||
import BlockHorizontalToolbar from '../BlockHorizontalToolbar';
|
||||
import React from 'react';
|
||||
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 { deltaToSlateValue, getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
|
||||
import { documentActions } from '@/appflowy_app/stores/reducers/document/slice';
|
||||
import { isSameDelta } from '$app/utils/document/blocks/text';
|
||||
|
||||
export function useTextInput(id: string) {
|
||||
const dispatch = useAppDispatch();
|
||||
@ -25,27 +26,18 @@ export function useTextInput(id: string) {
|
||||
|
||||
const { editor, yText } = useBindYjs(id, delta);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(documentActions.removeTextSelection(id));
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
const [value, setValue] = useState<Descendant[]>([]);
|
||||
|
||||
const storeSelection = useCallback(() => {
|
||||
// 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
|
||||
if (!ReactEditor.isFocused(editor) || !editor.selection || !editor.selection.anchor || !editor.selection.focus)
|
||||
return;
|
||||
const { anchor, focus } = editor.selection;
|
||||
const selection = { anchor, focus } as TextSelection;
|
||||
if (!ReactEditor.isFocused(editor)) return;
|
||||
const selection = editor.selection as TextSelection;
|
||||
dispatch(documentActions.setTextSelection({ blockId: id, selection }));
|
||||
}, [editor]);
|
||||
|
||||
const currentSelection = useAppSelector((state) => state.document.textSelections[id]);
|
||||
const restoreSelection = useCallback(() => {
|
||||
if (editor.selection && JSON.stringify(currentSelection) === JSON.stringify(editor.selection)) return;
|
||||
setSelection(editor, currentSelection);
|
||||
}, [editor, currentSelection]);
|
||||
|
||||
@ -54,13 +46,15 @@ export function useTextInput(id: string) {
|
||||
setValue(e);
|
||||
storeSelection();
|
||||
},
|
||||
|
||||
[storeSelection]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
restoreSelection();
|
||||
}, [restoreSelection]);
|
||||
return () => {
|
||||
dispatch(documentActions.removeTextSelection(id));
|
||||
};
|
||||
}, [id, restoreSelection]);
|
||||
|
||||
if (editor.selection && ReactEditor.isFocused(editor)) {
|
||||
const domSelection = window.getSelection();
|
||||
@ -128,12 +122,13 @@ function useBindYjs(id: string, delta: TextDelta[]) {
|
||||
if (!yText) return;
|
||||
|
||||
// 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)) {
|
||||
yText.delete(0, yText.length);
|
||||
yText.applyDelta(delta);
|
||||
// It should be noted that the selection will be lost after the yText is updated
|
||||
setSelection(editor, currentSelection);
|
||||
}
|
||||
const isSame = isSameDelta(delta, yText.toDelta());
|
||||
if (isSame) return;
|
||||
|
||||
yText.delete(0, yText.length);
|
||||
yText.applyDelta(delta);
|
||||
// It should be noted that the selection will be lost after the yText is updated
|
||||
setSelection(editor, currentSelection);
|
||||
}, [delta, currentSelection, editor]);
|
||||
|
||||
return { editor, yText: yTextRef.current };
|
||||
@ -167,7 +162,6 @@ function setSelection(editor: ReactEditor, currentSelection: TextSelection) {
|
||||
if (!currentSelection || !currentSelection.anchor || !currentSelection.focus) {
|
||||
if (ReactEditor.isFocused(editor)) {
|
||||
ReactEditor.blur(editor);
|
||||
ReactEditor.deselect(editor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -178,12 +172,12 @@ function setSelection(editor: ReactEditor, currentSelection: TextSelection) {
|
||||
}
|
||||
|
||||
const { path, offset } = currentSelection.focus;
|
||||
// It is possible that the current selection is out of range
|
||||
const children = getDeltaFromSlateNodes(editor.children);
|
||||
|
||||
// the path always has 2 elements,
|
||||
// because the slate node is a two-dimensional array
|
||||
const index = path[1];
|
||||
// It is possible that the current selection is out of range
|
||||
if (children[index].insert.length < offset) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,17 +1,75 @@
|
||||
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 = [
|
||||
BlockType.TextBlock,
|
||||
BlockType.PageBlock,
|
||||
BlockType.TodoListBlock,
|
||||
BlockType.QuoteBlock,
|
||||
BlockType.CalloutBlock,
|
||||
];
|
||||
|
||||
/**
|
||||
* Block types that split node can extend to the next line
|
||||
*/
|
||||
export const splitableBlockTypes = [BlockType.TextBlock, BlockType.TodoListBlock];
|
||||
export const blockConfig: Record<
|
||||
string,
|
||||
{
|
||||
/**
|
||||
* Whether the block can have children
|
||||
*/
|
||||
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',
|
||||
TextBlock = 'text',
|
||||
TodoListBlock = 'todo_list',
|
||||
BulletedListBlock = 'bulleted_list',
|
||||
NumberedListBlock = 'numbered_list',
|
||||
CodeBlock = 'code',
|
||||
EmbedBlock = 'embed',
|
||||
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 { get } from '@/appflowy_app/utils/tool';
|
||||
import { blockPB2Node } from '$app/utils/document/blocks/common';
|
||||
import { Log } from '$app/utils/log';
|
||||
|
||||
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
||||
|
||||
@ -65,6 +66,7 @@ export class DocumentController {
|
||||
};
|
||||
|
||||
applyActions = async (actions: ReturnType<typeof BlockActionPB.prototype.toObject>[]) => {
|
||||
Log.debug('applyActions', actions);
|
||||
await this.backendService.applyActions(actions);
|
||||
};
|
||||
|
||||
@ -153,6 +155,7 @@ export class DocumentController {
|
||||
if (!this.onDocChange) return;
|
||||
const { events, is_remote } = DocEventPB.deserializeBinary(payload);
|
||||
|
||||
Log.debug('DocumentController', 'updated', { events, is_remote });
|
||||
events.forEach((blockEvent) => {
|
||||
blockEvent.event.forEach((_payload) => {
|
||||
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 { 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 { outdentNodeThunk } from './outdent';
|
||||
import { setCursorAfterThunk } from '../../cursor';
|
||||
import { turnToTextBlockThunk } from '$app_reducers/document/async-actions/blocks/text/index';
|
||||
import { getPrevLineId } from '$app/utils/document/blocks/common';
|
||||
import { turnToTextBlockThunk } from '$app_reducers/document/async-actions/blocks/text/turn_to';
|
||||
|
||||
const composeNodeThunk = createAsyncThunk(
|
||||
'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 { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { allowedChildrenBlockTypes } from '$app/constants/document/config';
|
||||
import { blockConfig } from '$app/constants/document/config';
|
||||
|
||||
export const indentNodeThunk = createAsyncThunk(
|
||||
'document/indentNode',
|
||||
@ -20,7 +20,8 @@ export const indentNodeThunk = createAsyncThunk(
|
||||
const newParentId = children[index - 1];
|
||||
const prevNode = state.nodes[newParentId];
|
||||
// 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
|
||||
const prevNodeChildren = state.children[prevNode.children];
|
||||
const newPrevId = prevNodeChildren[prevNodeChildren.length - 1];
|
||||
|
@ -1,32 +1,8 @@
|
||||
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,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
export * from './delete';
|
||||
export * from './indent';
|
||||
export * from './insert';
|
||||
export * from './backspace';
|
||||
export * from './outdent';
|
||||
export * from './split';
|
||||
export * from './turn_to';
|
||||
export * from './update';
|
||||
|
@ -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 { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { documentActions } from '$app_reducers/document/slice';
|
||||
import { setCursorBeforeThunk } from '../../cursor';
|
||||
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(
|
||||
'document/splitNode',
|
||||
@ -18,10 +18,11 @@ export const splitNodeThunk = createAsyncThunk(
|
||||
const node = state.nodes[id];
|
||||
if (!node.parent) return;
|
||||
const children = state.children[node.children];
|
||||
const prevId = children.length > 0 ? null : node.id;
|
||||
const parent = children.length > 0 ? node : state.nodes[node.parent];
|
||||
const prevId = node.id;
|
||||
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 newNode = newBlock<any>(newNodeType, parent.id, {
|
||||
...defaultData,
|
||||
@ -34,7 +35,14 @@ export const splitNodeThunk = createAsyncThunk(
|
||||
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
|
||||
dispatch(documentActions.updateNodeData({ id: retainNode.id, data: { delta: retain } }));
|
||||
// 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 { dispatch, getState } = thunkAPI;
|
||||
const state = (getState() as { document: DocumentState }).document;
|
||||
const node = state.nodes[id];
|
||||
// The block map should be updated immediately
|
||||
// or the component will use the old data to update the editor
|
||||
dispatch(documentActions.updateNodeData({ id, data: { delta } }));
|
||||
|
||||
const node = state.nodes[id];
|
||||
// the transaction is delayed to avoid too many updates
|
||||
debounceApplyUpdate(controller, {
|
||||
...node,
|
||||
@ -47,17 +47,16 @@ export const updateNodeDataThunk = createAsyncThunk<
|
||||
const { id, data, controller } = payload;
|
||||
const { dispatch, getState } = thunkAPI;
|
||||
const state = (getState() as { document: DocumentState }).document;
|
||||
|
||||
dispatch(documentActions.updateNodeData({ id, data: { ...data } }));
|
||||
|
||||
const node = state.nodes[id];
|
||||
|
||||
const newData = { ...node.data, ...data };
|
||||
|
||||
dispatch(documentActions.updateNodeData({ id, data: newData }));
|
||||
|
||||
await controller.applyActions([
|
||||
controller.getUpdateAction({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
...data,
|
||||
},
|
||||
data: newData,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
@ -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
|
||||
const newSelection = getStartLineSelectionByOffset(delta, textOffset);
|
||||
|
||||
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 './blocks';
|
||||
export * from './turn_to';
|
||||
|
@ -2,9 +2,17 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { BlockData, BlockType, DocumentState, NestedBlock } from '$app/interfaces/document';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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(
|
||||
'document/turnToBlock',
|
||||
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 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);
|
||||
// insert new block after current block
|
||||
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
|
||||
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
|
||||
const newPrev = newParent.id === parent.id ? block.id : '';
|
||||
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 * as Y from 'yjs';
|
||||
|
||||
export function newTextBlock(parentId: string, data: TextBlockData): NestedBlock {
|
||||
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);
|
||||
};
|
||||
// dispose controller before unload
|
||||
window.addEventListener('beforeunload', closeDocument);
|
||||
|
||||
return closeDocument;
|
||||
}, [params.id]);
|
||||
|
||||
|
@ -14,7 +14,10 @@ lazy_static! {
|
||||
pub fn register_notification_sender<T: NotificationSender>(sender: T) {
|
||||
let box_sender = Box::new(sender);
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user