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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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,
fieldType: field.field_type,
fieldOptions: {
NumberFormatPB: typeOption.format,
numberFormat: typeOption.format,
},
};
}
@ -82,8 +82,8 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
title: field.name,
fieldType: field.field_type,
fieldOptions: {
DateFormatPB: typeOption.date_format,
TimeFormatPB: typeOption.time_format,
dateFormat: typeOption.date_format,
timeFormat: typeOption.time_format,
includeTime: typeOption.include_time,
},
};

View File

@ -8,7 +8,7 @@ export default function DocumentTitle({ id }: { id: string }) {
if (!node) return null;
return (
<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} />
</div>
</NodeContext.Provider>

View File

@ -1,9 +1,9 @@
import React from 'react';
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 ? (
<div className='pl-[1.5em]'>
<div {...props}>
{childIds.map((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 HeadingBlock from '$app/components/document/HeadingBlock';
import TodoListBlock from '$app/components/document/TodoListBlock';
import QuoteBlock from '$app/components/document/QuoteBlock';
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
const { node, childIds, isSelected, ref } = useNode(id);
@ -22,6 +23,9 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
case BlockType.TodoListBlock: {
return <TodoListBlock node={node} childIds={childIds} />;
}
case BlockType.QuoteBlock: {
return <QuoteBlock node={node} childIds={childIds} />;
}
default:
return null;
}
@ -31,7 +35,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
return (
<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()}
<div className='block-overlay' />
{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>
</div>
<NodeChildren childIds={childIds} />
<NodeChildren className='pl-[1.5em]' childIds={childIds} />
</>
);
}

View File

@ -1,14 +1,19 @@
import { useCallback, useContext, useMemo } from 'react';
import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document';
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 { 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 } = useActions(id);
const { toHeadingBlockAction, toCheckboxBlockAction, toQuoteBlockAction } = useActions(id);
const toHeadingBlockEvent = useMemo(() => {
return {
triggerEventKey: keyBoardEventKeyMap.Space,
@ -25,9 +30,17 @@ export function useMarkDown(id: string) {
};
}, [toCheckboxBlockAction]);
const toQuoteBlockEvent = useMemo(() => {
return {
triggerEventKey: keyBoardEventKeyMap.Space,
canHandle: canHandleToQuoteBlock,
handler: toQuoteBlockAction,
};
}, [toQuoteBlockAction]);
const markdownEvents = useMemo(
() => [toHeadingBlockEvent, toCheckboxBlockEvent],
[toHeadingBlockEvent, toCheckboxBlockEvent]
() => [toHeadingBlockEvent, toCheckboxBlockEvent, toQuoteBlockEvent],
[toHeadingBlockEvent, toCheckboxBlockEvent, toQuoteBlockEvent]
);
return {
@ -56,8 +69,18 @@ function useActions(id: string) {
[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,
};
}

View File

@ -36,7 +36,7 @@ export default function TodoListBlock({
<TextBlock node={node} />
</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
*/
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

View File

@ -8,6 +8,7 @@ export enum BlockType {
CodeBlock = 'code',
EmbedBlock = 'embed',
QuoteBlock = 'quote',
CalloutBlock = 'callout',
DividerBlock = 'divider',
MediaBlock = 'media',
TableBlock = 'table',
@ -22,6 +23,10 @@ export interface TodoListBlockData extends TextBlockData {
checked: boolean;
}
export interface QuoteBlockData extends TextBlockData {
size: 'default' | 'large';
}
export interface TextBlockData {
delta: TextDelta[];
}
@ -34,6 +39,8 @@ export type BlockData<Type> = Type extends BlockType.HeadingBlock
? PageBlockData
: Type extends BlockType.TodoListBlock
? TodoListBlockData
: Type extends BlockType.QuoteBlock
? QuoteBlockData
: TextBlockData;
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 { Descendant, Element, Text } from 'slate';
import { Descendant, Editor, Element, Text } from 'slate';
import { BlockPB } from '@/services/backend';
import { Log } from '$app/utils/log';
import { nanoid } from 'nanoid';
import { getAfterRangeAt } from '$app/utils/document/slate/text';
export function deltaToSlateValue(delta: TextDelta[]) {
const slateNode = {
@ -21,6 +22,14 @@ export function deltaToSlateValue(delta: TextDelta[]) {
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[]) {
const element = slateNodes[0] as Element;
const children = element.children as Text[];

View File

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