mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: Support heading block (#2376)
* feat: support transform heading block according to markdown * fix: folder scroll
This commit is contained in:
parent
55cb7acc7f
commit
f5b23e5fc1
@ -1,8 +1,8 @@
|
||||
import { toggleFormat, isFormatActive } from '$app/utils/slate/format';
|
||||
import { toggleFormat, isFormatActive } from '$app/utils/document/slate/format';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
import { command } from '$app/constants/toolbar';
|
||||
import { command } from '$app/constants/document/toolbar';
|
||||
import FormatIcon from './FormatIcon';
|
||||
import { BaseEditor } from 'slate';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material';
|
||||
import { iconSize } from '$app/constants/toolbar';
|
||||
import { iconSize } from '$app/constants/document/toolbar';
|
||||
|
||||
export default function FormatIcon({ icon }: { icon: string }) {
|
||||
switch (icon) {
|
@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useFocused, useSlate } from 'slate-react';
|
||||
import { calcToolbarPosition } from '$app/utils/slate/toolbar';
|
||||
import { calcToolbarPosition } from '$app/utils/document/slate/toolbar';
|
||||
export function useHoveringToolbar(id: string) {
|
||||
const editor = useSlate();
|
||||
const inFocus = useFocused();
|
@ -1,8 +1,8 @@
|
||||
import FormatButton from './FormatButton';
|
||||
import Portal from '../../BlockPortal';
|
||||
import Portal from '../BlockPortal';
|
||||
import { useHoveringToolbar } from './index.hooks';
|
||||
|
||||
const HoveringToolbar = ({ id }: { id: string }) => {
|
||||
const BlockHorizontalToolbar = ({ id }: { id: string }) => {
|
||||
const { inFocus, ref, editor } = useHoveringToolbar(id);
|
||||
if (!inFocus) return null;
|
||||
|
||||
@ -27,4 +27,4 @@ const HoveringToolbar = ({ id }: { id: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default HoveringToolbar;
|
||||
export default BlockHorizontalToolbar;
|
@ -1,7 +1,7 @@
|
||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||
import { useAppDispatch } from '@/appflowy_app/stores/store';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { insertAfterNodeThunk, deleteNodeThunk } from '@/appflowy_app/stores/reducers/document/async_actions';
|
||||
import { insertAfterNodeThunk, deleteNodeThunk } from '$app/stores/reducers/document/async-actions';
|
||||
|
||||
export enum ActionType {
|
||||
InsertAfter = 'insertAfter',
|
||||
|
@ -3,7 +3,7 @@ import { useAppSelector } from '@/appflowy_app/stores/store';
|
||||
import { debounce } from '@/appflowy_app/utils/tool';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
export function useBlockSideTools({ container }: { container: HTMLDivElement }) {
|
||||
export function useBlockSideToolbar({ container }: { container: HTMLDivElement }) {
|
||||
const [nodeId, setHoverNodeId] = useState<string>('');
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useBlockSideTools } from './BlockSideTools.hooks';
|
||||
import { useBlockSideToolbar } from './BlockSideToolbar.hooks';
|
||||
import ExpandCircleDownSharpIcon from '@mui/icons-material/ExpandCircleDownSharp';
|
||||
import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded';
|
||||
import Portal from '../BlockPortal';
|
||||
@ -8,8 +8,8 @@ import BlockMenu from '../BlockMenu';
|
||||
|
||||
const sx = { height: 24, width: 24 };
|
||||
|
||||
export default function BlockSideTools(props: { container: HTMLDivElement }) {
|
||||
const { nodeId, ref, menuOpen, handleToggleMenu } = useBlockSideTools(props);
|
||||
export default function BlockSideToolbar(props: { container: HTMLDivElement }) {
|
||||
const { nodeId, ref, menuOpen, handleToggleMenu } = useBlockSideToolbar(props);
|
||||
|
||||
if (!nodeId) return null;
|
||||
return (
|
@ -1,22 +1,16 @@
|
||||
import TextBlock from '../TextBlock';
|
||||
import { HeadingBlockData, Node } from '@/appflowy_app/interfaces/document';
|
||||
import { BlockType, NestedBlock } from '@/appflowy_app/interfaces/document';
|
||||
|
||||
const fontSize: Record<string, string> = {
|
||||
1: 'mt-8 text-3xl',
|
||||
2: 'mt-6 text-2xl',
|
||||
3: 'mt-4 text-xl',
|
||||
1: 'mt-5 text-3xl',
|
||||
2: 'mt-4 text-2xl',
|
||||
3: 'text-xl',
|
||||
};
|
||||
|
||||
export default function HeadingBlock({
|
||||
node,
|
||||
}: {
|
||||
node: Node & {
|
||||
data: HeadingBlockData;
|
||||
};
|
||||
}) {
|
||||
export default function HeadingBlock({ node }: { node: NestedBlock<BlockType.HeadingBlock> }) {
|
||||
return (
|
||||
<div className={`${fontSize[node.data.level]} font-semibold `}>
|
||||
{/*<TextBlock node={node} childIds={[]} delta={delta} />*/}
|
||||
<TextBlock node={node} childIds={[]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -4,18 +4,22 @@ import { withErrorBoundary } from 'react-error-boundary';
|
||||
import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
|
||||
import TextBlock from '../TextBlock';
|
||||
import { NodeContext } from '../_shared/SubscribeNode.hooks';
|
||||
import { Node } from '$app/interfaces/document';
|
||||
import { BlockType } from '$app/interfaces/document';
|
||||
import HeadingBlock from '$app/components/document/HeadingBlock';
|
||||
|
||||
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const { node, childIds, isSelected, ref } = useNode(id);
|
||||
|
||||
const renderBlock = useCallback(() => {
|
||||
switch (node.type) {
|
||||
case 'text': {
|
||||
case BlockType.TextBlock: {
|
||||
return <TextBlock node={node} childIds={childIds} />;
|
||||
}
|
||||
case BlockType.HeadingBlock: {
|
||||
return <HeadingBlock node={node} />;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
}, [node, childIds]);
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import BlockSideTools from '../BlockSideTools';
|
||||
import BlockSideToolbar from '../BlockSideToolbar';
|
||||
import BlockSelection from '../BlockSelection';
|
||||
|
||||
export default function Overlay({ container }: { container: HTMLDivElement }) {
|
||||
const [isDragging, setDragging] = useState(false);
|
||||
return (
|
||||
<>
|
||||
{isDragging ? null : <BlockSideTools container={container} />}
|
||||
{isDragging ? null : <BlockSideToolbar container={container} />}
|
||||
<BlockSelection onDragging={setDragging} container={container} />
|
||||
</>
|
||||
);
|
||||
|
@ -18,9 +18,17 @@ function Root({ documentData }: { documentData: DocumentData }) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div id='appflowy-block-doc' className='h-[100%] overflow-hidden'>
|
||||
<div
|
||||
id='appflowy-block-doc'
|
||||
className='h-[100%] overflow-hidden'
|
||||
onKeyDown={(e) => {
|
||||
// prevent backspace from going back
|
||||
if (e.key === 'Backspace') {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<VirtualizedList node={node} childIds={childIds} renderNode={renderNode} />
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { Editor } from 'slate';
|
||||
import { TextDelta, TextSelection } from '$app/interfaces/document';
|
||||
import { TextBlockKeyEventHandlerParams, TextDelta, TextSelection } from '$app/interfaces/document';
|
||||
import { useTextInput } from '../_shared/TextInput.hooks';
|
||||
import { useAppDispatch } from '@/appflowy_app/stores/store';
|
||||
import { DocumentControllerContext } from '@/appflowy_app/stores/effects/document/document_controller';
|
||||
@ -8,22 +8,24 @@ import {
|
||||
backspaceNodeThunk,
|
||||
indentNodeThunk,
|
||||
splitNodeThunk,
|
||||
} from '@/appflowy_app/stores/reducers/document/async_actions';
|
||||
setCursorNextLineThunk,
|
||||
setCursorPreLineThunk,
|
||||
} from '@/appflowy_app/stores/reducers/document/async-actions';
|
||||
import { documentActions } from '@/appflowy_app/stores/reducers/document/slice';
|
||||
import {
|
||||
triggerHotkey,
|
||||
canHandleEnterKey,
|
||||
canHandleBackspaceKey,
|
||||
canHandleTabKey,
|
||||
onHandleEnterKey,
|
||||
keyBoardEventKeyMap,
|
||||
canHandleUpKey,
|
||||
canHandleDownKey,
|
||||
canHandleEnterKey,
|
||||
canHandleLeftKey,
|
||||
canHandleRightKey,
|
||||
} from '@/appflowy_app/utils/slate/hotkey';
|
||||
import { updateNodeDeltaThunk } from '$app/stores/reducers/document/async_actions/update';
|
||||
import { setCursorPreLineThunk, setCursorNextLineThunk } from '$app/stores/reducers/document/async_actions/set_cursor';
|
||||
canHandleTabKey,
|
||||
canHandleUpKey,
|
||||
onHandleEnterKey,
|
||||
triggerHotkey,
|
||||
} from '$app/utils/document/slate/hotkey';
|
||||
import { updateNodeDeltaThunk } from '$app_reducers/document/async-actions/blocks/text/update';
|
||||
import { useMarkDown } from './useMarkDown.hooks';
|
||||
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
|
||||
|
||||
export function useTextBlock(id: string) {
|
||||
const { editor, onChange, value } = useTextInput(id);
|
||||
@ -54,25 +56,15 @@ export function useTextBlock(id: string) {
|
||||
};
|
||||
}
|
||||
|
||||
type TextBlockKeyEventHandlerParams = [React.KeyboardEvent<HTMLDivElement>, Editor];
|
||||
|
||||
function useTextBlockKeyEvent(id: string, editor: Editor) {
|
||||
const { indentAction, backSpaceAction, splitAction, wrapAction, focusPreLineAction, focusNextLineAction } =
|
||||
useActions(id);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const keepSelection = useCallback(() => {
|
||||
// This is a hack to make sure the selection is updated after next render
|
||||
// It will save the selection to the store, and the selection will be restored
|
||||
if (!editor.selection || !editor.selection.anchor || !editor.selection.focus) return;
|
||||
const { anchor, focus } = editor.selection;
|
||||
const selection = { anchor, focus } as TextSelection;
|
||||
dispatch(documentActions.setTextSelection({ blockId: id, selection }));
|
||||
}, [editor]);
|
||||
const { markdownEvents } = useMarkDown(id);
|
||||
|
||||
const enterEvent = useMemo(() => {
|
||||
return {
|
||||
key: keyBoardEventKeyMap.Enter,
|
||||
triggerEventKey: keyBoardEventKeyMap.Enter,
|
||||
canHandle: canHandleEnterKey,
|
||||
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||
onHandleEnterKey(...args, {
|
||||
@ -85,29 +77,27 @@ function useTextBlockKeyEvent(id: string, editor: Editor) {
|
||||
|
||||
const tabEvent = useMemo(() => {
|
||||
return {
|
||||
key: keyBoardEventKeyMap.Tab,
|
||||
triggerEventKey: keyBoardEventKeyMap.Tab,
|
||||
canHandle: canHandleTabKey,
|
||||
handler: (..._args: TextBlockKeyEventHandlerParams) => {
|
||||
keepSelection();
|
||||
void indentAction();
|
||||
},
|
||||
};
|
||||
}, [keepSelection, indentAction]);
|
||||
}, [indentAction]);
|
||||
|
||||
const backSpaceEvent = useMemo(() => {
|
||||
return {
|
||||
key: keyBoardEventKeyMap.Backspace,
|
||||
triggerEventKey: keyBoardEventKeyMap.Backspace,
|
||||
canHandle: canHandleBackspaceKey,
|
||||
handler: (..._args: TextBlockKeyEventHandlerParams) => {
|
||||
keepSelection();
|
||||
void backSpaceAction();
|
||||
},
|
||||
};
|
||||
}, [keepSelection, backSpaceAction]);
|
||||
}, [backSpaceAction]);
|
||||
|
||||
const upEvent = useMemo(() => {
|
||||
return {
|
||||
key: keyBoardEventKeyMap.Up,
|
||||
triggerEventKey: keyBoardEventKeyMap.Up,
|
||||
canHandle: canHandleUpKey,
|
||||
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||
void focusPreLineAction({
|
||||
@ -119,7 +109,7 @@ function useTextBlockKeyEvent(id: string, editor: Editor) {
|
||||
|
||||
const downEvent = useMemo(() => {
|
||||
return {
|
||||
key: keyBoardEventKeyMap.Down,
|
||||
triggerEventKey: keyBoardEventKeyMap.Down,
|
||||
canHandle: canHandleDownKey,
|
||||
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||
void focusNextLineAction({
|
||||
@ -131,7 +121,7 @@ function useTextBlockKeyEvent(id: string, editor: Editor) {
|
||||
|
||||
const leftEvent = useMemo(() => {
|
||||
return {
|
||||
key: keyBoardEventKeyMap.Left,
|
||||
triggerEventKey: keyBoardEventKeyMap.Left,
|
||||
canHandle: canHandleLeftKey,
|
||||
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||
void focusPreLineAction({
|
||||
@ -144,7 +134,7 @@ function useTextBlockKeyEvent(id: string, editor: Editor) {
|
||||
|
||||
const rightEvent = useMemo(() => {
|
||||
return {
|
||||
key: keyBoardEventKeyMap.Right,
|
||||
triggerEventKey: keyBoardEventKeyMap.Right,
|
||||
canHandle: canHandleRightKey,
|
||||
handler: (...args: TextBlockKeyEventHandlerParams) => {
|
||||
void focusNextLineAction({
|
||||
@ -159,6 +149,8 @@ function useTextBlockKeyEvent(id: string, editor: Editor) {
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
// This is list of key events that can be handled by TextBlock
|
||||
const keyEvents = [enterEvent, backSpaceEvent, tabEvent, upEvent, downEvent, leftEvent, rightEvent];
|
||||
|
||||
keyEvents.push(...markdownEvents);
|
||||
const matchKey = keyEvents.find((keyEvent) => keyEvent.canHandle(event, editor));
|
||||
if (!matchKey) {
|
||||
triggerHotkey(event, editor);
|
||||
@ -169,7 +161,7 @@ function useTextBlockKeyEvent(id: string, editor: Editor) {
|
||||
event.preventDefault();
|
||||
matchKey.handler(event, editor);
|
||||
},
|
||||
[editor, enterEvent, backSpaceEvent, tabEvent, upEvent, downEvent, leftEvent, rightEvent]
|
||||
[editor, enterEvent, backSpaceEvent, tabEvent, upEvent, downEvent, leftEvent, rightEvent, markdownEvents]
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -2,9 +2,9 @@ import { Slate, Editable } from 'slate-react';
|
||||
import Leaf from './Leaf';
|
||||
import { useTextBlock } from './TextBlock.hooks';
|
||||
import NodeComponent from '../Node';
|
||||
import HoveringToolbar from '../_shared/HoveringToolbar';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Node } from '$app/interfaces/document';
|
||||
import BlockHorizontalToolbar from '../BlockHorizontalToolbar';
|
||||
import React from 'react';
|
||||
import { BlockType, NestedBlock } from '$app/interfaces/document';
|
||||
|
||||
function TextBlock({
|
||||
node,
|
||||
@ -12,7 +12,7 @@ function TextBlock({
|
||||
placeholder,
|
||||
...props
|
||||
}: {
|
||||
node: Node;
|
||||
node: NestedBlock;
|
||||
childIds?: string[];
|
||||
placeholder?: string;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
@ -21,7 +21,7 @@ function TextBlock({
|
||||
<>
|
||||
<div {...props} className={`py-[2px] ${props.className}`}>
|
||||
<Slate editor={editor} onChange={onChange} value={value}>
|
||||
<HoveringToolbar id={node.id} />
|
||||
<BlockHorizontalToolbar id={node.id} />
|
||||
<Editable
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onDOMBeforeInput={onDOMBeforeInput}
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document';
|
||||
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
|
||||
import { canHandleToHeadingBlock } from '$app/utils/document/slate/markdown';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||
import { turnToHeadingBlockThunk } from '$app_reducers/document/async-actions/blocks/heading';
|
||||
|
||||
export function useMarkDown(id: string) {
|
||||
const { toHeadingBlockAction } = useActions(id);
|
||||
const toHeadingBlockEvent = useMemo(() => {
|
||||
return {
|
||||
triggerEventKey: keyBoardEventKeyMap.Space,
|
||||
canHandle: canHandleToHeadingBlock,
|
||||
handler: toHeadingBlockAction,
|
||||
};
|
||||
}, [toHeadingBlockAction]);
|
||||
|
||||
const markdownEvents = useMemo(() => [toHeadingBlockEvent], [toHeadingBlockEvent]);
|
||||
|
||||
return {
|
||||
markdownEvents,
|
||||
};
|
||||
}
|
||||
|
||||
function useActions(id: string) {
|
||||
const controller = useContext(DocumentControllerContext);
|
||||
const dispatch = useAppDispatch();
|
||||
const toHeadingBlockAction = useCallback(
|
||||
(...args: TextBlockKeyEventHandlerParams) => {
|
||||
if (!controller) return;
|
||||
const [_event, editor] = args;
|
||||
dispatch(turnToHeadingBlockThunk({ id, editor, controller }));
|
||||
},
|
||||
[controller, dispatch, id]
|
||||
);
|
||||
|
||||
return {
|
||||
toHeadingBlockAction,
|
||||
};
|
||||
}
|
@ -1,17 +1,16 @@
|
||||
import { createEditor, Descendant, Transforms } from 'slate';
|
||||
import { withReact, ReactEditor } from 'slate-react';
|
||||
import * as Y from 'yjs';
|
||||
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
|
||||
import { useCallback, useContext, useMemo, useRef, useEffect, useState } from 'react';
|
||||
|
||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||
import { TextDelta, TextSelection } from '$app/interfaces/document';
|
||||
import { NodeContext } from './SubscribeNode.hooks';
|
||||
import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
|
||||
|
||||
import { createEditor, Descendant, Transforms } from 'slate';
|
||||
import { withReact, ReactEditor } from 'slate-react';
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core';
|
||||
import { updateNodeDeltaThunk } from '@/appflowy_app/stores/reducers/document/async_actions/update';
|
||||
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 { deltaToSlateValue, getDeltaFromSlateNodes } from '@/appflowy_app/utils/block';
|
||||
|
||||
export function useTextInput(id: string) {
|
||||
const dispatch = useAppDispatch();
|
||||
@ -34,16 +33,35 @@ export function useTextInput(id: string) {
|
||||
|
||||
const [value, setValue] = useState<Descendant[]>([]);
|
||||
|
||||
const onChange = useCallback((e: Descendant[]) => {
|
||||
setValue(e);
|
||||
}, []);
|
||||
const storeSelection = useCallback(() => {
|
||||
// This is a hack to make sure the selection is updated after next render
|
||||
// It will save the selection to the store, and the selection will be restored
|
||||
if (!ReactEditor.isFocused(editor) || !editor.selection || !editor.selection.anchor || !editor.selection.focus)
|
||||
return;
|
||||
const { anchor, focus } = editor.selection;
|
||||
const selection = { anchor, focus } as TextSelection;
|
||||
dispatch(documentActions.setTextSelection({ blockId: id, selection }));
|
||||
}, [editor]);
|
||||
|
||||
const currentSelection = useAppSelector((state) => state.document.textSelections[id]);
|
||||
|
||||
useEffect(() => {
|
||||
const restoreSelection = useCallback(() => {
|
||||
if (editor.selection && JSON.stringify(currentSelection) === JSON.stringify(editor.selection)) return;
|
||||
setSelection(editor, currentSelection);
|
||||
}, [editor, currentSelection]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: Descendant[]) => {
|
||||
setValue(e);
|
||||
storeSelection();
|
||||
},
|
||||
|
||||
[storeSelection]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
restoreSelection();
|
||||
}, [restoreSelection]);
|
||||
|
||||
if (editor.selection && ReactEditor.isFocused(editor)) {
|
||||
const domSelection = window.getSelection();
|
||||
// this is a hack to fix the issue where the selection is not in the dom
|
||||
@ -98,7 +116,6 @@ function useBindYjs(id: string, delta: TextDelta[]) {
|
||||
};
|
||||
|
||||
yText.observe(textEventHandler);
|
||||
|
||||
return () => {
|
||||
yText.unobserve(textEventHandler);
|
||||
};
|
||||
@ -148,8 +165,10 @@ function useController(id: string) {
|
||||
function setSelection(editor: ReactEditor, currentSelection: TextSelection) {
|
||||
// If the current selection is empty, blur the editor and deselect the selection
|
||||
if (!currentSelection || !currentSelection.anchor || !currentSelection.focus) {
|
||||
ReactEditor.blur(editor);
|
||||
ReactEditor.deselect(editor);
|
||||
if (ReactEditor.isFocused(editor)) {
|
||||
ReactEditor.blur(editor);
|
||||
ReactEditor.deselect(editor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -105,14 +105,20 @@ export const NavigationPanel = ({
|
||||
<div className={'flex flex-col'}>
|
||||
<AppLogo iconToShow={'hide'} onHideMenuClick={onHideMenuClick}></AppLogo>
|
||||
<WorkspaceUser></WorkspaceUser>
|
||||
<div className={'relative flex flex-col'} style={{ height: 'calc(100vh - 300px)' }}>
|
||||
<div className={'flex flex-col overflow-auto px-2'} ref={el}>
|
||||
<div className={'relative flex flex-1 flex-col'}>
|
||||
<div
|
||||
className={'flex flex-col overflow-auto px-2'}
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 350px)',
|
||||
}}
|
||||
ref={el}
|
||||
>
|
||||
<WorkspaceApps folders={folders} pages={pages} onPageClick={onPageClick} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col'}>
|
||||
<div className={'flex max-h-[215px] flex-col'}>
|
||||
<div className={'border-b border-shade-6 px-2 pb-4'}>
|
||||
{/*<PluginsButton></PluginsButton>*/}
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { BlockType } from '$app/interfaces/document';
|
||||
|
||||
/**
|
||||
* Block types that are allowed to have children
|
||||
*/
|
||||
export const allowedChildrenBlockTypes = [BlockType.TextBlock, BlockType.PageBlock];
|
@ -0,0 +1,10 @@
|
||||
export const keyBoardEventKeyMap = {
|
||||
Enter: 'Enter',
|
||||
Backspace: 'Backspace',
|
||||
Tab: 'Tab',
|
||||
Up: 'ArrowUp',
|
||||
Down: 'ArrowDown',
|
||||
Left: 'ArrowLeft',
|
||||
Right: 'ArrowRight',
|
||||
Space: ' ',
|
||||
};
|
@ -1,3 +1,5 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
export enum BlockType {
|
||||
PageBlock = 'page',
|
||||
HeadingBlock = 'heading',
|
||||
@ -12,9 +14,8 @@ export enum BlockType {
|
||||
ColumnBlock = 'column',
|
||||
}
|
||||
|
||||
export interface HeadingBlockData {
|
||||
export interface HeadingBlockData extends TextBlockData {
|
||||
level: number;
|
||||
delta: TextDelta[];
|
||||
}
|
||||
|
||||
export interface TextBlockData {
|
||||
@ -23,12 +24,16 @@ export interface TextBlockData {
|
||||
|
||||
export type PageBlockData = TextBlockData;
|
||||
|
||||
export type BlockData = TextBlockData | HeadingBlockData | PageBlockData;
|
||||
export type BlockData<Type> = Type extends BlockType.HeadingBlock
|
||||
? HeadingBlockData
|
||||
: Type extends BlockType.PageBlock
|
||||
? PageBlockData
|
||||
: TextBlockData;
|
||||
|
||||
export interface NestedBlock {
|
||||
export interface NestedBlock<Type = any> {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
data: BlockData | Record<string, any>;
|
||||
data: BlockData<Type>;
|
||||
parent: string | null;
|
||||
children: string;
|
||||
}
|
||||
@ -98,3 +103,5 @@ export interface BlockPBValue {
|
||||
children: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export type TextBlockKeyEventHandlerParams = [React.KeyboardEvent<HTMLDivElement>, Editor];
|
||||
|
@ -12,9 +12,9 @@ import {
|
||||
} from '@/services/backend';
|
||||
import { DocumentObserver } from './document_observer';
|
||||
import * as Y from 'yjs';
|
||||
import { blockPB2Node } from '@/appflowy_app/utils/block';
|
||||
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '@/appflowy_app/constants/block';
|
||||
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '$app/constants/document/block';
|
||||
import { get } from '@/appflowy_app/utils/tool';
|
||||
import { blockPB2Node } from '$app/utils/document/blocks/common';
|
||||
|
||||
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
||||
|
||||
@ -46,7 +46,9 @@ export class DocumentController {
|
||||
if (document.ok) {
|
||||
const nodes: DocumentData['nodes'] = {};
|
||||
get<Map<string, BlockPB>>(document.val, [BLOCK_MAP_NAME]).forEach((block) => {
|
||||
nodes[block.id] = blockPB2Node(block);
|
||||
Object.assign(nodes, {
|
||||
[block.id]: blockPB2Node(block),
|
||||
});
|
||||
});
|
||||
const children: Record<string, string[]> = {};
|
||||
get<Map<string, ChildrenPB>>(document.val, [META_NAME, CHILDREN_MAP_NAME]).forEach((child, key) => {
|
||||
@ -97,6 +99,12 @@ export class DocumentController {
|
||||
};
|
||||
};
|
||||
|
||||
getMoveChildrenAction = (children: Node[], parentId: string, prevId: string | null) => {
|
||||
return children.reverse().map((child) => {
|
||||
return this.getMoveAction(child, parentId, prevId);
|
||||
});
|
||||
};
|
||||
|
||||
getDeleteAction = (node: Node) => {
|
||||
return {
|
||||
action: BlockActionTypePB.Delete,
|
||||
|
@ -0,0 +1,42 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { Editor } from 'slate';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { DocumentState } from '$app/interfaces/document';
|
||||
import { getHeadingDataFromEditor, newHeadingBlock } from '$app/utils/document/blocks/heading';
|
||||
import { setCursorBeforeThunk } from '$app_reducers/document/async-actions/cursor';
|
||||
|
||||
export const turnToHeadingBlockThunk = createAsyncThunk(
|
||||
'document/turnToHeadingBlock',
|
||||
async (payload: { id: string; editor: Editor; controller: DocumentController }, thunkAPI) => {
|
||||
const { id, editor, controller } = payload;
|
||||
const { dispatch, getState } = thunkAPI;
|
||||
const state = (getState() as { document: DocumentState }).document;
|
||||
|
||||
const node = state.nodes[id];
|
||||
if (!node.parent) return;
|
||||
|
||||
const parent = state.nodes[node.parent];
|
||||
const children = state.children[node.children].map((id) => state.nodes[id]);
|
||||
|
||||
/**
|
||||
* transform to heading block
|
||||
* 1. insert heading block after current block
|
||||
* 2. move all children to parent after heading block, because heading block can't have children
|
||||
* 3. delete current block
|
||||
*/
|
||||
|
||||
const data = getHeadingDataFromEditor(editor);
|
||||
if (!data) return;
|
||||
const headingBlock = newHeadingBlock(parent.id, data);
|
||||
const insertHeadingAction = controller.getInsertAction(headingBlock, node.id);
|
||||
|
||||
const moveChildrenActions = controller.getMoveChildrenAction(children, parent.id, headingBlock.id);
|
||||
|
||||
const deleteAction = controller.getDeleteAction(node);
|
||||
|
||||
// submit actions
|
||||
await controller.applyActions([insertHeadingAction, ...moveChildrenActions, deleteAction]);
|
||||
// set cursor
|
||||
await dispatch(setCursorBeforeThunk({ id: headingBlock.id }));
|
||||
}
|
||||
);
|
@ -1,10 +1,11 @@
|
||||
import { BlockType, DocumentState } from '@/appflowy_app/interfaces/document';
|
||||
import { BlockType, DocumentState } from '$app/interfaces/document';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { documentActions } from '../slice';
|
||||
import { documentActions } from '$app_reducers/document/slice';
|
||||
import { outdentNodeThunk } from './outdent';
|
||||
import { setCursorAfterThunk } from './set_cursor';
|
||||
import { getPrevLineId } from '$app/utils/block';
|
||||
import { setCursorAfterThunk } from '../../cursor';
|
||||
import { turnToTextBlockThunk } from '$app_reducers/document/async-actions/blocks/text/index';
|
||||
import { getPrevLineId } from '$app/utils/document/blocks/common';
|
||||
|
||||
const composeNodeThunk = createAsyncThunk(
|
||||
'document/composeNode',
|
||||
@ -32,11 +33,8 @@ const composeNodeThunk = createAsyncThunk(
|
||||
const updateAction = controller.getUpdateAction(newNode);
|
||||
|
||||
// move children
|
||||
const children = state.children[node.children];
|
||||
// the reverse can ensure that every child will be inserted in first place and don't need to update prevId
|
||||
const moveActions = children.reverse().map((childId) => {
|
||||
return controller.getMoveAction(state.nodes[childId], newNode.id, '');
|
||||
});
|
||||
const children = state.children[node.children].map((id) => state.nodes[id]);
|
||||
const moveActions = controller.getMoveChildrenAction(children, newNode.id, '');
|
||||
|
||||
// delete node
|
||||
const deleteAction = controller.getDeleteAction(node);
|
||||
@ -88,7 +86,8 @@ export const backspaceNodeThunk = createAsyncThunk(
|
||||
const nextNodeId = children[index + 1];
|
||||
// transform to text block
|
||||
if (node.type !== BlockType.TextBlock) {
|
||||
// todo: transform to text block
|
||||
await dispatch(turnToTextBlockThunk({ id, controller }));
|
||||
return;
|
||||
}
|
||||
// compose to previous line when it has next sibling or no ancestor
|
||||
if (nextNodeId || !ancestorId) {
|
@ -7,7 +7,7 @@ export const deleteNodeThunk = createAsyncThunk(
|
||||
'document/deleteNode',
|
||||
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||
const { id, controller } = payload;
|
||||
const { dispatch, getState } = thunkAPI;
|
||||
const { getState } = thunkAPI;
|
||||
const state = getState() as { document: DocumentState };
|
||||
const node = state.document.nodes[id];
|
||||
if (!node) return;
|
@ -1,6 +1,7 @@
|
||||
import { BlockType, DocumentState } from '@/appflowy_app/interfaces/document';
|
||||
import { BlockType, DocumentState } from '$app/interfaces/document';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { allowedChildrenBlockTypes } from '$app/constants/document/config';
|
||||
|
||||
export const indentNodeThunk = createAsyncThunk(
|
||||
'document/indentNode',
|
||||
@ -19,7 +20,7 @@ export const indentNodeThunk = createAsyncThunk(
|
||||
const newParentId = children[index - 1];
|
||||
const prevNode = state.nodes[newParentId];
|
||||
// check if prev node is allowed to have children
|
||||
if (prevNode.type !== BlockType.TextBlock) return;
|
||||
if (!allowedChildrenBlockTypes.includes(prevNode.type)) return;
|
||||
// check if prev node has children and get last child for new prev node
|
||||
const prevNodeChildren = state.children[prevNode.children];
|
||||
const newPrevId = prevNodeChildren[prevNodeChildren.length - 1];
|
@ -0,0 +1,39 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { DocumentState } from '$app/interfaces/document';
|
||||
import { newTextBlock } from '$app/utils/document/blocks/text';
|
||||
import { setCursorBeforeThunk } from '$app_reducers/document/async-actions/cursor';
|
||||
|
||||
export const turnToTextBlockThunk = createAsyncThunk(
|
||||
'document/turnToTextBlock',
|
||||
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||
const { id, controller } = payload;
|
||||
const { dispatch, getState } = thunkAPI;
|
||||
const state = (getState() as { document: DocumentState }).document;
|
||||
|
||||
const node = state.nodes[id];
|
||||
if (!node.parent) return;
|
||||
|
||||
const parent = state.nodes[node.parent];
|
||||
const children = state.children[node.children].map((id) => state.nodes[id]);
|
||||
|
||||
/**
|
||||
* transform to text block
|
||||
* 1. insert text block after current block
|
||||
* 2. move children to text block
|
||||
* 3. delete current block
|
||||
*/
|
||||
|
||||
const textBlock = newTextBlock(parent.id, {
|
||||
delta: node.data.delta,
|
||||
});
|
||||
const insertTextAction = controller.getInsertAction(textBlock, node.id);
|
||||
const moveChildrenActions = controller.getMoveChildrenAction(children, textBlock.id, '');
|
||||
const deleteAction = controller.getDeleteAction(node);
|
||||
|
||||
// submit actions
|
||||
await controller.applyActions([insertTextAction, ...moveChildrenActions, deleteAction]);
|
||||
// set cursor
|
||||
await dispatch(setCursorBeforeThunk({ id: textBlock.id }));
|
||||
}
|
||||
);
|
@ -1,7 +1,7 @@
|
||||
import { BlockType, DocumentState, NestedBlock } from '@/appflowy_app/interfaces/document';
|
||||
import { DocumentState } from '$app/interfaces/document';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { generateId } from '@/appflowy_app/utils/block';
|
||||
import { newTextBlock } from '$app/utils/document/blocks/text';
|
||||
|
||||
export const insertAfterNodeThunk = createAsyncThunk(
|
||||
'document/insertAfterNode',
|
||||
@ -14,15 +14,9 @@ export const insertAfterNodeThunk = createAsyncThunk(
|
||||
const parentId = node.parent;
|
||||
if (!parentId) return;
|
||||
// create new node
|
||||
const newNode: NestedBlock = {
|
||||
id: generateId(),
|
||||
parent: parentId,
|
||||
type: BlockType.TextBlock,
|
||||
data: {
|
||||
delta: [],
|
||||
},
|
||||
children: generateId(),
|
||||
};
|
||||
const newNode = newTextBlock(parentId, {
|
||||
delta: [],
|
||||
});
|
||||
await controller.applyActions([controller.getInsertAction(newNode, node.id)]);
|
||||
}
|
||||
);
|
@ -1,9 +1,9 @@
|
||||
import { BlockType, DocumentState, TextDelta } from '@/appflowy_app/interfaces/document';
|
||||
import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller';
|
||||
import { BlockType, DocumentState, TextDelta } from '$app/interfaces/document';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { generateId } from '@/appflowy_app/utils/block';
|
||||
import { documentActions } from '../slice';
|
||||
import { setCursorBeforeThunk } from './set_cursor';
|
||||
import { documentActions } from '$app_reducers/document/slice';
|
||||
import { setCursorBeforeThunk } from '../../cursor';
|
||||
import { newTextBlock } from '$app/utils/document/blocks/text';
|
||||
|
||||
export const splitNodeThunk = createAsyncThunk(
|
||||
'document/splitNode',
|
||||
@ -19,15 +19,10 @@ export const splitNodeThunk = createAsyncThunk(
|
||||
const children = state.children[node.children];
|
||||
const prevId = children.length > 0 ? null : node.id;
|
||||
const parent = children.length > 0 ? node : state.nodes[node.parent];
|
||||
const newNode = {
|
||||
id: generateId(),
|
||||
parent: parent.id,
|
||||
type: BlockType.TextBlock,
|
||||
data: {
|
||||
delta: insert,
|
||||
},
|
||||
children: generateId(),
|
||||
};
|
||||
|
||||
const newNode = newTextBlock(parent.id, {
|
||||
delta: insert,
|
||||
});
|
||||
const retainNode = {
|
||||
...node,
|
||||
data: {
|
@ -1,7 +1,7 @@
|
||||
import { TextDelta, NestedBlock, DocumentState } from '@/appflowy_app/interfaces/document';
|
||||
import { DocumentController } from '@/appflowy_app/stores/effects/document/document_controller';
|
||||
import { TextDelta, NestedBlock, DocumentState } from '$app/interfaces/document';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { documentActions } from '../slice';
|
||||
import { documentActions } from '$app_reducers/document/slice';
|
||||
import { debounce } from '$app/utils/tool';
|
||||
export const updateNodeDeltaThunk = createAsyncThunk(
|
||||
'document/updateNodeDelta',
|
@ -1,7 +1,6 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { documentActions } from '../slice';
|
||||
import { DocumentState, TextSelection } from '$app/interfaces/document';
|
||||
import { getNextLineId, getPrevLineId } from '$app/utils/block';
|
||||
import { Editor } from 'slate';
|
||||
import {
|
||||
getBeforeRangeAt,
|
||||
@ -10,7 +9,8 @@ import {
|
||||
getNodeBeginSelection,
|
||||
getNodeEndSelection,
|
||||
getStartLineSelectionByOffset,
|
||||
} from '$app/utils/slate/text';
|
||||
} from '$app/utils/document/slate/text';
|
||||
import { getNextLineId, getPrevLineId } from '$app/utils/document/blocks/common';
|
||||
|
||||
export const setCursorBeforeThunk = createAsyncThunk(
|
||||
'document/setCursorBefore',
|
@ -0,0 +1,7 @@
|
||||
export * from './blocks/text/delete';
|
||||
export * from './blocks/text/indent';
|
||||
export * from './blocks/text/insert';
|
||||
export * from './blocks/text/backspace';
|
||||
export * from './blocks/text/outdent';
|
||||
export * from './blocks/text/split';
|
||||
export * from './cursor';
|
@ -1,6 +0,0 @@
|
||||
export * from './delete';
|
||||
export * from './indent';
|
||||
export * from './insert';
|
||||
export * from './backspace';
|
||||
export * from './outdent';
|
||||
export * from './split';
|
@ -2,7 +2,7 @@ import { DocumentState, Node, TextSelection } from '@/appflowy_app/interfaces/do
|
||||
import { BlockEventPayloadPB } from '@/services/backend';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { RegionGrid } from '@/appflowy_app/utils/region_grid';
|
||||
import { parseValue, matchChange } from '@/appflowy_app/utils/block_change';
|
||||
import { parseValue, matchChange } from '$app/utils/document/subscribe';
|
||||
|
||||
const regionGrid = new RegionGrid(50);
|
||||
|
||||
@ -92,6 +92,8 @@ export const documentSlice = createSlice({
|
||||
) => {
|
||||
const { blockId, selection } = action.payload;
|
||||
const node = state.nodes[blockId];
|
||||
const oldSelection = state.textSelections[blockId];
|
||||
if (JSON.stringify(oldSelection) === JSON.stringify(selection)) return;
|
||||
if (!node || !selection) {
|
||||
delete state.textSelections[blockId];
|
||||
} else {
|
||||
|
@ -17,7 +17,7 @@ import { databaseSlice } from './reducers/database/slice';
|
||||
import { documentSlice } from './reducers/document/slice';
|
||||
import { boardSlice } from './reducers/board/slice';
|
||||
import { errorSlice } from './reducers/error/slice';
|
||||
import { activePageIdSlice } from './reducers/activePageId/slice';
|
||||
import { activePageIdSlice } from '$app_reducers/active-page-id/slice';
|
||||
|
||||
const listenerMiddlewareInstance = createListenerMiddleware({
|
||||
onError: () => console.error,
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { BlockPB } from '@/services/backend/models/flowy-document2';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { BlockData, BlockType, DocumentState, NestedBlock, TextDelta } from '$app/interfaces/document';
|
||||
import { Descendant, Element, Text } from 'slate';
|
||||
import { BlockType, DocumentState, NestedBlock, TextDelta } from '../interfaces/document';
|
||||
import { Log } from './log';
|
||||
export function generateId() {
|
||||
return nanoid(10);
|
||||
}
|
||||
import { BlockPB } from '@/services/backend';
|
||||
import { Log } from '$app/utils/log';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export function deltaToSlateValue(delta: TextDelta[]) {
|
||||
const slateNode = {
|
||||
@ -53,6 +50,10 @@ export function blockPB2Node(block: BlockPB) {
|
||||
return node;
|
||||
}
|
||||
|
||||
export function generateId() {
|
||||
return nanoid(10);
|
||||
}
|
||||
|
||||
export function getPrevLineId(state: DocumentState, id: string) {
|
||||
const node = state.nodes[id];
|
||||
if (!node.parent) return;
|
||||
@ -99,3 +100,13 @@ export function getNextNodeId(state: DocumentState, id: string) {
|
||||
const nextNodeId = children[index + 1];
|
||||
return nextNodeId;
|
||||
}
|
||||
|
||||
export function newBlock<Type>(type: BlockType, parentId: string, data: BlockData<Type>): NestedBlock<Type> {
|
||||
return {
|
||||
id: generateId(),
|
||||
type,
|
||||
parent: parentId,
|
||||
children: generateId(),
|
||||
data,
|
||||
};
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { Editor } from 'slate';
|
||||
import { getAfterRangeAt, getBeforeRangeAt } from '$app/utils/document/slate/text';
|
||||
import { BlockType, HeadingBlockData, NestedBlock } from '$app/interfaces/document';
|
||||
import { getDeltaFromSlateNodes, newBlock } from '$app/utils/document/blocks/common';
|
||||
|
||||
export function newHeadingBlock(parentId: string, data: HeadingBlockData): NestedBlock {
|
||||
return newBlock<BlockType.HeadingBlock>(BlockType.HeadingBlock, parentId, data);
|
||||
}
|
||||
|
||||
export function getHeadingDataFromEditor(editor: Editor): HeadingBlockData | undefined {
|
||||
const selection = editor.selection;
|
||||
if (!selection) return;
|
||||
const hashTags = Editor.string(editor, getBeforeRangeAt(editor, selection));
|
||||
const level = hashTags.match(/#/g)?.length;
|
||||
if (!level) return;
|
||||
const slateNodes = Editor.fragment(editor, getAfterRangeAt(editor, selection));
|
||||
const delta = getDeltaFromSlateNodes(slateNodes);
|
||||
return {
|
||||
level,
|
||||
delta,
|
||||
};
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { BlockType, NestedBlock, TextBlockData } from '$app/interfaces/document';
|
||||
import { newBlock } from '$app/utils/document/blocks/common';
|
||||
|
||||
export function newTextBlock(parentId: string, data: TextBlockData): NestedBlock {
|
||||
return newBlock<BlockType.TextBlock>(BlockType.TextBlock, parentId, data);
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
import { toggleFormat } from './format';
|
||||
import { Editor, Range } from 'slate';
|
||||
import { getBeforeRangeAt, getDelta, getAfterRangeAt, pointInEnd, pointInBegin, clonePoint } from './text';
|
||||
import { clonePoint, getAfterRangeAt, getBeforeRangeAt, getDelta, pointInBegin, pointInEnd } from './text';
|
||||
import { SelectionPoint, TextDelta, TextSelection } from '$app/interfaces/document';
|
||||
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
|
||||
|
||||
const HOTKEYS: Record<string, string> = {
|
||||
'mod+b': 'bold',
|
||||
@ -13,16 +14,6 @@ const HOTKEYS: Record<string, string> = {
|
||||
'mod+shift+S': 'strikethrough',
|
||||
};
|
||||
|
||||
export const keyBoardEventKeyMap = {
|
||||
Enter: 'Enter',
|
||||
Backspace: 'Backspace',
|
||||
Tab: 'Tab',
|
||||
Up: 'ArrowUp',
|
||||
Down: 'ArrowDown',
|
||||
Left: 'ArrowLeft',
|
||||
Right: 'ArrowRight',
|
||||
};
|
||||
|
||||
export function triggerHotkey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
||||
for (const hotkey in HOTKEYS) {
|
||||
if (isHotkey(hotkey, event)) {
|
@ -0,0 +1,18 @@
|
||||
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
|
||||
import { getBeforeRangeAt } from '$app/utils/document/slate/text';
|
||||
import { Editor } from 'slate';
|
||||
|
||||
export function canHandleToHeadingBlock(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor): boolean {
|
||||
const isSpaceKey = event.key === keyBoardEventKeyMap.Space;
|
||||
const selection = editor.selection;
|
||||
|
||||
if (!isSpaceKey || !selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const beforeSpaceContent = Editor.string(editor, getBeforeRangeAt(editor, selection));
|
||||
|
||||
const isHeadingMarkdown = /^(#{1,3})$/.test(beforeSpaceContent.trim());
|
||||
|
||||
return isHeadingMarkdown;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { DeltaTypePB } from '@/services/backend/models/flowy-document2';
|
||||
import { BlockType, NestedBlock, DocumentState, ChangeType, BlockPBValue } from '../interfaces/document';
|
||||
import { Log } from './log';
|
||||
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '../constants/block';
|
||||
import { BlockType, NestedBlock, DocumentState, ChangeType, BlockPBValue } from '../../interfaces/document';
|
||||
import { Log } from '../log';
|
||||
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '../../constants/document/block';
|
||||
|
||||
// This is a list of all the possible changes that can happen to document data
|
||||
const matchCases = [
|
||||
@ -153,12 +153,14 @@ function onMatchChildrenDelete(state: DocumentState, id: string, _children: stri
|
||||
* @param value
|
||||
*/
|
||||
export function blockChangeValue2Node(value: BlockPBValue): NestedBlock {
|
||||
const block = {
|
||||
const block: NestedBlock = {
|
||||
id: value.id,
|
||||
type: value.ty as BlockType,
|
||||
parent: value.parent,
|
||||
children: value.children,
|
||||
data: {},
|
||||
data: {
|
||||
delta: [],
|
||||
},
|
||||
};
|
||||
if ('data' in value && typeof value.data === 'string') {
|
||||
try {
|
@ -19,7 +19,8 @@
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"$app/*": ["src/appflowy_app/*"]
|
||||
"$app/*": ["src/appflowy_app/*"],
|
||||
"$app_reducers/*": ["src/appflowy_app/stores/reducers/*"],
|
||||
},
|
||||
},
|
||||
"include": ["src", "vite.config.ts", "../app_flowy/assets/translations"],
|
||||
|
@ -27,7 +27,8 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: '@/', replacement: `${__dirname}/src/` },
|
||||
{ find: '$app/', replacement: `${__dirname}/src/appflowy_app/` }
|
||||
{ find: '$app/', replacement: `${__dirname}/src/appflowy_app/` },
|
||||
{ find: '$app_reducers/', replacement: `${__dirname}/src/appflowy_app/stores/reducers/` },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user