Support quote block (#2415)

* feat: support quote block

* fix: database ts error
This commit is contained in:
Kilu.He
2023-05-03 14:54:07 +08:00
committed by GitHub
parent 7db36e3f1e
commit 76b94e363e
15 changed files with 139 additions and 23 deletions

View File

@ -70,7 +70,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
title: field.name, title: field.name,
fieldType: field.field_type, fieldType: field.field_type,
fieldOptions: { fieldOptions: {
NumberFormatPB: typeOption.format, numberFormat: typeOption.format,
}, },
}; };
} }
@ -82,8 +82,8 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
title: field.name, title: field.name,
fieldType: field.field_type, fieldType: field.field_type,
fieldOptions: { fieldOptions: {
DateFormatPB: typeOption.date_format, dateFormat: typeOption.date_format,
TimeFormatPB: typeOption.time_format, timeFormat: typeOption.time_format,
includeTime: typeOption.include_time, includeTime: typeOption.include_time,
}, },
}; };

View File

@ -8,7 +8,7 @@ export default function DocumentTitle({ id }: { id: string }) {
if (!node) return null; if (!node) return null;
return ( return (
<NodeContext.Provider value={node}> <NodeContext.Provider value={node}>
<div data-block-id={node.id} className='doc-title relative mb-2 px-2 pt-[50px] text-4xl font-bold'> <div data-block-id={node.id} className='doc-title relative mb-2 px-1 pt-[50px] text-4xl font-bold'>
<TextBlock placeholder='Untitled' childIds={[]} node={node} /> <TextBlock placeholder='Untitled' childIds={[]} node={node} />
</div> </div>
</NodeContext.Provider> </NodeContext.Provider>

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import NodeComponent from '$app/components/document/Node/index'; import NodeComponent from '$app/components/document/Node/index';
function NodeChildren({ childIds }: { childIds?: string[] }) { function NodeChildren({ childIds, ...props }: { childIds?: string[] } & React.HTMLAttributes<HTMLDivElement>) {
return childIds && childIds.length > 0 ? ( return childIds && childIds.length > 0 ? (
<div className='pl-[1.5em]'> <div {...props}>
{childIds.map((item) => ( {childIds.map((item) => (
<NodeComponent key={item} id={item} /> <NodeComponent key={item} id={item} />
))} ))}

View File

@ -7,6 +7,7 @@ import { NodeContext } from '../_shared/SubscribeNode.hooks';
import { BlockType } from '$app/interfaces/document'; import { BlockType } from '$app/interfaces/document';
import HeadingBlock from '$app/components/document/HeadingBlock'; import HeadingBlock from '$app/components/document/HeadingBlock';
import TodoListBlock from '$app/components/document/TodoListBlock'; import TodoListBlock from '$app/components/document/TodoListBlock';
import QuoteBlock from '$app/components/document/QuoteBlock';
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) { function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
const { node, childIds, isSelected, ref } = useNode(id); const { node, childIds, isSelected, ref } = useNode(id);
@ -22,6 +23,9 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
case BlockType.TodoListBlock: { case BlockType.TodoListBlock: {
return <TodoListBlock node={node} childIds={childIds} />; return <TodoListBlock node={node} childIds={childIds} />;
} }
case BlockType.QuoteBlock: {
return <QuoteBlock node={node} childIds={childIds} />;
}
default: default:
return null; return null;
} }
@ -31,7 +35,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
return ( return (
<NodeContext.Provider value={node}> <NodeContext.Provider value={node}>
<div {...props} ref={ref} data-block-id={node.id} className={`relative px-2 ${props.className}`}> <div {...props} ref={ref} data-block-id={node.id} className={`relative px-1 ${props.className}`}>
{renderBlock()} {renderBlock()}
<div className='block-overlay' /> <div className='block-overlay' />
{isSelected ? ( {isSelected ? (

View File

@ -0,0 +1,20 @@
import { BlockType, NestedBlock } from '$app/interfaces/document';
import TextBlock from '$app/components/document/TextBlock';
import NodeChildren from '$app/components/document/Node/NodeChildren';
export default function QuoteBlock({
node,
childIds,
}: {
node: NestedBlock<BlockType.QuoteBlock>;
childIds?: string[];
}) {
return (
<div className={'py-[2px]'}>
<div className={'border-l-4 border-solid border-main-accent px-3 '}>
<TextBlock node={node} />
<NodeChildren childIds={childIds} />
</div>
</div>
);
}

View File

@ -33,7 +33,7 @@ function TextBlock({
/> />
</Slate> </Slate>
</div> </div>
<NodeChildren childIds={childIds} /> <NodeChildren className='pl-[1.5em]' childIds={childIds} />
</> </>
); );
} }

View File

@ -1,14 +1,19 @@
import { useCallback, useContext, useMemo } from 'react'; import { useCallback, useContext, useMemo } from 'react';
import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document'; import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document';
import { keyBoardEventKeyMap } from '$app/constants/document/text_block'; import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
import { canHandleToHeadingBlock, canHandleToCheckboxBlock } from '$app/utils/document/slate/markdown'; import {
canHandleToHeadingBlock,
canHandleToCheckboxBlock,
canHandleToQuoteBlock,
} from '$app/utils/document/slate/markdown';
import { useAppDispatch } from '$app/stores/store'; import { useAppDispatch } from '$app/stores/store';
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller'; import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
import { turnToHeadingBlockThunk } from '$app_reducers/document/async-actions/blocks/heading'; import { turnToHeadingBlockThunk } from '$app_reducers/document/async-actions/blocks/heading';
import { turnToTodoListBlockThunk } from '$app_reducers/document/async-actions/blocks/todo_list'; 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) { export function useMarkDown(id: string) {
const { toHeadingBlockAction, toCheckboxBlockAction } = useActions(id); const { toHeadingBlockAction, toCheckboxBlockAction, toQuoteBlockAction } = useActions(id);
const toHeadingBlockEvent = useMemo(() => { const toHeadingBlockEvent = useMemo(() => {
return { return {
triggerEventKey: keyBoardEventKeyMap.Space, triggerEventKey: keyBoardEventKeyMap.Space,
@ -25,9 +30,17 @@ export function useMarkDown(id: string) {
}; };
}, [toCheckboxBlockAction]); }, [toCheckboxBlockAction]);
const toQuoteBlockEvent = useMemo(() => {
return {
triggerEventKey: keyBoardEventKeyMap.Space,
canHandle: canHandleToQuoteBlock,
handler: toQuoteBlockAction,
};
}, [toQuoteBlockAction]);
const markdownEvents = useMemo( const markdownEvents = useMemo(
() => [toHeadingBlockEvent, toCheckboxBlockEvent], () => [toHeadingBlockEvent, toCheckboxBlockEvent, toQuoteBlockEvent],
[toHeadingBlockEvent, toCheckboxBlockEvent] [toHeadingBlockEvent, toCheckboxBlockEvent, toQuoteBlockEvent]
); );
return { return {
@ -56,8 +69,18 @@ function useActions(id: string) {
[controller, dispatch, id] [controller, dispatch, id]
); );
const toQuoteBlockAction = useCallback(
(...args: TextBlockKeyEventHandlerParams) => {
if (!controller) return;
const [_event, editor] = args;
dispatch(turnToQuoteBlockThunk({ id, controller, editor }));
},
[controller, dispatch, id]
);
return { return {
toHeadingBlockAction, toHeadingBlockAction,
toCheckboxBlockAction, toCheckboxBlockAction,
toQuoteBlockAction,
}; };
} }

View File

@ -36,7 +36,7 @@ export default function TodoListBlock({
<TextBlock node={node} /> <TextBlock node={node} />
</div> </div>
</div> </div>
<NodeChildren childIds={childIds} /> <NodeChildren className='pl-[1.5em]' childIds={childIds} />
</> </>
); );
} }

View File

@ -3,7 +3,13 @@ import { BlockType } from '$app/interfaces/document';
/** /**
* Block types that are allowed to have children * Block types that are allowed to have children
*/ */
export const allowedChildrenBlockTypes = [BlockType.TextBlock, BlockType.PageBlock, BlockType.TodoListBlock]; export const allowedChildrenBlockTypes = [
BlockType.TextBlock,
BlockType.PageBlock,
BlockType.TodoListBlock,
BlockType.QuoteBlock,
BlockType.CalloutBlock,
];
/** /**
* Block types that split node can extend to the next line * Block types that split node can extend to the next line

View File

@ -8,6 +8,7 @@ export enum BlockType {
CodeBlock = 'code', CodeBlock = 'code',
EmbedBlock = 'embed', EmbedBlock = 'embed',
QuoteBlock = 'quote', QuoteBlock = 'quote',
CalloutBlock = 'callout',
DividerBlock = 'divider', DividerBlock = 'divider',
MediaBlock = 'media', MediaBlock = 'media',
TableBlock = 'table', TableBlock = 'table',
@ -22,6 +23,10 @@ export interface TodoListBlockData extends TextBlockData {
checked: boolean; checked: boolean;
} }
export interface QuoteBlockData extends TextBlockData {
size: 'default' | 'large';
}
export interface TextBlockData { export interface TextBlockData {
delta: TextDelta[]; delta: TextDelta[];
} }
@ -34,6 +39,8 @@ export type BlockData<Type> = Type extends BlockType.HeadingBlock
? PageBlockData ? PageBlockData
: Type extends BlockType.TodoListBlock : Type extends BlockType.TodoListBlock
? TodoListBlockData ? TodoListBlockData
: Type extends BlockType.QuoteBlock
? QuoteBlockData
: TextBlockData; : TextBlockData;
export interface NestedBlock<Type = any> { export interface NestedBlock<Type = any> {

View File

@ -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 { 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,
})
);
}
);

View File

@ -1,8 +1,9 @@
import { BlockData, BlockType, DocumentState, NestedBlock, TextDelta } from '$app/interfaces/document'; import { BlockData, BlockType, DocumentState, NestedBlock, TextDelta } from '$app/interfaces/document';
import { Descendant, Element, Text } from 'slate'; import { Descendant, Editor, Element, Text } from 'slate';
import { BlockPB } from '@/services/backend'; import { BlockPB } from '@/services/backend';
import { Log } from '$app/utils/log'; import { Log } from '$app/utils/log';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { getAfterRangeAt } from '$app/utils/document/slate/text';
export function deltaToSlateValue(delta: TextDelta[]) { export function deltaToSlateValue(delta: TextDelta[]) {
const slateNode = { const slateNode = {
@ -21,6 +22,14 @@ export function deltaToSlateValue(delta: TextDelta[]) {
return slateNodes; return slateNodes;
} }
export function getDeltaAfterSelection(editor: Editor): TextDelta[] | undefined {
const selection = editor.selection;
if (!selection) return;
const slateNodes = Editor.fragment(editor, getAfterRangeAt(editor, selection));
const delta = getDeltaFromSlateNodes(slateNodes);
return delta;
}
export function getDeltaFromSlateNodes(slateNodes: Descendant[]) { export function getDeltaFromSlateNodes(slateNodes: Descendant[]) {
const element = slateNodes[0] as Element; const element = slateNodes[0] as Element;
const children = element.children as Text[]; const children = element.children as Text[];

View File

@ -1,11 +1,7 @@
import { Editor } from 'slate'; import { Editor } from 'slate';
import { getAfterRangeAt, getBeforeRangeAt } from '$app/utils/document/slate/text'; import { getAfterRangeAt, getBeforeRangeAt } from '$app/utils/document/slate/text';
import { BlockType, HeadingBlockData, NestedBlock } from '$app/interfaces/document'; import { HeadingBlockData } from '$app/interfaces/document';
import { getDeltaFromSlateNodes, newBlock } from '$app/utils/document/blocks/common'; import { getDeltaAfterSelection, getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
export function newHeadingBlock(parentId: string, data: HeadingBlockData): NestedBlock {
return newBlock<BlockType.HeadingBlock>(BlockType.HeadingBlock, parentId, data);
}
/** /**
* get heading data from editor, only support markdown * get heading data from editor, only support markdown
@ -17,8 +13,8 @@ export function getHeadingDataFromEditor(editor: Editor): HeadingBlockData | und
const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection)); const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection));
const level = hashTags.match(/#/g)?.length; const level = hashTags.match(/#/g)?.length;
if (!level) return; if (!level) return;
const slateNodes = Editor.fragment(editor, getAfterRangeAt(editor, selection)); const delta = getDeltaAfterSelection(editor);
const delta = getDeltaFromSlateNodes(slateNodes); if (!delta) return;
return { return {
level, level,
delta, delta,

View File

@ -0,0 +1,11 @@
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',
};
}

View File

@ -18,6 +18,15 @@ export function canHandleToCheckboxBlock(event: React.KeyboardEvent<HTMLDivEleme
return isCheckboxMarkdown; 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) { function getMarkdownFlag(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
const isSpaceKey = event.key === keyBoardEventKeyMap.Space; const isSpaceKey = event.key === keyBoardEventKeyMap.Space;
const selection = editor.selection; const selection = editor.selection;