Support divider block and callout block ()

* feat: divider block

* feat: callout block
This commit is contained in:
Kilu.He 2023-05-08 10:31:35 +08:00 committed by GitHub
parent 96c058db9b
commit ba8cbe170c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 328 additions and 78 deletions
frontend/appflowy_tauri
package.jsonpnpm-lock.yaml
src/appflowy_app
components/document
constants/document
interfaces
stores/reducers/document/async-actions
utils/document/blocks

View File

@ -15,6 +15,8 @@
"tauri:dev": "tauri dev"
},
"dependencies": {
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.11.11",
@ -24,6 +26,7 @@
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.2.0",
"dayjs": "^1.11.7",
"emoji-mart": "^5.5.2",
"events": "^3.3.0",
"google-protobuf": "^3.21.2",
"i18next": "^22.4.10",

View File

@ -1,6 +1,8 @@
lockfileVersion: 5.4
specifiers:
'@emoji-mart/data': ^1.1.2
'@emoji-mart/react': ^1.1.1
'@emotion/react': ^11.10.6
'@emotion/styled': ^11.10.6
'@mui/icons-material': ^5.11.11
@ -23,6 +25,7 @@ specifiers:
'@vitejs/plugin-react': ^3.0.0
autoprefixer: ^10.4.13
dayjs: ^1.11.7
emoji-mart: ^5.5.2
eslint: ^8.34.0
eslint-plugin-react: ^7.32.2
eslint-plugin-react-hooks: ^4.6.0
@ -60,6 +63,8 @@ specifiers:
yjs: ^13.5.51
dependencies:
'@emoji-mart/data': 1.1.2
'@emoji-mart/react': 1.1.1_kyrnz3vmphzqyjjk2ivrm6bcsu
'@emotion/react': 11.10.6_pmekkgnqduwlme35zpnqhenc34
'@emotion/styled': 11.10.6_oouaibmszuch5k64ms7uxp2aia
'@mui/icons-material': 5.11.11_ao76n7r2cajsoyr3cbwrn7geoi
@ -69,6 +74,7 @@ dependencies:
'@tanstack/react-virtual': 3.0.0-beta.54_react@18.2.0
'@tauri-apps/api': 1.2.0
dayjs: 1.11.7
emoji-mart: 5.5.2
events: 3.3.0
google-protobuf: 3.21.2
i18next: 22.4.10
@ -467,6 +473,20 @@ packages:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: false
/@emoji-mart/data/1.1.2:
resolution: {integrity: sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==}
dev: false
/@emoji-mart/react/1.1.1_kyrnz3vmphzqyjjk2ivrm6bcsu:
resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==}
peerDependencies:
emoji-mart: ^5.2
react: ^16.8 || ^17 || ^18
dependencies:
emoji-mart: 5.5.2
react: 18.2.0
dev: false
/@emotion/babel-plugin/11.10.6:
resolution: {integrity: sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==}
dependencies:
@ -2308,6 +2328,10 @@ packages:
engines: {node: '>=12'}
dev: false
/emoji-mart/5.5.2:
resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==}
dev: false
/emoji-regex/8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: false

View File

@ -1,4 +1,4 @@
import { toggleFormat, isFormatActive } from '$app/utils/document/slate/format';
import { toggleFormat, isFormatActive } from '$app/utils/document/blocks/text/format';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';

View File

@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react';
import { useFocused, useSlate } from 'slate-react';
import { calcToolbarPosition } from '$app/utils/document/slate/toolbar';
import { calcToolbarPosition } from '$app/utils/document/blocks/text/toolbar';
export function useHoveringToolbar(id: string) {
const editor = useSlate();
const inFocus = useFocused();

View File

@ -133,14 +133,16 @@ export function useBlockSelection({
useEffect(() => {
if (!ref.current) return;
document.addEventListener('mousedown', handleDragStart);
document.addEventListener('mousemove', handleDraging);
document.addEventListener('mouseup', handleDragEnd);
const doc = document.getElementById('appflowy-block-doc');
if (!doc) return;
doc.addEventListener('mousedown', handleDragStart);
doc.addEventListener('mousemove', handleDraging);
doc.addEventListener('mouseup', handleDragEnd);
return () => {
document.removeEventListener('mousedown', handleDragStart);
document.removeEventListener('mousemove', handleDraging);
document.removeEventListener('mouseup', handleDragEnd);
doc.removeEventListener('mousedown', handleDragStart);
doc.removeEventListener('mousemove', handleDraging);
doc.removeEventListener('mouseup', handleDragEnd);
};
}, [handleDragStart, handleDragEnd, handleDraging]);

View File

@ -0,0 +1,48 @@
import { useCallback, useContext, useMemo, useState } from 'react';
import emojiData, { EmojiMartData, Emoji } from '@emoji-mart/data';
import { useAppDispatch } from '$app/stores/store';
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
export function useCalloutBlock(nodeId: string) {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = useMemo(() => Boolean(anchorEl), [anchorEl]);
const id = useMemo(() => (open ? 'emoji-popover' : undefined), [open]);
const dispatch = useAppDispatch();
const controller = useContext(DocumentControllerContext);
const closeEmojiSelect = useCallback(() => {
setAnchorEl(null);
}, []);
const openEmojiSelect = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
}, []);
const onEmojiSelect = useCallback(
(emoji: { native: string }) => {
if (!controller) return;
console.log('emoji', emoji.native);
void dispatch(
updateNodeDataThunk({
id: nodeId,
controller,
data: {
icon: emoji.native,
},
})
);
closeEmojiSelect();
},
[controller, dispatch, nodeId, closeEmojiSelect]
);
return {
anchorEl,
closeEmojiSelect,
openEmojiSelect,
open,
id,
onEmojiSelect,
};
}

View File

@ -0,0 +1,51 @@
import { BlockType, NestedBlock } from '$app/interfaces/document';
import TextBlock from '$app/components/document/TextBlock';
import NodeChildren from '$app/components/document/Node/NodeChildren';
import { IconButton, Popover } from '@mui/material';
import emojiData from '@emoji-mart/data';
import Picker from '@emoji-mart/react';
import { useCalloutBlock } from '$app/components/document/CalloutBlock/CalloutBlock.hooks';
export default function CalloutBlock({
node,
childIds,
}: {
node: NestedBlock<BlockType.CalloutBlock>;
childIds?: string[];
}) {
const { openEmojiSelect, open, closeEmojiSelect, id, anchorEl, onEmojiSelect } = useCalloutBlock(node.id);
return (
<div className={'my-1 flex rounded border border-solid border-main-accent bg-main-secondary p-4'}>
<div className={'w-[1.5em]'}>
<div className={'flex h-[calc(1.5em_+_2px)] w-[24px] select-none items-center justify-start'}>
<IconButton
aria-describedby={id}
onClick={openEmojiSelect}
className={`m-0 h-[100%] w-[100%] rounded-full p-0 transition`}
>
{node.data.icon}
</IconButton>
<Popover
id={id}
anchorEl={anchorEl}
open={open}
onClose={closeEmojiSelect}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<Picker searchPosition={'static'} locale={'en'} autoFocus data={emojiData} onEmojiSelect={onEmojiSelect} />
</Popover>
</div>
</div>
<div className={'flex-1'}>
<div>
<TextBlock node={node} />
</div>
<NodeChildren childIds={childIds} />
</div>
</div>
);
}

View File

@ -0,0 +1,7 @@
export default function DividerBlock() {
return (
<div className={`flex h-[1em] w-[100%] items-center justify-center`}>
<div className={'h-[1px] w-[100%] bg-shade-5'} />
</div>
);
}

View File

@ -13,6 +13,8 @@ import QuoteBlock from '$app/components/document/QuoteBlock';
import BulletedListBlock from '$app/components/document/BulletedListBlock';
import NumberedListBlock from '$app/components/document/NumberedListBlock';
import ToggleListBlock from '$app/components/document/ToggleListBlock';
import DividerBlock from '$app/components/document/DividerBlock';
import CalloutBlock from '$app/components/document/CalloutBlock';
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
const { node, childIds, isSelected, ref } = useNode(id);
@ -40,6 +42,12 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
case BlockType.ToggleListBlock: {
return <ToggleListBlock node={node} childIds={childIds} />;
}
case BlockType.DividerBlock: {
return <DividerBlock />;
}
case BlockType.CalloutBlock: {
return <CalloutBlock node={node} childIds={childIds} />;
}
default:
return (
<Alert severity='info' className='mb-2'>

View File

@ -12,7 +12,7 @@ import {
canHandleUpKey,
onHandleEnterKey,
triggerHotkey,
} from '$app/utils/document/slate/hotkey';
} from '$app/utils/document/blocks/text/hotkey';
import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document';
import { useActions } from './Actions.hooks';

View File

@ -3,10 +3,10 @@ import { BlockData, BlockType, TextBlockKeyEventHandlerParams } from '$app/inter
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
import { useAppDispatch } from '$app/stores/store';
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
import { turnToBlockThunk } from '$app_reducers/document/async-actions';
import { turnToBlockThunk, turnToDividerBlockThunk } 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 { getBeforeRangeAt } from '$app/utils/document/blocks/text/delta';
import {
getHeadingDataFromEditor,
getQuoteDataFromEditor,
@ -14,27 +14,29 @@ import {
getBulletedDataFromEditor,
getNumberedListDataFromEditor,
getToggleListDataFromEditor,
getCalloutDataFromEditor,
} 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,
[BlockType.ToggleListBlock]: getToggleListDataFromEditor,
};
import { getDeltaAfterSelection } from '$app/utils/document/blocks/common';
export function useTurnIntoBlock(id: string) {
const controller = useContext(DocumentControllerContext);
const dispatch = useAppDispatch();
const turnIntoBlockEvents = useMemo(() => {
return Object.entries(blockDataFactoryMap).map(([type, getData]) => {
const spaceTriggerEvents = Object.entries({
[BlockType.HeadingBlock]: getHeadingDataFromEditor,
[BlockType.TodoListBlock]: getTodoListDataFromEditor,
[BlockType.QuoteBlock]: getQuoteDataFromEditor,
[BlockType.BulletedListBlock]: getBulletedDataFromEditor,
[BlockType.NumberedListBlock]: getNumberedListDataFromEditor,
[BlockType.ToggleListBlock]: getToggleListDataFromEditor,
[BlockType.CalloutBlock]: getCalloutDataFromEditor,
}).map(([type, getData]) => {
const blockType = type as BlockType;
const triggerKey = keyBoardEventKeyMap.Space;
return {
triggerEventKey: keyBoardEventKeyMap.Space,
canHandle: canHandle(blockType),
canHandle: canHandle(blockType, triggerKey),
handler: (...args: TextBlockKeyEventHandlerParams) => {
if (!controller) return;
const [_event, editor] = args;
@ -43,7 +45,20 @@ export function useTurnIntoBlock(id: string) {
dispatch(turnToBlockThunk({ id, data, type: blockType, controller }));
},
};
}, []);
});
return [
...spaceTriggerEvents,
{
triggerEventKey: keyBoardEventKeyMap.Reduce,
canHandle: canHandle(BlockType.DividerBlock, keyBoardEventKeyMap.Reduce),
handler: (...args: TextBlockKeyEventHandlerParams) => {
if (!controller) return;
const [_event, editor] = args;
const delta = getDeltaAfterSelection(editor) || [];
dispatch(turnToDividerBlockThunk({ id, controller, delta }));
},
},
];
}, [controller, dispatch, id]);
return {
@ -51,7 +66,7 @@ export function useTurnIntoBlock(id: string) {
};
}
function canHandle(type: BlockType) {
function canHandle(type: BlockType, triggerKey: string) {
const config = blockConfig[type];
const regex = config.markdownRegexps;
@ -62,16 +77,16 @@ function canHandle(type: BlockType) {
return (...args: TextBlockKeyEventHandlerParams) => {
const [event, editor] = args;
const isSpaceKey = event.key === keyBoardEventKeyMap.Space;
const isTrigger = event.key === triggerKey;
const selection = editor.selection;
if (!isSpaceKey || !selection) {
if (!isTrigger || !selection) {
return false;
}
const flag = Editor.string(editor, getBeforeRangeAt(editor, selection)).trim();
if (flag === null) return false;
return regex.some((r) => r.test(flag));
return regex.some((r) => r.test(`${flag}${triggerKey}`));
};
}

View File

@ -3,7 +3,7 @@ import Leaf from './Leaf';
import { useTextBlock } from './TextBlock.hooks';
import BlockHorizontalToolbar from '../BlockHorizontalToolbar';
import React from 'react';
import { NestedBlock } from '$app/interfaces/document';
import { BlockType, NestedBlock } from '$app/interfaces/document';
import NodeChildren from '$app/components/document/Node/NodeChildren';
function TextBlock({

View File

@ -11,7 +11,8 @@ import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
import { updateNodeDeltaThunk } from '$app_reducers/document/async-actions/blocks/text/update';
import { deltaToSlateValue, getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
import { documentActions } from '@/appflowy_app/stores/reducers/document/slice';
import { isSameDelta } from '$app/utils/document/blocks/text';
import { isSameDelta } from '$app/utils/document/blocks/text/delta';
export function useTextInput(id: string) {
const dispatch = useAppDispatch();
@ -175,7 +176,7 @@ function setSelection(editor: ReactEditor, currentSelection: TextSelection) {
const children = getDeltaFromSlateNodes(editor.children);
// the path always has 2 elements,
// because the slate node is a two-dimensional array
// because the text node is a two-dimensional array
const index = path[1];
// It is possible that the current selection is out of range
if (children[index].insert.length < offset) {

View File

@ -58,7 +58,7 @@ export const blockConfig: Record<
/**
* # or ## or ###
*/
markdownRegexps: [/^(#{1,3})$/],
markdownRegexps: [/^(#{1,3})(\s)+$/],
},
[BlockType.TodoListBlock]: {
canAddChild: true,
@ -73,7 +73,7 @@ export const blockConfig: Record<
/**
* -[] or -[x] or -[ ] or [] or [x] or [ ]
*/
markdownRegexps: [/^((-)?\[(x|\s)?\])$/],
markdownRegexps: [/^((-)?\[(x|\s)?\])(\s)+$/],
},
[BlockType.BulletedListBlock]: {
canAddChild: true,
@ -88,7 +88,7 @@ export const blockConfig: Record<
/**
* - or + or *
*/
markdownRegexps: [/^(\s*[-+*])$/],
markdownRegexps: [/^(\s*[-+*])(\s)+$/],
},
[BlockType.NumberedListBlock]: {
canAddChild: true,
@ -104,7 +104,7 @@ export const blockConfig: Record<
* 1. or 2. or 3.
* a. or b. or c.
*/
markdownRegexps: [/^(\s*[\d|a-zA-Z]+\.)$/],
markdownRegexps: [/^(\s*[\d|a-zA-Z]+\.)(\s)+$/],
},
[BlockType.QuoteBlock]: {
canAddChild: true,
@ -119,7 +119,22 @@ export const blockConfig: Record<
/**
* " or or
*/
markdownRegexps: [/^("|“|”)$/],
markdownRegexps: [/^("|“|”)(\s)+$/],
},
[BlockType.CalloutBlock]: {
canAddChild: true,
defaultData: {
delta: [],
icon: 'bulb',
},
splitProps: {
nextLineRelationShip: SplitRelationship.NextSibling,
nextLineBlockType: BlockType.TextBlock,
},
/**
* [!TIP] or [!INFO] or [!WARNING] or [!DANGER]
*/
markdownRegexps: [/^(\[!)(TIP|INFO|WARNING|DANGER)(\])(\s)+$/],
},
[BlockType.ToggleListBlock]: {
canAddChild: true,
@ -134,8 +149,16 @@ export const blockConfig: Record<
/**
* >
*/
markdownRegexps: [/^(>)$/],
markdownRegexps: [/^(>)(\s)+$/],
},
[BlockType.DividerBlock]: {
canAddChild: false,
/**
* ---
*/
markdownRegexps: [/^(-{3,})$/],
},
[BlockType.CodeBlock]: {
canAddChild: false,
/**

View File

@ -7,4 +7,5 @@ export const keyBoardEventKeyMap = {
Left: 'ArrowLeft',
Right: 'ArrowRight',
Space: ' ',
Reduce: '-',
};

View File

@ -42,10 +42,16 @@ export interface QuoteBlockData extends TextBlockData {
size: 'default' | 'large';
}
export interface CalloutBlockData extends TextBlockData {
icon: string;
}
export interface TextBlockData {
delta: TextDelta[];
}
export interface DividerBlockData {}
export type PageBlockData = TextBlockData;
export type BlockData<Type> = Type extends BlockType.HeadingBlock
@ -62,7 +68,13 @@ export type BlockData<Type> = Type extends BlockType.HeadingBlock
? NumberedListBlockData
: Type extends BlockType.ToggleListBlock
? ToggleListBlockData
: TextBlockData;
: Type extends BlockType.DividerBlock
? DividerBlockData
: Type extends BlockType.CalloutBlock
? CalloutBlockData
: Type extends BlockType.TextBlock
? TextBlockData
: any;
export interface NestedBlock<Type = any> {
id: string;

View File

@ -1,22 +1,28 @@
import { DocumentState } from '$app/interfaces/document';
import { BlockData, BlockType, DocumentState } from '$app/interfaces/document';
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { newTextBlock } from '$app/utils/document/blocks/text';
import { newBlock } from '$app/utils/document/blocks/common';
export const insertAfterNodeThunk = createAsyncThunk(
'document/insertAfterNode',
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
const { controller } = payload;
const { dispatch, getState } = thunkAPI;
async (payload: { id: string; controller: DocumentController; data?: BlockData<any>; type?: BlockType }, thunkAPI) => {
const {
controller,
type = BlockType.TextBlock,
data = {
delta: [],
},
} = payload;
const { getState } = thunkAPI;
const state = getState() as { document: DocumentState };
const node = state.document.nodes[payload.id];
if (!node) return;
const parentId = node.parent;
if (!parentId) return;
// create new node
const newNode = newTextBlock(parentId, {
delta: [],
});
const newNode = newBlock<any>(type, parentId, data);
await controller.applyActions([controller.getInsertAction(newNode, node.id)]);
return newNode.id;
}
);

View File

@ -9,7 +9,7 @@ import {
getNodeBeginSelection,
getNodeEndSelection,
getStartLineSelectionByOffset,
} from '$app/utils/document/slate/text';
} from '$app/utils/document/blocks/text/delta';
import { getNextLineId, getPrevLineId } from '$app/utils/document/blocks/common';
export const setCursorBeforeThunk = createAsyncThunk(
@ -43,13 +43,15 @@ export const setCursorPreLineThunk = createAsyncThunk(
const state = (getState() as { document: DocumentState }).document;
const prevId = getPrevLineId(state, id);
if (!prevId) return;
const prevLineNode = state.nodes[prevId];
// if prev line have no delta, just set block is selected
if (!prevLineNode.data.delta) {
dispatch(documentActions.setSelectionById(prevId));
return;
let prevLineNode = state.nodes[prevId];
// Find the prev line that has delta
while (prevLineNode && !prevLineNode.data.delta) {
const id = getPrevLineId(state, prevLineNode.id);
if (!id) return;
prevLineNode = state.nodes[id];
}
if (!prevLineNode) return;
// whatever the selection is, set cursor to the end of prev line when focusEnd is true
if (focusEnd) {
@ -76,14 +78,16 @@ export const setCursorNextLineThunk = createAsyncThunk(
const node = state.nodes[id];
const nextId = getNextLineId(state, id);
if (!nextId) return;
const nextLineNode = state.nodes[nextId];
const delta = nextLineNode.data.delta;
// if next line have no delta, just set block is selected
if (!delta) {
dispatch(documentActions.setSelectionById(nextId));
return;
let nextLineNode = state.nodes[nextId];
// Find the next line that has delta
while (nextLineNode && !nextLineNode.data.delta) {
const id = getNextLineId(state, nextLineNode.id);
if (!id) return;
nextLineNode = state.nodes[id];
}
if (!nextLineNode) return;
const delta = nextLineNode.data.delta;
// whatever the selection is, set cursor to the start of next line when focusStart is true
if (focusStart) {
await dispatch(setCursorBeforeThunk({ id: nextLineNode.id }));

View File

@ -1,9 +1,10 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { BlockData, BlockType, DocumentState, NestedBlock } from '$app/interfaces/document';
import { BlockData, BlockType, DocumentState, NestedBlock, TextDelta } from '$app/interfaces/document';
import { setCursorBeforeThunk } from '$app_reducers/document/async-actions/cursor';
import { blockConfig } from '$app/constants/document/config';
import { newBlock } from '$app/utils/document/blocks/common';
import { insertAfterNodeThunk } from '$app_reducers/document/async-actions/blocks';
/**
* transform to block
@ -47,3 +48,29 @@ export const turnToBlockThunk = createAsyncThunk(
await dispatch(setCursorBeforeThunk({ id: block.id }));
}
);
/**
* turn to divider block
* 1. insert text block with delta after current block
* 2. turn current block to divider block
*/
export const turnToDividerBlockThunk = createAsyncThunk(
'document/turnToDividerBlock',
async (payload: { id: string; controller: DocumentController; delta: TextDelta[] }, thunkAPI) => {
const { id, controller, delta } = payload;
const { dispatch } = thunkAPI;
const { payload: newNodeId } = await dispatch(
insertAfterNodeThunk({
id,
controller,
type: BlockType.TextBlock,
data: {
delta,
},
})
);
if (!newNodeId) return;
await dispatch(turnToBlockThunk({ id, type: BlockType.DividerBlock, controller, data: {} }));
dispatch(setCursorBeforeThunk({ id: newNodeId as string }));
}
);

View File

@ -3,7 +3,7 @@ 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';
import { getAfterRangeAt } from '$app/utils/document/blocks/text/delta';
export function deltaToSlateValue(delta: TextDelta[]) {
const slateNode = {

View File

@ -1,12 +1,13 @@
import { Editor } from 'slate';
import {
BulletListBlockData,
CalloutBlockData,
HeadingBlockData,
NumberedListBlockData,
TodoListBlockData,
ToggleListBlockData,
} from '$app/interfaces/document';
import { getBeforeRangeAt } from '$app/utils/document/slate/text';
import { getBeforeRangeAt } from '$app/utils/document/blocks/text/delta';
import { getDeltaAfterSelection } from '$app/utils/document/blocks/common';
/**
@ -94,3 +95,26 @@ export function getToggleListDataFromEditor(editor: Editor): ToggleListBlockData
collapsed: false,
};
}
/**
* get callout data from editor, only support markdown
*/
export function getCalloutDataFromEditor(editor: Editor): CalloutBlockData | undefined {
const delta = getDeltaAfterSelection(editor);
if (!delta) return;
const selection = editor.selection;
if (!selection) return;
const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection));
const tag = hashTags.match(/(TIP|INFO|WARNING|DANGER)/g)?.[0];
if (!tag) return;
const iconMap: Record<string, string> = {
TIP: '💡',
INFO: '❗',
WARNING: '⚠️',
DANGER: '‼️',
};
return {
delta,
icon: iconMap[tag],
};
}

View File

@ -1,16 +0,0 @@
import { BlockType, NestedBlock, TextBlockData, TextDelta } from '$app/interfaces/document';
import { newBlock } from '$app/utils/document/blocks/common';
import * as Y from 'yjs';
export function newTextBlock(parentId: string, data: TextBlockData): NestedBlock {
return newBlock<BlockType.TextBlock>(BlockType.TextBlock, parentId, data);
}
export function isSameDelta(referDelta: TextDelta[], delta: TextDelta[]) {
const ydoc = new Y.Doc();
const yText = ydoc.getText('1');
const yTextRefer = ydoc.getText('2');
yText.applyDelta(delta);
yTextRefer.applyDelta(referDelta);
return JSON.stringify(yText.toDelta()) === JSON.stringify(yTextRefer.toDelta());
}

View File

@ -1,5 +1,6 @@
import { Editor, Element, Text, Location } from 'slate';
import { Editor, Element, Location, Text } from 'slate';
import { SelectionPoint, TextDelta, TextSelection } from '$app/interfaces/document';
import * as Y from 'yjs';
export function getDelta(editor: Editor, at: Location): TextDelta[] {
const baseElement = Editor.fragment(editor, at)[0] as Element;
@ -198,3 +199,12 @@ export function clonePoint(point: SelectionPoint): SelectionPoint {
offset: point.offset,
};
}
export function isSameDelta(referDelta: TextDelta[], delta: TextDelta[]) {
const ydoc = new Y.Doc();
const yText = ydoc.getText('1');
const yTextRefer = ydoc.getText('2');
yText.applyDelta(delta);
yTextRefer.applyDelta(referDelta);
return JSON.stringify(yText.toDelta()) === JSON.stringify(yTextRefer.toDelta());
}

View File

@ -1,7 +1,7 @@
import isHotkey from 'is-hotkey';
import { toggleFormat } from './format';
import { Editor, Range } from 'slate';
import { clonePoint, getAfterRangeAt, getBeforeRangeAt, getDelta, pointInBegin, pointInEnd } from './text';
import { clonePoint, getAfterRangeAt, getBeforeRangeAt, getDelta, pointInBegin, pointInEnd } from './delta';
import { SelectionPoint, TextDelta, TextSelection } from '$app/interfaces/document';
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';