mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Support quote block (#2415)
* feat: support quote block * fix: database ts error
This commit is contained in:
parent
7db36e3f1e
commit
76b94e363e
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
))}
|
||||
|
@ -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 ? (
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -33,7 +33,7 @@ function TextBlock({
|
||||
/>
|
||||
</Slate>
|
||||
</div>
|
||||
<NodeChildren childIds={childIds} />
|
||||
<NodeChildren className='pl-[1.5em]' childIds={childIds} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export default function TodoListBlock({
|
||||
<TextBlock node={node} />
|
||||
</div>
|
||||
</div>
|
||||
<NodeChildren childIds={childIds} />
|
||||
<NodeChildren className='pl-[1.5em]' childIds={childIds} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
@ -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[];
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
};
|
||||
}
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user