mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Feat/number list block (#2453)
* feat: support bulleted list block * feat: support number list block
This commit is contained in:
parent
ad99998d33
commit
e2ced6524f
@ -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;
|
@ -1,7 +1,7 @@
|
|||||||
|
import NodeComponent from '$app/components/document/Node';
|
||||||
import React from 'react';
|
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 = () => {
|
const renderResizer = () => {
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-[46px] flex-shrink-0 flex-grow-0 transition-opacity`} style={{ opacity: 0 }}></div>
|
<div className={`relative w-[46px] flex-shrink-0 flex-grow-0 transition-opacity`} style={{ opacity: 0 }}></div>
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import ColumnBlock from '../ColumnBlock';
|
|
||||||
|
|
||||||
import { Node } from '$app/interfaces/document';
|
import { Node } from '$app/interfaces/document';
|
||||||
|
import { ColumnBlock } from './Column';
|
||||||
|
|
||||||
export default function ColumnListBlock({
|
export default function ColumnListBlock({
|
||||||
node,
|
node,
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -8,6 +8,9 @@ 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';
|
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>) {
|
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const { node, childIds, isSelected, ref } = useNode(id);
|
const { node, childIds, isSelected, ref } = useNode(id);
|
||||||
@ -26,8 +29,18 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
|
|||||||
case BlockType.QuoteBlock: {
|
case BlockType.QuoteBlock: {
|
||||||
return <QuoteBlock node={node} childIds={childIds} />;
|
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:
|
default:
|
||||||
return null;
|
return (
|
||||||
|
<Alert severity='info' className='mb-2'>
|
||||||
|
<p>The current version does not support this Block.</p>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [node, childIds]);
|
}, [node, childIds]);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
@ -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;
|
@ -7,12 +7,20 @@ import { turnToBlockThunk } from '$app_reducers/document/async-actions';
|
|||||||
import { blockConfig } from '$app/constants/document/config';
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
import { Editor } from 'slate';
|
import { Editor } from 'slate';
|
||||||
import { getBeforeRangeAt } from '$app/utils/document/slate/text';
|
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> = {
|
const blockDataFactoryMap: Record<string, (editor: Editor) => BlockData<any> | undefined> = {
|
||||||
[BlockType.HeadingBlock]: getHeadingDataFromEditor,
|
[BlockType.HeadingBlock]: getHeadingDataFromEditor,
|
||||||
[BlockType.TodoListBlock]: getTodoListDataFromEditor,
|
[BlockType.TodoListBlock]: getTodoListDataFromEditor,
|
||||||
[BlockType.QuoteBlock]: getQuoteDataFromEditor,
|
[BlockType.QuoteBlock]: getQuoteDataFromEditor,
|
||||||
|
[BlockType.BulletedListBlock]: getBulletedDataFromEditor,
|
||||||
|
[BlockType.NumberedListBlock]: getNumberedListDataFromEditor
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useTurnIntoBlock(id: string) {
|
export function useTurnIntoBlock(id: string) {
|
||||||
|
@ -53,8 +53,9 @@ export const blockConfig: Record<
|
|||||||
splitType: BlockType.NumberedListBlock,
|
splitType: BlockType.NumberedListBlock,
|
||||||
/**
|
/**
|
||||||
* 1. or 2. or 3.
|
* 1. or 2. or 3.
|
||||||
|
* a. or b. or c.
|
||||||
*/
|
*/
|
||||||
markdownRegexps: [/^(\s*\d+\.)$/],
|
markdownRegexps: [/^(\s*[\d|a-zA-Z]+\.)$/],
|
||||||
},
|
},
|
||||||
[BlockType.QuoteBlock]: {
|
[BlockType.QuoteBlock]: {
|
||||||
canAddChild: true,
|
canAddChild: true,
|
||||||
|
@ -25,6 +25,14 @@ export interface TodoListBlockData extends TextBlockData {
|
|||||||
checked: boolean;
|
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 {
|
export interface QuoteBlockData extends TextBlockData {
|
||||||
size: 'default' | 'large';
|
size: 'default' | 'large';
|
||||||
}
|
}
|
||||||
@ -43,6 +51,10 @@ export type BlockData<Type> = Type extends BlockType.HeadingBlock
|
|||||||
? TodoListBlockData
|
? TodoListBlockData
|
||||||
: Type extends BlockType.QuoteBlock
|
: Type extends BlockType.QuoteBlock
|
||||||
? QuoteBlockData
|
? QuoteBlockData
|
||||||
|
: Type extends BlockType.BulletedListBlock
|
||||||
|
? BulletListBlockData
|
||||||
|
: Type extends BlockType.NumberedListBlock
|
||||||
|
? NumberedListBlockData
|
||||||
: TextBlockData;
|
: TextBlockData;
|
||||||
|
|
||||||
export interface NestedBlock<Type = any> {
|
export interface NestedBlock<Type = any> {
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { Editor } from 'slate';
|
import { Editor } from 'slate';
|
||||||
import { HeadingBlockData, TodoListBlockData } from '$app/interfaces/document';
|
import {
|
||||||
import { getAfterRangeAt, getBeforeRangeAt } from '$app/utils/document/slate/text';
|
BulletListBlockData,
|
||||||
import { getDeltaAfterSelection, getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
|
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
|
* get heading data from editor, only support markdown
|
||||||
@ -43,10 +48,36 @@ export function getTodoListDataFromEditor(editor: Editor): TodoListBlockData | u
|
|||||||
if (!selection) return;
|
if (!selection) return;
|
||||||
const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection));
|
const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection));
|
||||||
const checked = hashTags.match(/x/g)?.length;
|
const checked = hashTags.match(/x/g)?.length;
|
||||||
const slateNodes = Editor.fragment(editor, getAfterRangeAt(editor, selection));
|
const delta = getDeltaAfterSelection(editor);
|
||||||
const delta = getDeltaFromSlateNodes(slateNodes);
|
if (!delta) return;
|
||||||
return {
|
return {
|
||||||
delta,
|
delta,
|
||||||
checked: !!checked,
|
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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user