mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: checkbox block (#2413)
This commit is contained in:
parent
93d787a9ae
commit
7db36e3f1e
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import NodeComponent from '$app/components/document/Node/index';
|
||||
|
||||
function NodeChildren({ childIds }: { childIds?: string[] }) {
|
||||
return childIds && childIds.length > 0 ? (
|
||||
<div className='pl-[1.5em]'>
|
||||
{childIds.map((item) => (
|
||||
<NodeComponent key={item} id={item} />
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default NodeChildren;
|
@ -6,6 +6,7 @@ import TextBlock from '../TextBlock';
|
||||
import { NodeContext } from '../_shared/SubscribeNode.hooks';
|
||||
import { BlockType } from '$app/interfaces/document';
|
||||
import HeadingBlock from '$app/components/document/HeadingBlock';
|
||||
import TodoListBlock from '$app/components/document/TodoListBlock';
|
||||
|
||||
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const { node, childIds, isSelected, ref } = useNode(id);
|
||||
@ -18,6 +19,9 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
|
||||
case BlockType.HeadingBlock: {
|
||||
return <HeadingBlock node={node} />;
|
||||
}
|
||||
case BlockType.TodoListBlock: {
|
||||
return <TodoListBlock node={node} childIds={childIds} />;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -151,15 +151,15 @@ function useTextBlockKeyEvent(id: string, editor: Editor) {
|
||||
const keyEvents = [enterEvent, backSpaceEvent, tabEvent, upEvent, downEvent, leftEvent, rightEvent];
|
||||
|
||||
keyEvents.push(...markdownEvents);
|
||||
const matchKey = keyEvents.find((keyEvent) => keyEvent.canHandle(event, editor));
|
||||
if (!matchKey) {
|
||||
const matchKeys = keyEvents.filter((keyEvent) => keyEvent.canHandle(event, editor));
|
||||
if (matchKeys.length === 0) {
|
||||
triggerHotkey(event, editor);
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
matchKey.handler(event, editor);
|
||||
matchKeys.forEach((matchKey) => matchKey.handler(event, editor));
|
||||
},
|
||||
[editor, enterEvent, backSpaceEvent, tabEvent, upEvent, downEvent, leftEvent, rightEvent, markdownEvents]
|
||||
);
|
||||
|
@ -4,7 +4,8 @@ import { useTextBlock } from './TextBlock.hooks';
|
||||
import NodeComponent from '../Node';
|
||||
import BlockHorizontalToolbar from '../BlockHorizontalToolbar';
|
||||
import React from 'react';
|
||||
import { BlockType, NestedBlock } from '$app/interfaces/document';
|
||||
import { NestedBlock } from '$app/interfaces/document';
|
||||
import NodeChildren from '$app/components/document/Node/NodeChildren';
|
||||
|
||||
function TextBlock({
|
||||
node,
|
||||
@ -17,9 +18,11 @@ function TextBlock({
|
||||
placeholder?: string;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.id);
|
||||
const className = props.className !== undefined ? ` ${props.className}` : '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div {...props} className={`py-[2px] ${props.className}`}>
|
||||
<div {...props} className={`py-[2px]${className}`}>
|
||||
<Slate editor={editor} onChange={onChange} value={value}>
|
||||
<BlockHorizontalToolbar id={node.id} />
|
||||
<Editable
|
||||
@ -30,13 +33,7 @@ function TextBlock({
|
||||
/>
|
||||
</Slate>
|
||||
</div>
|
||||
{childIds && childIds.length > 0 ? (
|
||||
<div className='pl-[1.5em]'>
|
||||
{childIds.map((item) => (
|
||||
<NodeComponent key={item} id={item} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<NodeChildren childIds={childIds} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document';
|
||||
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
|
||||
import { canHandleToHeadingBlock } from '$app/utils/document/slate/markdown';
|
||||
import { canHandleToHeadingBlock, canHandleToCheckboxBlock } 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';
|
||||
|
||||
export function useMarkDown(id: string) {
|
||||
const { toHeadingBlockAction } = useActions(id);
|
||||
const { toHeadingBlockAction, toCheckboxBlockAction } = useActions(id);
|
||||
const toHeadingBlockEvent = useMemo(() => {
|
||||
return {
|
||||
triggerEventKey: keyBoardEventKeyMap.Space,
|
||||
@ -16,7 +17,18 @@ export function useMarkDown(id: string) {
|
||||
};
|
||||
}, [toHeadingBlockAction]);
|
||||
|
||||
const markdownEvents = useMemo(() => [toHeadingBlockEvent], [toHeadingBlockEvent]);
|
||||
const toCheckboxBlockEvent = useMemo(() => {
|
||||
return {
|
||||
triggerEventKey: keyBoardEventKeyMap.Space,
|
||||
canHandle: canHandleToCheckboxBlock,
|
||||
handler: toCheckboxBlockAction,
|
||||
};
|
||||
}, [toCheckboxBlockAction]);
|
||||
|
||||
const markdownEvents = useMemo(
|
||||
() => [toHeadingBlockEvent, toCheckboxBlockEvent],
|
||||
[toHeadingBlockEvent, toCheckboxBlockEvent]
|
||||
);
|
||||
|
||||
return {
|
||||
markdownEvents,
|
||||
@ -35,7 +47,17 @@ function useActions(id: string) {
|
||||
[controller, dispatch, id]
|
||||
);
|
||||
|
||||
const toCheckboxBlockAction = useCallback(
|
||||
(...args: TextBlockKeyEventHandlerParams) => {
|
||||
if (!controller) return;
|
||||
const [_event, editor] = args;
|
||||
dispatch(turnToTodoListBlockThunk({ id, controller, editor }));
|
||||
},
|
||||
[controller, dispatch, id]
|
||||
);
|
||||
|
||||
return {
|
||||
toHeadingBlockAction,
|
||||
toCheckboxBlockAction,
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||
import { updateNodeDataThunk } from '$app_reducers/document/async-actions/blocks/text/update';
|
||||
import { BlockData, BlockType } from '$app/interfaces/document';
|
||||
import isHotkey from 'is-hotkey';
|
||||
|
||||
export function useTodoListBlock(id: string, data: BlockData<BlockType.TodoListBlock>) {
|
||||
const dispatch = useAppDispatch();
|
||||
const controller = useContext(DocumentControllerContext);
|
||||
const toggleCheckbox = useCallback(() => {
|
||||
if (!controller) return;
|
||||
void dispatch(
|
||||
updateNodeDataThunk({
|
||||
id,
|
||||
controller,
|
||||
data: {
|
||||
checked: !data.checked,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [controller, dispatch, id, data.checked]);
|
||||
|
||||
const handleShortcut = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
// Accepts mod for the classic "cmd on Mac, ctrl on Windows" use case.
|
||||
if (isHotkey('mod+enter', event)) {
|
||||
toggleCheckbox();
|
||||
}
|
||||
},
|
||||
[toggleCheckbox]
|
||||
);
|
||||
|
||||
return {
|
||||
toggleCheckbox,
|
||||
handleShortcut,
|
||||
};
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { BlockType, NestedBlock } from '$app/interfaces/document';
|
||||
import TextBlock from '$app/components/document/TextBlock';
|
||||
import { useTodoListBlock } from '$app/components/document/TodoListBlock/TodoListBlock.hooks';
|
||||
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
||||
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||
import React from 'react';
|
||||
import NodeChildren from '$app/components/document/Node/NodeChildren';
|
||||
|
||||
export default function TodoListBlock({
|
||||
node,
|
||||
childIds,
|
||||
}: {
|
||||
node: NestedBlock<BlockType.TodoListBlock>;
|
||||
childIds?: string[];
|
||||
}) {
|
||||
const { id, data } = node;
|
||||
const { toggleCheckbox, handleShortcut } = useTodoListBlock(id, node.data);
|
||||
|
||||
const checked = !!data.checked;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex'} onKeyDownCapture={handleShortcut}>
|
||||
<div className={'flex h-[calc(1.5em_+_2px)] w-[24px] select-none items-center justify-start'}>
|
||||
<div className={'relative flex h-4 w-4 items-center justify-start transition'}>
|
||||
<div>{checked ? <EditorCheckSvg /> : <EditorUncheckSvg />}</div>
|
||||
<input
|
||||
type={'checkbox'}
|
||||
checked={checked}
|
||||
onChange={toggleCheckbox}
|
||||
className={'absolute h-[100%] w-[100%] cursor-pointer opacity-0'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex-1'}>
|
||||
<TextBlock node={node} />
|
||||
</div>
|
||||
</div>
|
||||
<NodeChildren childIds={childIds} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -3,4 +3,9 @@ import { BlockType } from '$app/interfaces/document';
|
||||
/**
|
||||
* Block types that are allowed to have children
|
||||
*/
|
||||
export const allowedChildrenBlockTypes = [BlockType.TextBlock, BlockType.PageBlock];
|
||||
export const allowedChildrenBlockTypes = [BlockType.TextBlock, BlockType.PageBlock, BlockType.TodoListBlock];
|
||||
|
||||
/**
|
||||
* Block types that split node can extend to the next line
|
||||
*/
|
||||
export const splitableBlockTypes = [BlockType.TextBlock, BlockType.TodoListBlock];
|
||||
|
@ -3,8 +3,8 @@ import { Editor } from 'slate';
|
||||
export enum BlockType {
|
||||
PageBlock = 'page',
|
||||
HeadingBlock = 'heading',
|
||||
ListBlock = 'list',
|
||||
TextBlock = 'text',
|
||||
TodoListBlock = 'todo_list',
|
||||
CodeBlock = 'code',
|
||||
EmbedBlock = 'embed',
|
||||
QuoteBlock = 'quote',
|
||||
@ -18,6 +18,10 @@ export interface HeadingBlockData extends TextBlockData {
|
||||
level: number;
|
||||
}
|
||||
|
||||
export interface TodoListBlockData extends TextBlockData {
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
export interface TextBlockData {
|
||||
delta: TextDelta[];
|
||||
}
|
||||
@ -28,6 +32,8 @@ export type BlockData<Type> = Type extends BlockType.HeadingBlock
|
||||
? HeadingBlockData
|
||||
: Type extends BlockType.PageBlock
|
||||
? PageBlockData
|
||||
: Type extends BlockType.TodoListBlock
|
||||
? TodoListBlockData
|
||||
: TextBlockData;
|
||||
|
||||
export interface NestedBlock<Type = any> {
|
||||
|
@ -1,42 +1,31 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { Editor } from 'slate';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { DocumentState } from '$app/interfaces/document';
|
||||
import { getHeadingDataFromEditor, newHeadingBlock } from '$app/utils/document/blocks/heading';
|
||||
import { setCursorBeforeThunk } from '$app_reducers/document/async-actions/cursor';
|
||||
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, getState } = thunkAPI;
|
||||
const state = (getState() as { document: DocumentState }).document;
|
||||
|
||||
const node = state.nodes[id];
|
||||
if (!node.parent) return;
|
||||
|
||||
const parent = state.nodes[node.parent];
|
||||
const children = state.children[node.children].map((id) => state.nodes[id]);
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const { dispatch } = thunkAPI;
|
||||
|
||||
const data = getHeadingDataFromEditor(editor);
|
||||
if (!data) return;
|
||||
const headingBlock = newHeadingBlock(parent.id, data);
|
||||
const insertHeadingAction = controller.getInsertAction(headingBlock, node.id);
|
||||
|
||||
const moveChildrenActions = controller.getMoveChildrenAction(children, parent.id, headingBlock.id);
|
||||
|
||||
const deleteAction = controller.getDeleteAction(node);
|
||||
|
||||
// submit actions
|
||||
await controller.applyActions([insertHeadingAction, ...moveChildrenActions, deleteAction]);
|
||||
// set cursor
|
||||
await dispatch(setCursorBeforeThunk({ id: headingBlock.id }));
|
||||
await dispatch(
|
||||
turnToBlockThunk({
|
||||
id,
|
||||
controller,
|
||||
type: BlockType.HeadingBlock,
|
||||
data,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -84,7 +84,7 @@ export const backspaceNodeThunk = createAsyncThunk(
|
||||
const index = children.indexOf(id);
|
||||
const prevNodeId = children[index - 1];
|
||||
const nextNodeId = children[index + 1];
|
||||
// transform to text block
|
||||
// turn to text block
|
||||
if (node.type !== BlockType.TextBlock) {
|
||||
await dispatch(turnToTextBlockThunk({ id, controller }));
|
||||
return;
|
||||
|
@ -1,39 +1,32 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { DocumentState } from '$app/interfaces/document';
|
||||
import { newTextBlock } from '$app/utils/document/blocks/text';
|
||||
import { setCursorBeforeThunk } from '$app_reducers/document/async-actions/cursor';
|
||||
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];
|
||||
if (!node.parent) return;
|
||||
|
||||
const parent = state.nodes[node.parent];
|
||||
const children = state.children[node.children].map((id) => state.nodes[id]);
|
||||
|
||||
/**
|
||||
* transform to text block
|
||||
* 1. insert text block after current block
|
||||
* 2. move children to text block
|
||||
* 3. delete current block
|
||||
*/
|
||||
|
||||
const textBlock = newTextBlock(parent.id, {
|
||||
const data = {
|
||||
delta: node.data.delta,
|
||||
});
|
||||
const insertTextAction = controller.getInsertAction(textBlock, node.id);
|
||||
const moveChildrenActions = controller.getMoveChildrenAction(children, textBlock.id, '');
|
||||
const deleteAction = controller.getDeleteAction(node);
|
||||
};
|
||||
|
||||
// submit actions
|
||||
await controller.applyActions([insertTextAction, ...moveChildrenActions, deleteAction]);
|
||||
// set cursor
|
||||
await dispatch(setCursorBeforeThunk({ id: textBlock.id }));
|
||||
await dispatch(
|
||||
turnToBlockThunk({
|
||||
id,
|
||||
controller,
|
||||
type: BlockType.TextBlock,
|
||||
data,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -3,7 +3,8 @@ import { DocumentController } from '$app/stores/effects/document/document_contro
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { documentActions } from '$app_reducers/document/slice';
|
||||
import { setCursorBeforeThunk } from '../../cursor';
|
||||
import { newTextBlock } from '$app/utils/document/blocks/text';
|
||||
import { getDefaultBlockData, newBlock } from '$app/utils/document/blocks/common';
|
||||
import { splitableBlockTypes } from '$app/constants/document/config';
|
||||
|
||||
export const splitNodeThunk = createAsyncThunk(
|
||||
'document/splitNode',
|
||||
@ -20,7 +21,10 @@ export const splitNodeThunk = createAsyncThunk(
|
||||
const prevId = children.length > 0 ? null : node.id;
|
||||
const parent = children.length > 0 ? node : state.nodes[node.parent];
|
||||
|
||||
const newNode = newTextBlock(parent.id, {
|
||||
const newNodeType = splitableBlockTypes.includes(node.type) ? node.type : BlockType.TextBlock;
|
||||
const defaultData = getDefaultBlockData(newNodeType);
|
||||
const newNode = newBlock<any>(newNodeType, parent.id, {
|
||||
...defaultData,
|
||||
delta: insert,
|
||||
});
|
||||
const retainNode = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TextDelta, NestedBlock, DocumentState } from '$app/interfaces/document';
|
||||
import { TextDelta, NestedBlock, DocumentState, BlockData } 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';
|
||||
@ -35,3 +35,29 @@ const debounceApplyUpdate = debounce((controller: DocumentController, updateNode
|
||||
}),
|
||||
]);
|
||||
}, 200);
|
||||
|
||||
export const updateNodeDataThunk = createAsyncThunk<
|
||||
void,
|
||||
{
|
||||
id: string;
|
||||
data: Partial<BlockData<any>>;
|
||||
controller: DocumentController;
|
||||
}
|
||||
>('document/updateNodeDataExceptDelta', async (payload, thunkAPI) => {
|
||||
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];
|
||||
await controller.applyActions([
|
||||
controller.getUpdateAction({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
...data,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
@ -0,0 +1,31 @@
|
||||
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,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
@ -0,0 +1,46 @@
|
||||
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 { newBlock } from '$app/utils/document/blocks/common';
|
||||
|
||||
export const turnToBlockThunk = createAsyncThunk(
|
||||
'document/turnToBlock',
|
||||
async (payload: { id: string; controller: DocumentController; type: BlockType; data: BlockData<any> }, thunkAPI) => {
|
||||
const { id, controller, type, data } = payload;
|
||||
const { dispatch, getState } = thunkAPI;
|
||||
const state = (getState() as { document: DocumentState }).document;
|
||||
|
||||
const node = state.nodes[id];
|
||||
if (!node.parent) return;
|
||||
|
||||
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);
|
||||
|
||||
// if new block is not allowed to have children, move children to parent
|
||||
const newParent = allowedChildrenBlockTypes.includes(block.type) ? 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);
|
||||
|
||||
// delete current block
|
||||
const deleteAction = controller.getDeleteAction(node);
|
||||
|
||||
// submit actions
|
||||
await controller.applyActions([insertHeadingAction, ...moveChildrenActions, deleteAction]);
|
||||
// set cursor in new block
|
||||
await dispatch(setCursorBeforeThunk({ id: block.id }));
|
||||
}
|
||||
);
|
@ -110,3 +110,17 @@ export function newBlock<Type>(type: BlockType, parentId: string, data: BlockDat
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultBlockData(type: BlockType) {
|
||||
switch (type) {
|
||||
case BlockType.TodoListBlock:
|
||||
return {
|
||||
checked: false,
|
||||
delta: [],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
delta: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ export function newHeadingBlock(parentId: string, data: HeadingBlockData): Neste
|
||||
return newBlock<BlockType.HeadingBlock>(BlockType.HeadingBlock, parentId, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* get heading data from editor, only support markdown
|
||||
* @param editor
|
||||
*/
|
||||
export function getHeadingDataFromEditor(editor: Editor): HeadingBlockData | undefined {
|
||||
const selection = editor.selection;
|
||||
if (!selection) return;
|
||||
|
@ -0,0 +1,21 @@
|
||||
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,
|
||||
};
|
||||
}
|
@ -3,16 +3,28 @@ 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;
|
||||
}
|
||||
|
||||
function getMarkdownFlag(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
||||
const isSpaceKey = event.key === keyBoardEventKeyMap.Space;
|
||||
const selection = editor.selection;
|
||||
|
||||
if (!isSpaceKey || !selection) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const beforeSpaceContent = Editor.string(editor, getBeforeRangeAt(editor, selection));
|
||||
|
||||
const isHeadingMarkdown = /^(#{1,3})$/.test(beforeSpaceContent.trim());
|
||||
|
||||
return isHeadingMarkdown;
|
||||
return Editor.string(editor, getBeforeRangeAt(editor, selection)).trim();
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { DocumentData } from '../interfaces/document';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
@ -14,9 +14,9 @@ export const useDocument = () => {
|
||||
const [controller, setController] = useState<DocumentController | null>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onDocumentChange = (props: { isRemote: boolean; data: BlockEventPayloadPB }) => {
|
||||
const onDocumentChange = useCallback((props: { isRemote: boolean; data: BlockEventPayloadPB }) => {
|
||||
dispatch(documentActions.onDataChange(props));
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let documentController: DocumentController | null = null;
|
||||
@ -34,14 +34,17 @@ export const useDocument = () => {
|
||||
Log.error(e);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
void (async () => {
|
||||
if (documentController) {
|
||||
await documentController.dispose();
|
||||
}
|
||||
Log.debug('close document', params.id);
|
||||
})();
|
||||
|
||||
const closeDocument = () => {
|
||||
if (documentController) {
|
||||
void documentController.dispose();
|
||||
}
|
||||
Log.debug('close document', params.id);
|
||||
};
|
||||
// dispose controller before unload
|
||||
window.addEventListener('beforeunload', closeDocument);
|
||||
return closeDocument;
|
||||
}, [params.id]);
|
||||
|
||||
return { documentId, documentData, controller };
|
||||
};
|
||||
|
@ -38,6 +38,7 @@ module.exports = {
|
||||
4: '#BDBDBD',
|
||||
5: '#E0E0E0',
|
||||
6: '#F2F2F2',
|
||||
7: '#FFFFFF',
|
||||
},
|
||||
surface: {
|
||||
1: '#F7F8FC',
|
||||
|
@ -57,6 +57,7 @@ impl DocumentManager {
|
||||
document
|
||||
.lock()
|
||||
.open(move |events, is_remote| {
|
||||
tracing::debug!("data_change: {:?}, from remote: {}", &events, is_remote);
|
||||
send_notification(&clone_doc_id, DocumentNotification::DidReceiveUpdate)
|
||||
.payload::<DocEventPB>((events, is_remote).into())
|
||||
.send();
|
||||
|
Loading…
Reference in New Issue
Block a user