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:
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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} />
|
||||||
))}
|
))}
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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>
|
</Slate>
|
||||||
</div>
|
</div>
|
||||||
<NodeChildren childIds={childIds} />
|
<NodeChildren className='pl-[1.5em]' childIds={childIds} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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> {
|
||||||
|
@ -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 { 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[];
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
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;
|
||||||
|
Reference in New Issue
Block a user