diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BulletedListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BulletedListBlock/index.tsx index 880fb29337..7747039265 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BulletedListBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BulletedListBlock/index.tsx @@ -8,7 +8,7 @@ function BulletedListBlock({ node, childIds }: { node: NestedBlock
-
+
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx index d6fde477f1..59152f5570 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx @@ -5,12 +5,14 @@ import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallback import TextBlock from '../TextBlock'; import { NodeContext } from '../_shared/SubscribeNode.hooks'; import { BlockType } from '$app/interfaces/document'; +import { Alert } from '@mui/material'; + 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'; +import ToggleListBlock from '$app/components/document/ToggleListBlock'; function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes) { const { node, childIds, isSelected, ref } = useNode(id); @@ -35,6 +37,9 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes; } + case BlockType.ToggleListBlock: { + return ; + } default: return ( @@ -48,7 +53,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes -
+
{renderBlock()}
{isSelected ? ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/NumberedListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/NumberedListBlock/index.tsx index 28c9028c8f..d3efb37196 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/NumberedListBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/NumberedListBlock/index.tsx @@ -11,7 +11,7 @@ function NumberedListBlock({ node, childIds }: { node: NestedBlock
{index}.
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/events/TurnIntoEvents.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/events/TurnIntoEvents.hooks.ts index 4983a1dc70..d9ffced9df 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/events/TurnIntoEvents.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/events/TurnIntoEvents.hooks.ts @@ -13,6 +13,7 @@ import { getTodoListDataFromEditor, getBulletedDataFromEditor, getNumberedListDataFromEditor, + getToggleListDataFromEditor, } from '$app/utils/document/blocks'; const blockDataFactoryMap: Record BlockData | undefined> = { @@ -20,7 +21,8 @@ const blockDataFactoryMap: Record BlockData | u [BlockType.TodoListBlock]: getTodoListDataFromEditor, [BlockType.QuoteBlock]: getQuoteDataFromEditor, [BlockType.BulletedListBlock]: getBulletedDataFromEditor, - [BlockType.NumberedListBlock]: getNumberedListDataFromEditor + [BlockType.NumberedListBlock]: getNumberedListDataFromEditor, + [BlockType.ToggleListBlock]: getToggleListDataFromEditor, }; export function useTurnIntoBlock(id: string) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx index 9cdec5f24a..01a4326e38 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx @@ -21,7 +21,7 @@ function TextBlock({ return ( <> -
+
-
+
{checked ? : }
) { + const dispatch = useAppDispatch(); + const controller = useContext(DocumentControllerContext); + const toggleCollapsed = useCallback(() => { + if (!controller) return; + void dispatch( + updateNodeDataThunk({ + id, + controller, + data: { + collapsed: !data.collapsed, + }, + }) + ); + }, [controller, dispatch, id, data.collapsed]); + + const handleShortcut = useCallback( + (event: React.KeyboardEvent) => { + // Accepts mod for the classic "cmd on Mac, ctrl on Windows" use case. + if (isHotkey('mod+enter', event)) { + toggleCollapsed(); + } + }, + [toggleCollapsed] + ); + + return { + toggleCollapsed, + handleShortcut, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ToggleListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ToggleListBlock/index.tsx new file mode 100644 index 0000000000..9c3b980bdf --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ToggleListBlock/index.tsx @@ -0,0 +1,41 @@ +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 { useToggleListBlock } from '$app/components/document/ToggleListBlock/ToggleListBlock.hooks'; +import { IconButton } from '@mui/material'; +import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg'; +import Button from '@mui/material/Button'; + +function ToggleListBlock({ node, childIds }: { node: NestedBlock; childIds?: string[] }) { + const { toggleCollapsed, handleShortcut } = useToggleListBlock(node.id, node.data); + const collapsed = node.data.collapsed; + return ( + <> +
+
+ +
+ +
+ +
+
+ {!collapsed && } + + ); +} + +export default ToggleListBlock; diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts index 421abc2af0..1495b8c141 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts @@ -1,5 +1,9 @@ -import { BlockType } from '$app/interfaces/document'; +import { BlockData, BlockType } from '$app/interfaces/document'; +export enum SplitRelationship { + NextSibling, + FirstChild, +} /** * If the block type is not in the config, it will be thrown an error in development env */ @@ -10,23 +14,47 @@ export const blockConfig: Record< * Whether the block can have children */ canAddChild: boolean; - /** - * the type of the block that will be split from the current block - */ - splitType: BlockType; /** * The regexps that will be used to match the markdown flag */ markdownRegexps?: RegExp[]; + + /** + * The default data of the block + */ + defaultData?: BlockData; + + /** + * The props that will be passed to the text split function + */ + splitProps?: { + /** + * The relationship between the next line block and the current block + */ + nextLineRelationShip: SplitRelationship; + /** + * The type of the next line block + */ + nextLineBlockType: BlockType; + }; } > = { [BlockType.TextBlock]: { canAddChild: true, - splitType: BlockType.TextBlock, + defaultData: { + delta: [], + }, + splitProps: { + nextLineRelationShip: SplitRelationship.NextSibling, + nextLineBlockType: BlockType.TextBlock, + }, }, [BlockType.HeadingBlock]: { canAddChild: false, - splitType: BlockType.TextBlock, + splitProps: { + nextLineRelationShip: SplitRelationship.NextSibling, + nextLineBlockType: BlockType.TextBlock, + }, /** * # or ## or ### */ @@ -34,7 +62,14 @@ export const blockConfig: Record< }, [BlockType.TodoListBlock]: { canAddChild: true, - splitType: BlockType.TodoListBlock, + defaultData: { + delta: [], + checked: false, + }, + splitProps: { + nextLineRelationShip: SplitRelationship.NextSibling, + nextLineBlockType: BlockType.TodoListBlock, + }, /** * -[] or -[x] or -[ ] or [] or [x] or [ ] */ @@ -42,7 +77,14 @@ export const blockConfig: Record< }, [BlockType.BulletedListBlock]: { canAddChild: true, - splitType: BlockType.BulletedListBlock, + defaultData: { + delta: [], + format: 'default', + }, + splitProps: { + nextLineRelationShip: SplitRelationship.NextSibling, + nextLineBlockType: BlockType.BulletedListBlock, + }, /** * - or + or * */ @@ -50,7 +92,14 @@ export const blockConfig: Record< }, [BlockType.NumberedListBlock]: { canAddChild: true, - splitType: BlockType.NumberedListBlock, + defaultData: { + delta: [], + format: 'default', + }, + splitProps: { + nextLineRelationShip: SplitRelationship.NextSibling, + nextLineBlockType: BlockType.NumberedListBlock, + }, /** * 1. or 2. or 3. * a. or b. or c. @@ -59,15 +108,36 @@ export const blockConfig: Record< }, [BlockType.QuoteBlock]: { canAddChild: true, - splitType: BlockType.TextBlock, + defaultData: { + delta: [], + size: 'default', + }, + splitProps: { + nextLineRelationShip: SplitRelationship.NextSibling, + nextLineBlockType: BlockType.TextBlock, + }, /** * " or “ or ” */ markdownRegexps: [/^("|“|”)$/], }, + [BlockType.ToggleListBlock]: { + canAddChild: true, + defaultData: { + delta: [], + collapsed: false, + }, + splitProps: { + nextLineRelationShip: SplitRelationship.FirstChild, + nextLineBlockType: BlockType.TextBlock, + }, + /** + * > + */ + markdownRegexps: [/^(>)$/], + }, [BlockType.CodeBlock]: { canAddChild: false, - splitType: BlockType.TextBlock, /** * ``` */ diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts index 42ef40808f..42b0ad6b86 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts @@ -7,6 +7,7 @@ export enum BlockType { TodoListBlock = 'todo_list', BulletedListBlock = 'bulleted_list', NumberedListBlock = 'numbered_list', + ToggleListBlock = 'toggle_list', CodeBlock = 'code', EmbedBlock = 'embed', QuoteBlock = 'quote', @@ -33,6 +34,10 @@ export interface NumberedListBlockData extends TextBlockData { format: 'default' | 'numbers' | 'letters' | 'roman_numerals'; } +export interface ToggleListBlockData extends TextBlockData { + collapsed: boolean; +} + export interface QuoteBlockData extends TextBlockData { size: 'default' | 'large'; } @@ -55,6 +60,8 @@ export type BlockData = Type extends BlockType.HeadingBlock ? BulletListBlockData : Type extends BlockType.NumberedListBlock ? NumberedListBlockData + : Type extends BlockType.ToggleListBlock + ? ToggleListBlockData : TextBlockData; export interface NestedBlock { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/text/split.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/text/split.ts index cab78e9e38..9b8902106a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/text/split.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/text/split.ts @@ -3,8 +3,8 @@ import { DocumentController } from '$app/stores/effects/document/document_contro import { createAsyncThunk } from '@reduxjs/toolkit'; import { documentActions } from '$app_reducers/document/slice'; import { setCursorBeforeThunk } from '../../cursor'; -import { getDefaultBlockData, newBlock } from '$app/utils/document/blocks/common'; -import { blockConfig } from '$app/constants/document/config'; +import { newBlock } from '$app/utils/document/blocks/common'; +import { blockConfig, SplitRelationship } from '$app/constants/document/config'; export const splitNodeThunk = createAsyncThunk( 'document/splitNode', @@ -18,13 +18,28 @@ export const splitNodeThunk = createAsyncThunk( const node = state.nodes[id]; if (!node.parent) return; const children = state.children[node.children]; - const prevId = node.id; const parent = state.nodes[node.parent]; - const config = blockConfig[node.type]; - const newNodeType = config.splitType; - const defaultData = getDefaultBlockData(newNodeType); - const newNode = newBlock(newNodeType, parent.id, { + const config = blockConfig[node.type].splitProps; + // Here we are using the splitProps property of the blockConfig object to determine the type of the new node. + // if the splitProps property is not defined for the block type, we throw an error. + if (!config) { + throw new Error(`Cannot split node of type ${node.type}`); + } + const newNodeType = config.nextLineBlockType; + const relationShip = config.nextLineRelationShip; + const defaultData = blockConfig[newNodeType].defaultData; + // if the defaultData property is not defined for the new block type, we throw an error. + if (!defaultData) { + throw new Error(`Cannot split node of type ${node.type} to ${newNodeType}`); + } + + // if the next line is a sibling, parent is the same as the current node, and prev is the current node. + // otherwise, parent is the current node, and prev is empty. + const newParentId = relationShip === SplitRelationship.NextSibling ? parent.id : node.id; + const newPrevId = relationShip === SplitRelationship.NextSibling ? node.id : ''; + + const newNode = newBlock(newNodeType, newParentId, { ...defaultData, delta: insert, }); @@ -35,14 +50,22 @@ export const splitNodeThunk = createAsyncThunk( delta: retain, }, }; - const insertAction = controller.getInsertAction(newNode, prevId); + const insertAction = controller.getInsertAction(newNode, newPrevId); const updateAction = controller.getUpdateAction(retainNode); - const moveChildrenAction = controller.getMoveChildrenAction( - children.map((id) => state.nodes[id]), - newNode.id, - '' - ); + + // if the next line is a sibling, we need to move the children of the current node to the new node. + // otherwise, we don't need to do anything. + const moveChildrenAction = + relationShip === SplitRelationship.NextSibling + ? controller.getMoveChildrenAction( + children.map((id) => state.nodes[id]), + newNode.id, + '' + ) + : []; + await controller.applyActions([insertAction, ...moveChildrenAction, updateAction]); + // update local node data dispatch(documentActions.updateNodeData({ id: retainNode.id, data: { delta: retain } })); // set cursor diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/common.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/common.ts index d521d4f1d4..97a5e9251f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/common.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/common.ts @@ -119,17 +119,3 @@ export function newBlock(type: BlockType, parentId: string, data: BlockDat data, }; } - -export function getDefaultBlockData(type: BlockType) { - switch (type) { - case BlockType.TodoListBlock: - return { - checked: false, - delta: [], - }; - default: - return { - delta: [], - }; - } -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/index.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/index.ts index 7b37f560c1..e42d280c0d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/index.ts @@ -4,6 +4,7 @@ import { HeadingBlockData, NumberedListBlockData, TodoListBlockData, + ToggleListBlockData, } from '$app/interfaces/document'; import { getBeforeRangeAt } from '$app/utils/document/slate/text'; import { getDeltaAfterSelection } from '$app/utils/document/blocks/common'; @@ -81,3 +82,15 @@ export function getNumberedListDataFromEditor(editor: Editor): NumberedListBlock format: 'default', }; } + +/** + * get toggle_list data from editor, only support markdown + */ +export function getToggleListDataFromEditor(editor: Editor): ToggleListBlockData | undefined { + const delta = getDeltaAfterSelection(editor); + if (!delta) return; + return { + delta, + collapsed: false, + }; +}