Feat/number list block (#2453)

* feat: support bulleted list block

* feat: support number list block
This commit is contained in:
Kilu.He 2023-05-04 17:20:23 +08:00 committed by GitHub
parent ad99998d33
commit e2ced6524f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 152 additions and 99 deletions

View File

@ -0,0 +1,23 @@
import React from 'react';
import { BlockType, NestedBlock } from '$app/interfaces/document';
import { Circle } from '@mui/icons-material';
import TextBlock from '$app/components/document/TextBlock';
import NodeChildren from '$app/components/document/Node/NodeChildren';
function BulletedListBlock({ node, childIds }: { node: NestedBlock<BlockType.BulletedListBlock>; childIds?: string[] }) {
return (
<>
<div className={'flex'}>
<div className={`relative flex h-[calc(1.5em_+_2px)] min-w-[24px] select-none items-center`}>
<Circle sx={{ width: 8, height: 8 }} />
</div>
<div className={'flex-1'}>
<TextBlock node={node} />
</div>
</div>
<NodeChildren className='pl-[1.5em]' childIds={childIds} />
</>
);
}
export default BulletedListBlock;

View File

@ -1,7 +1,7 @@
import NodeComponent from '$app/components/document/Node';
import React from 'react';
import NodeComponent from '../Node';
export default function ColumnBlock({ id, index, width }: { id: string; index: number; width: string }) {
export function ColumnBlock({ id, index, width }: { id: string; index: number; width: string }) {
const renderResizer = () => {
return (
<div className={`relative w-[46px] flex-shrink-0 flex-grow-0 transition-opacity`} style={{ opacity: 0 }}></div>

View File

@ -1,7 +1,6 @@
import React, { useMemo } from 'react';
import ColumnBlock from '../ColumnBlock';
import { Node } from '$app/interfaces/document';
import { ColumnBlock } from './Column';
export default function ColumnListBlock({
node,

View File

@ -1,30 +0,0 @@
import { Circle } from '@mui/icons-material';
import NodeComponent from '../Node';
import { Node } from '$app/interfaces/document';
export default function BulletedListBlock({
title,
node,
childIds,
}: {
title: JSX.Element;
node: Node;
childIds?: string[];
}) {
return (
<div className='bulleted-list-block relative'>
<div className='relative flex'>
<div className={`relative flex h-[calc(1.5em_+_3px_+_3px)] min-w-[24px] select-none items-center`}>
<Circle sx={{ width: 8, height: 8 }} />
</div>
{title}
</div>
<div className='pl-[24px]'>
{childIds?.map((item) => (
<NodeComponent key={item} id={item} />
))}
</div>
</div>
);
}

View File

@ -1,30 +0,0 @@
import NodeComponent from '../Node';
import { Node } from '$app/interfaces/document';
export default function NumberedListBlock({
title,
node,
childIds,
}: {
title: JSX.Element;
node: Node;
childIds?: string[];
}) {
const index = 1;
return (
<div className='numbered-list-block'>
<div className='relative flex'>
<div
className={`relative flex h-[calc(1.5em_+_3px_+_3px)] min-w-[24px] max-w-[24px] select-none items-center`}
>{`${index} .`}</div>
{title}
</div>
<div className='pl-[24px]'>
{childIds?.map((item) => (
<NodeComponent key={item} id={item} />
))}
</div>
</div>
);
}

View File

@ -1,27 +0,0 @@
import React, { useMemo } from 'react';
import TextBlock from '../TextBlock';
import NumberedListBlock from './NumberedListBlock';
import BulletedListBlock from './BulletedListBlock';
import ColumnListBlock from './ColumnListBlock';
import { Node, TextDelta } from '@/appflowy_app/interfaces/document';
export default function ListBlock({ node }: { node: Node }) {
const title = useMemo(() => {
// if (node.data.style?.type === 'column') return <></>;
return <div className='flex-1'>{/*<TextBlock delta={delta} node={node} childIds={[]} />*/}</div>;
}, [node]);
// if (node.data.type === 'numbered') {
// return <NumberedListBlock title={title} node={node} />;
// }
//
// if (node.data.type === 'bulleted') {
// return <BulletedListBlock title={title} node={node} />;
// }
//
// if (node.data.type === 'column') {
// return <ColumnListBlock node={node} />;
// }
return null;
}

View File

@ -8,6 +8,9 @@ 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';
import BulletedListBlock from '$app/components/document/BulletedListBlock';
import NumberedListBlock from '$app/components/document/NumberedListBlock';
import { Alert } from '@mui/material';
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
const { node, childIds, isSelected, ref } = useNode(id);
@ -26,8 +29,18 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
case BlockType.QuoteBlock: {
return <QuoteBlock node={node} childIds={childIds} />;
}
case BlockType.BulletedListBlock: {
return <BulletedListBlock node={node} childIds={childIds} />;
}
case BlockType.NumberedListBlock: {
return <NumberedListBlock node={node} childIds={childIds} />;
}
default:
return null;
return (
<Alert severity='info' className='mb-2'>
<p>The current version does not support this Block.</p>
</Alert>
);
}
}, [node, childIds]);

View File

@ -0,0 +1,26 @@
import { useAppSelector } from '$app/stores/store';
import { BlockType, NestedBlock } from '$app/interfaces/document';
export function useNumberedListBlock(node: NestedBlock<BlockType.NumberedListBlock>) {
// Find the last index of the previous blocks
const prevNumberedIndex = useAppSelector((state) => {
const nodes = state['document'].nodes;
const children = state['document'].children;
// The parent must be existed
const parent = nodes[node.parent!];
const siblings = children[parent.children];
const index = siblings.indexOf(node.id);
if (index === 0) return 0;
const prevNodeIds = siblings.slice(0, index);
// The index is distance from last block to the last non-numbered-list block
const lastIndex = prevNodeIds.reverse().findIndex((id) => {
return nodes[id].type !== BlockType.NumberedListBlock;
});
if (lastIndex === -1) return prevNodeIds.length;
return lastIndex;
});
return {
index: prevNumberedIndex + 1,
};
}

View File

@ -0,0 +1,27 @@
import React from 'react';
import { BlockType, NestedBlock } from '$app/interfaces/document';
import TextBlock from '$app/components/document/TextBlock';
import NodeChildren from '$app/components/document/Node/NodeChildren';
import { useNumberedListBlock } from '$app/components/document/NumberedListBlock/NumberedListBlock.hooks';
function NumberedListBlock({ node, childIds }: { node: NestedBlock<BlockType.NumberedListBlock>; childIds?: string[] }) {
const { index } = useNumberedListBlock(node);
return (
<>
<div className={'flex'}>
<div
className={`relative flex h-[calc(1.5em_+_4px)] min-w-[24px] select-none items-center whitespace-nowrap text-center`}
>
{index}.
</div>
<div className={'flex-1'}>
<TextBlock node={node} />
</div>
</div>
<NodeChildren className='pl-[1.5em]' childIds={childIds} />
</>
);
}
export default NumberedListBlock;

View File

@ -7,12 +7,20 @@ import { turnToBlockThunk } from '$app_reducers/document/async-actions';
import { blockConfig } from '$app/constants/document/config';
import { Editor } from 'slate';
import { getBeforeRangeAt } from '$app/utils/document/slate/text';
import { getHeadingDataFromEditor, getQuoteDataFromEditor, getTodoListDataFromEditor } from '$app/utils/document/blocks';
import {
getHeadingDataFromEditor,
getQuoteDataFromEditor,
getTodoListDataFromEditor,
getBulletedDataFromEditor,
getNumberedListDataFromEditor,
} from '$app/utils/document/blocks';
const blockDataFactoryMap: Record<string, (editor: Editor) => BlockData<any> | undefined> = {
[BlockType.HeadingBlock]: getHeadingDataFromEditor,
[BlockType.TodoListBlock]: getTodoListDataFromEditor,
[BlockType.QuoteBlock]: getQuoteDataFromEditor,
[BlockType.BulletedListBlock]: getBulletedDataFromEditor,
[BlockType.NumberedListBlock]: getNumberedListDataFromEditor
};
export function useTurnIntoBlock(id: string) {

View File

@ -53,8 +53,9 @@ export const blockConfig: Record<
splitType: BlockType.NumberedListBlock,
/**
* 1. or 2. or 3.
* a. or b. or c.
*/
markdownRegexps: [/^(\s*\d+\.)$/],
markdownRegexps: [/^(\s*[\d|a-zA-Z]+\.)$/],
},
[BlockType.QuoteBlock]: {
canAddChild: true,

View File

@ -25,6 +25,14 @@ export interface TodoListBlockData extends TextBlockData {
checked: boolean;
}
export interface BulletListBlockData extends TextBlockData {
format: 'default' | 'circle' | 'square' | 'disc';
}
export interface NumberedListBlockData extends TextBlockData {
format: 'default' | 'numbers' | 'letters' | 'roman_numerals';
}
export interface QuoteBlockData extends TextBlockData {
size: 'default' | 'large';
}
@ -43,6 +51,10 @@ export type BlockData<Type> = Type extends BlockType.HeadingBlock
? TodoListBlockData
: Type extends BlockType.QuoteBlock
? QuoteBlockData
: Type extends BlockType.BulletedListBlock
? BulletListBlockData
: Type extends BlockType.NumberedListBlock
? NumberedListBlockData
: TextBlockData;
export interface NestedBlock<Type = any> {

View File

@ -1,7 +1,12 @@
import { Editor } from 'slate';
import { HeadingBlockData, TodoListBlockData } from '$app/interfaces/document';
import { getAfterRangeAt, getBeforeRangeAt } from '$app/utils/document/slate/text';
import { getDeltaAfterSelection, getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
import {
BulletListBlockData,
HeadingBlockData,
NumberedListBlockData,
TodoListBlockData,
} from '$app/interfaces/document';
import { getBeforeRangeAt } from '$app/utils/document/slate/text';
import { getDeltaAfterSelection } from '$app/utils/document/blocks/common';
/**
* get heading data from editor, only support markdown
@ -43,10 +48,36 @@ export function getTodoListDataFromEditor(editor: Editor): TodoListBlockData | u
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);
const delta = getDeltaAfterSelection(editor);
if (!delta) return;
return {
delta,
checked: !!checked,
};
}
/**
* get bulleted_list data from editor, only support markdown
* @param editor
*/
export function getBulletedDataFromEditor(editor: Editor): BulletListBlockData | undefined {
const delta = getDeltaAfterSelection(editor);
if (!delta) return;
return {
delta,
format: 'default',
};
}
/**
* get numbered_list data from editor, only support markdown
* @param editor
*/
export function getNumberedListDataFromEditor(editor: Editor): NumberedListBlockData | undefined {
const delta = getDeltaAfterSelection(editor);
if (!delta) return;
return {
delta,
format: 'default',
};
}