- {isDragging ?
: null}
+
+
);
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx
index 589c5e8cb9..7142ec8baf 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx
@@ -5,13 +5,17 @@ import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded';
import Portal from '../BlockPortal';
import { IconButton } from '@mui/material';
import BlockMenu from '../BlockMenu';
+import { useAppSelector } from '$app/stores/store';
const sx = { height: 24, width: 24 };
export default function BlockSideToolbar(props: { container: HTMLDivElement }) {
const { nodeId, style, ref, menuOpen, handleToggleMenu } = useBlockSideToolbar(props);
+ const isDragging = useAppSelector(
+ (state) => state.documentRangeSelection.isDragging || state.documentRectSelection.isDragging
+ );
- if (!nodeId) return null;
+ if (!nodeId || isDragging) return null;
return (
<>
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/CalloutBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/CalloutBlock/index.tsx
index feaa568e35..7d37b05e21 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/CalloutBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/CalloutBlock/index.tsx
@@ -1,10 +1,11 @@
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 { IconButton } from '@mui/material';
import emojiData from '@emoji-mart/data';
import Picker from '@emoji-mart/react';
import { useCalloutBlock } from '$app/components/document/CalloutBlock/CalloutBlock.hooks';
+import Popover from '@mui/material/Popover';
export default function CalloutBlock({
node,
@@ -17,7 +18,7 @@ export default function CalloutBlock({
return (
-
+
e.stopPropagation()}>
{
newChildren = {newChildren};
}
+ const className = [
+ 'token',
+ leaf.prism_token && leaf.prism_token,
+ leaf.strikethrough && 'line-through',
+ leaf.selectionHighlighted && 'bg-main-secondary',
+ ].filter(Boolean);
+
return (
-
+
{newChildren}
);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx
index f94d2f3873..fb89a5c9f3 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx
@@ -1,7 +1,6 @@
import { BlockType, NestedBlock } from '$app/interfaces/document';
import { useCodeBlock } from './CodeBlock.hooks';
import { Editable, Slate } from 'slate-react';
-import BlockHorizontalToolbar from '$app/components/document/BlockHorizontalToolbar';
import React from 'react';
import { CodeLeaf, CodeBlockElement } from './elements';
import SelectLanguage from './SelectLanguage';
@@ -23,10 +22,13 @@ export default function CodeBlock({
-
decorateCodeFunc(entry, language)}
+ decorate={(entry) => {
+ const codeRange = decorateCodeFunc(entry, language);
+ const range = rest.decorate(entry);
+ return [...range, ...codeRange];
+ }}
renderLeaf={CodeLeaf}
renderElement={CodeBlockElement}
placeholder={placeholder || 'Please enter some text...'}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx
index 304c09027b..d1a08c7071 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Overlay/index.tsx
@@ -1,13 +1,14 @@
-import React, { useState } from 'react';
+import React from 'react';
import BlockSideToolbar from '../BlockSideToolbar';
import BlockSelection from '../BlockSelection';
+import TextActionMenu from '$app/components/document/TextActionMenu';
export default function Overlay({ container }: { container: HTMLDivElement }) {
- const [isDragging, setDragging] = useState(false);
return (
<>
- {isDragging ? null : }
-
+
+
+
>
);
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
index 8069b0e1a4..a0795df350 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
@@ -19,18 +19,11 @@ function Root({ documentData }: { documentData: DocumentData }) {
}
return (
- {
- // prevent backspace from going back
- if (e.key === 'Backspace') {
- e.stopPropagation();
- }
- }}
- >
-
-
+ <>
+
+
+
+ >
);
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/index.hooks.ts
new file mode 100644
index 0000000000..821d03e893
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/index.hooks.ts
@@ -0,0 +1,43 @@
+import { useEffect, useRef, useState } from 'react';
+import { calcToolbarPosition } from '$app/utils/document/toolbar';
+import { useAppSelector } from '$app/stores/store';
+
+export function useMenuStyle(container: HTMLDivElement) {
+ const ref = useRef(null);
+ const range = useAppSelector((state) => state.documentRangeSelection);
+
+ const [scrollTop, setScrollTop] = useState(container.scrollTop);
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+
+ const id = range.focus?.id;
+ if (!id) return;
+
+ const position = calcToolbarPosition(el);
+
+ if (!position) {
+ el.style.opacity = '0';
+ el.style.pointerEvents = 'none';
+ } else {
+ el.style.opacity = '1';
+ el.style.pointerEvents = 'auto';
+ el.style.top = position.top;
+ el.style.left = position.left;
+ }
+ });
+
+ useEffect(() => {
+ const handleScroll = () => {
+ setScrollTop(container.scrollTop);
+ };
+ container.addEventListener('scroll', handleScroll);
+ return () => {
+ container.removeEventListener('scroll', handleScroll);
+ };
+ }, [container]);
+
+ return {
+ ref,
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/index.tsx
new file mode 100644
index 0000000000..3625e53c5a
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/index.tsx
@@ -0,0 +1,46 @@
+import { useMenuStyle } from './index.hooks';
+import { useAppSelector } from '$app/stores/store';
+import { isEqual } from '$app/utils/tool';
+import TextActionMenuList from '$app/components/document/TextActionMenu/menu';
+
+const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
+ const { ref } = useMenuStyle(container);
+
+ return (
+ {
+ // prevent toolbar from taking focus away from editor
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+
+
+ );
+};
+const TextActionMenu = ({ container }: { container: HTMLDivElement }) => {
+ const canShow = useAppSelector((state) => {
+ const range = state.documentRangeSelection;
+ if (range.isDragging) return false;
+ const anchorNode = range.anchor;
+ const focusNode = range.focus;
+ if (!anchorNode || !focusNode) return false;
+ const isSameLine = anchorNode.id === focusNode.id;
+ const isCollapsed = isEqual(anchorNode.selection.anchor, anchorNode.selection.focus);
+ return !(isSameLine && isCollapsed);
+ });
+ if (!canShow) return null;
+
+ return (
+
+
+
+ );
+};
+
+export default TextActionMenu;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatButton.tsx
new file mode 100644
index 0000000000..5fc5127c9c
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatButton.tsx
@@ -0,0 +1,68 @@
+import IconButton from '@mui/material/IconButton';
+import FormatIcon from './FormatIcon';
+import React, { useCallback, useEffect, useMemo, useContext } from 'react';
+import { TextAction } from '$app/interfaces/document';
+import MenuTooltip from '$app/components/document/TextActionMenu/menu/MenuTooltip';
+import { getFormatActiveThunk, toggleFormatThunk } from '$app_reducers/document/async-actions/format';
+import { useAppDispatch, useAppSelector } from '$app/stores/store';
+import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
+import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
+
+const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) => {
+ const dispatch = useAppDispatch();
+ const controller = useContext(DocumentControllerContext);
+
+ const focusId = useAppSelector((state) => state.documentRangeSelection.focus?.id || '');
+ const { node: focusNode } = useSubscribeNode(focusId);
+
+ const [isActive, setIsActive] = React.useState(false);
+ const color = useMemo(() => (isActive ? '#00BCF0' : 'white'), [isActive]);
+
+ const formatTooltips: Record = useMemo(
+ () => ({
+ [TextAction.Bold]: 'Bold',
+ [TextAction.Italic]: 'Italic',
+ [TextAction.Underline]: 'Underline',
+ [TextAction.Strikethrough]: 'Strike through',
+ [TextAction.Code]: 'Mark as Code',
+ }),
+ []
+ );
+
+ const isFormatActive = useCallback(async () => {
+ if (!focusNode) return false;
+ const { payload: isActive } = await dispatch(getFormatActiveThunk(format));
+ return !!isActive;
+ }, [dispatch, format, focusNode]);
+
+ const toggleFormat = useCallback(
+ async (format: TextAction) => {
+ if (!controller) return;
+ await dispatch(
+ toggleFormatThunk({
+ format,
+ controller,
+ isActive,
+ })
+ );
+ },
+ [controller, dispatch, isActive]
+ );
+
+ useEffect(() => {
+ void (async () => {
+ const isActive = await isFormatActive();
+ setIsActive(isActive);
+ })();
+ }, [isFormatActive]);
+
+ return (
+
+ toggleFormat(format)}>
+
+
+
+ );
+};
+
+export default FormatButton;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockHorizontalToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatIcon.tsx
similarity index 91%
rename from frontend/appflowy_tauri/src/appflowy_app/components/document/BlockHorizontalToolbar/FormatIcon.tsx
rename to frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatIcon.tsx
index 39aeafebac..c3fa3bb69e 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockHorizontalToolbar/FormatIcon.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatIcon.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material';
-import { iconSize } from '$app/constants/document/toolbar';
+export const iconSize = { width: 18, height: 18 };
export default function FormatIcon({ icon }: { icon: string }) {
switch (icon) {
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/MenuTooltip.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/MenuTooltip.tsx
new file mode 100644
index 0000000000..73d7251010
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/MenuTooltip.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import Tooltip from '@mui/material/Tooltip';
+
+function MenuTooltip({ title, children }: { children: JSX.Element; title?: string }) {
+ return (
+
+ {title}
+
+ }
+ placement='top-start'
+ >
+
{children}
+
+ );
+}
+
+export default MenuTooltip;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/TurnIntoSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/TurnIntoSelect.tsx
new file mode 100644
index 0000000000..c608a490e6
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/TurnIntoSelect.tsx
@@ -0,0 +1,50 @@
+import React, { useCallback } from 'react';
+import TurnIntoPopover from '$app/components/document/_shared/TurnInto';
+import Button from '@mui/material/Button';
+import ArrowDropDown from '@mui/icons-material/ArrowDropDown';
+import MenuTooltip from './MenuTooltip';
+import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
+
+function TurnIntoSelect({ id }: { id: string }) {
+ const [anchorEl, setAnchorEl] = React.useState
(null);
+
+ const { node } = useSubscribeNode(id);
+ const handleClick = useCallback((event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ }, []);
+
+ const handleClose = useCallback(() => {
+ setAnchorEl(null);
+ }, []);
+
+ const open = Boolean(anchorEl);
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default TurnIntoSelect;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/index.hooks.ts
new file mode 100644
index 0000000000..d5c52fdfd2
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/index.hooks.ts
@@ -0,0 +1,47 @@
+import { useAppSelector } from '$app/stores/store';
+import { useMemo } from 'react';
+import {
+ blockConfig,
+ defaultTextActionProps,
+ multiLineTextActionGroups,
+ multiLineTextActionProps,
+ textActionGroups,
+} from '$app/constants/document/config';
+import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
+import { TextAction } from '$app/interfaces/document';
+
+export function useTextActionMenu() {
+ const range = useAppSelector((state) => state.documentRangeSelection);
+
+ const id = useMemo(() => {
+ return range.anchor?.id === range.focus?.id ? range.anchor?.id : undefined;
+ }, [range]);
+
+ const { node } = useSubscribeNode(id || '');
+
+ const items = useMemo(() => {
+ if (node) {
+ const config = blockConfig[node.type];
+ const { customItems, excludeItems } = {
+ ...defaultTextActionProps,
+ ...config.textActionMenuProps,
+ };
+ return customItems?.filter((item) => !excludeItems?.includes(item)) || [];
+ } else {
+ return multiLineTextActionProps.customItems || [];
+ }
+ }, [node]);
+
+ // the groups have default items, so we need to filter the items if this node has excluded items
+ const groupItems: TextAction[][] = useMemo(() => {
+ const groups = node ? textActionGroups : multiLineTextActionGroups;
+ return groups.map((group) => {
+ return group.filter((item) => items.includes(item));
+ });
+ }, [JSON.stringify(items), node]);
+
+ return {
+ groupItems,
+ id,
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/index.tsx
new file mode 100644
index 0000000000..4184f99260
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/index.tsx
@@ -0,0 +1,39 @@
+import { TextAction } from '$app/interfaces/document';
+import React, { useCallback } from 'react';
+import TurnIntoSelect from '$app/components/document/TextActionMenu/menu/TurnIntoSelect';
+import FormatButton from '$app/components/document/TextActionMenu/menu/FormatButton';
+import { useTextActionMenu } from '$app/components/document/TextActionMenu/menu/index.hooks';
+
+function TextActionMenuList() {
+ const { groupItems, id } = useTextActionMenu();
+ const renderNode = useCallback((action: TextAction, id?: string) => {
+ switch (action) {
+ case TextAction.Turn:
+ return id ? : null;
+ case TextAction.Bold:
+ case TextAction.Italic:
+ case TextAction.Underline:
+ case TextAction.Strikethrough:
+ case TextAction.Code:
+ return ;
+ default:
+ return null;
+ }
+ }, []);
+
+ return (
+
+ {groupItems.map((group, i: number) => (
+
+ {group.map((item) => (
+
+ {renderNode(item, id)}
+
+ ))}
+
+ ))}
+
+ );
+}
+
+export default TextActionMenuList;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx
index 60aa7ecb77..06416564ee 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx
@@ -10,20 +10,12 @@ interface LeafProps extends RenderLeafProps {
selectionHighlighted?: boolean;
};
}
-const Leaf = ({
- attributes,
- children,
- leaf,
-}: LeafProps) => {
+const Leaf = ({ attributes, children, leaf }: LeafProps) => {
let newChildren = children;
if (leaf.bold) {
newChildren = {children};
}
- if (leaf.code) {
- newChildren = {newChildren};
- }
-
if (leaf.italic) {
newChildren = {newChildren};
}
@@ -32,16 +24,14 @@ const Leaf = ({
newChildren = {newChildren};
}
- let className = "";
- if (leaf.strikethrough) {
- className += "line-through";
- }
- if (leaf.selectionHighlighted) {
- className += " bg-main-secondary";
- }
+ const className = [
+ leaf.strikethrough && 'line-through',
+ leaf.selectionHighlighted && 'bg-main-secondary',
+ leaf.code && 'bg-main-selector',
+ ].filter(Boolean);
return (
-
+
{newChildren}
);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/events/Events.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/events/Events.hooks.ts
index 1b6ea72606..bf592938d6 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/events/Events.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/events/Events.hooks.ts
@@ -2,20 +2,23 @@ import { Editor } from 'slate';
import { useTurnIntoBlock } from './TurnIntoEvents.hooks';
import { useCallback, useContext, useMemo } from 'react';
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
-import { triggerHotkey } from '$app/utils/document/blocks/text/hotkey';
import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document';
import isHotkey from 'is-hotkey';
import { indentNodeThunk, outdentNodeThunk, splitNodeThunk } from '$app_reducers/document/async-actions';
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
-import { useAppDispatch } from '$app/stores/store';
+import { useAppDispatch, useAppSelector } from '$app/stores/store';
import { useDefaultTextInputEvents } from '$app/components/document/_shared/Text/TextEvents.hooks';
import { ReactEditor } from 'slate-react';
export function useTextBlockKeyEvent(id: string, editor: ReactEditor) {
const controller = useContext(DocumentControllerContext);
const dispatch = useAppDispatch();
-
const defaultTextInputEvents = useDefaultTextInputEvents(id);
+ const isFocusCurrentNode = useAppSelector((state) => {
+ const { anchor, focus } = state.documentRangeSelection;
+ if (!anchor || !focus) return false;
+ return anchor.id === id && focus.id === id;
+ });
const { turnIntoBlockEvents } = useTurnIntoBlock(id);
@@ -84,18 +87,20 @@ export function useTextBlockKeyEvent(id: string, editor: ReactEditor) {
const onKeyDown = useCallback(
(event: React.KeyboardEvent) => {
+ if (!isFocusCurrentNode) {
+ event.preventDefault();
+ return;
+ }
+
+ event.stopPropagation();
// This is list of key events that can be handled by TextBlock
const keyEvents = [...events, ...turnIntoBlockEvents];
const matchKeys = keyEvents.filter((keyEvent) => keyEvent.canHandle(event, editor));
- if (matchKeys.length === 0) {
- triggerHotkey(event, editor);
- return;
- }
matchKeys.forEach((matchKey) => matchKey.handler(event, editor));
},
- [editor, events, turnIntoBlockEvents]
+ [editor, events, turnIntoBlockEvents, isFocusCurrentNode]
);
return {
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 880e7f629f..275a79f94e 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
@@ -1,8 +1,7 @@
import { Slate, Editable } from 'slate-react';
import Leaf from './Leaf';
import { useTextBlock } from './TextBlock.hooks';
-import BlockHorizontalToolbar from '../BlockHorizontalToolbar';
-import React, { useEffect } from 'react';
+import React from 'react';
import { NestedBlock } from '$app/interfaces/document';
import NodeChildren from '$app/components/document/Node/NodeChildren';
@@ -23,7 +22,6 @@ function TextBlock({
<>
- {/**/}
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
index c73b1315c9..e159b73b11 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
@@ -2,7 +2,7 @@ import { useAppSelector } from '@/appflowy_app/stores/store';
import { useMemo, useRef } from 'react';
import { DocumentState, Node, RangeSelectionState } from '$app/interfaces/document';
import { nodeInRange } from '$app/utils/document/blocks/common';
-import { getNodeEndSelection, selectionIsForward } from '$app/utils/document/blocks/text/delta';
+import { getNodeEndSelection } from '$app/utils/document/blocks/text/delta';
/**
* Subscribe node information
@@ -18,7 +18,7 @@ export function useSubscribeNode(id: string) {
});
const isSelected = useAppSelector((state) => {
- return state.documentRectSelection.includes(id) || false;
+ return state.documentRectSelection.selection.includes(id) || false;
});
// Memoize the node and its children
@@ -50,6 +50,7 @@ export function useSubscribeRangeSelection(id: string) {
if (range.focus?.id === id) {
return range.focus.selection;
}
+
return getAmendInRangeNodeSelection(id, range, state.document);
});
@@ -60,17 +61,17 @@ export function useSubscribeRangeSelection(id: string) {
}
function getAmendInRangeNodeSelection(id: string, range: RangeSelectionState, document: DocumentState) {
- if (!range.anchor || !range.focus || range.anchor.id === range.focus.id) {
+ if (!range.anchor || !range.focus || range.anchor.id === range.focus.id || range.isForward === undefined) {
return null;
}
- const isForward = selectionIsForward(range.anchor.selection);
+
const isNodeInRange = nodeInRange(
id,
{
startId: range.anchor.id,
endId: range.focus.id,
},
- isForward,
+ range.isForward,
document
);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextEvents.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextEvents.hooks.ts
index d4af6150e2..f6d878a557 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextEvents.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextEvents.hooks.ts
@@ -11,7 +11,7 @@ import {
canHandleUpKey,
} from '$app/utils/document/blocks/text/hotkey';
import { TextBlockKeyEventHandlerParams } from '$app/interfaces/document';
-import { ReactEditor } from "slate-react";
+import { ReactEditor } from 'slate-react';
export function useDefaultTextInputEvents(id: string) {
const dispatch = useAppDispatch();
@@ -81,11 +81,11 @@ export function useDefaultTextInputEvents(id: string) {
triggerEventKey: keyBoardEventKeyMap.Backspace,
canHandle: canHandleBackspaceKey,
handler: (...args: TextBlockKeyEventHandlerParams) => {
- const [e, _] = args;
+ const [e, editor] = args;
e.preventDefault();
void (async () => {
if (!controller) return;
- await dispatch(backspaceNodeThunk({ id, controller }));
+ await dispatch(backspaceNodeThunk({ id, controller, editor }));
})();
},
},
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextInput.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextInput.hooks.ts
index ae41fe8bdb..635976f621 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextInput.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextInput.hooks.ts
@@ -1,4 +1,4 @@
-import { createEditor, Descendant, Editor } from 'slate';
+import { createEditor, Descendant, Editor, Transforms } from 'slate';
import { withReact } from 'slate-react';
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
@@ -8,12 +8,12 @@ import { useAppDispatch } from '$app/stores/store';
import { updateNodeDeltaThunk } from '$app_reducers/document/async-actions/blocks/text/update';
import { deltaToSlateValue, slateValueToDelta } from '$app/utils/document/blocks/common';
import { isSameDelta } from '$app/utils/document/blocks/text/delta';
-import { debounce } from '$app/utils/tool';
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
import { useTextSelections } from '$app/components/document/_shared/Text/TextSelection.hooks';
export function useTextInput(id: string) {
const { node } = useSubscribeNode(id);
+
const [editor] = useState(() => withReact(createEditor()));
const isComposition = useRef(false);
const { setLastActiveSelection, ...selectionProps } = useTextSelections(id, editor);
@@ -24,16 +24,15 @@ export function useTextInput(id: string) {
}
return node.data.delta;
}, [node]);
+ const [value, setValue] = useState(deltaToSlateValue(delta));
const { sync, receive } = useUpdateDelta(id, editor);
- const [value, setValue] = useState(deltaToSlateValue(delta));
-
// Update the editor's value when the node's delta changes.
useEffect(() => {
// If composition is in progress, do nothing.
if (isComposition.current) return;
- receive(delta);
+ receive(delta, setValue);
}, [delta, receive]);
// Update the node's delta when the editor's value changes.
@@ -88,33 +87,30 @@ function useUpdateDelta(id: string, editor: Editor) {
const dispatch = useAppDispatch();
const penddingRef = useRef(false);
- // when user input, update the node's delta after 200ms
- const debounceUpdate = useMemo(() => {
- return debounce(() => {
- if (!controller) return;
- const delta = slateValueToDelta(editor.children);
- void (async () => {
- await dispatch(
- updateNodeDeltaThunk({
- id,
- delta,
- controller,
- })
- );
- // reset pendding flag
- penddingRef.current = false;
- })();
- }, 200);
+ const update = useCallback(() => {
+ if (!controller) return;
+ const delta = slateValueToDelta(editor.children);
+ void (async () => {
+ await dispatch(
+ updateNodeDeltaThunk({
+ id,
+ delta,
+ controller,
+ })
+ );
+ // reset pendding flag
+ penddingRef.current = false;
+ })();
}, [controller, dispatch, editor, id]);
const sync = useCallback(() => {
// set pendding flag
penddingRef.current = true;
- debounceUpdate();
- }, [debounceUpdate]);
+ update();
+ }, [update]);
const receive = useCallback(
- (delta: TextDelta[]) => {
+ (delta: TextDelta[], setValue: (children: Descendant[]) => void) => {
// if pendding, do nothing
if (penddingRef.current) return;
@@ -123,18 +119,14 @@ function useUpdateDelta(id: string, editor: Editor) {
const isSame = isSameDelta(delta, localDelta);
if (isSame) return;
+ Transforms.deselect(editor);
const slateValue = deltaToSlateValue(delta);
editor.children = slateValue;
+ setValue(slateValue);
},
[editor]
);
- useEffect(() => {
- return () => {
- debounceUpdate.cancel();
- };
- });
-
return {
sync,
receive,
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextSelection.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextSelection.hooks.ts
index b98e891747..234bcf4837 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextSelection.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Text/TextSelection.hooks.ts
@@ -1,14 +1,14 @@
-import { MouseEventHandler, useCallback, useEffect } from 'react';
+import { MouseEvent, useCallback, useEffect, useRef } from 'react';
import { BaseRange, Editor, Node, Path, Range, Transforms } from 'slate';
import { EditableProps } from 'slate-react/dist/components/editable';
import { useSubscribeRangeSelection } from '$app/components/document/_shared/SubscribeNode.hooks';
import { useAppDispatch } from '$app/stores/store';
-import { rangeSelectionActions } from '$app_reducers/document/slice';
import { TextSelection } from '$app/interfaces/document';
import { ReactEditor } from 'slate-react';
import { syncRangeSelectionThunk } from '$app_reducers/document/async-actions/range_selection';
-import { getCollapsedRange } from '$app/utils/document/blocks/common';
-import { getEditorEndPoint, selectionIsForward } from '$app/utils/document/blocks/text/delta';
+import { getNodeEndSelection } from '$app/utils/document/blocks/text/delta';
+import { slateValueToDelta } from '$app/utils/document/blocks/common';
+import { isEqual } from '$app/utils/tool';
export function useTextSelections(id: string, editor: ReactEditor) {
const { rangeRef, currentSelection } = useSubscribeRangeSelection(id);
@@ -16,15 +16,21 @@ export function useTextSelections(id: string, editor: ReactEditor) {
useEffect(() => {
if (!rangeRef.current) return;
- const { isDragging, focus, anchor } = rangeRef.current;
- if (isDragging || anchor?.id !== focus?.id || !currentSelection || !Range.isCollapsed(currentSelection as BaseRange))
+ if (!currentSelection) {
+ ReactEditor.deselect(editor);
+ ReactEditor.blur(editor);
return;
+ }
+ const { isDragging, focus } = rangeRef.current;
+ if (isDragging || focus?.id !== id) return;
if (!ReactEditor.isFocused(editor)) {
ReactEditor.focus(editor);
}
- Transforms.select(editor, currentSelection);
- }, [currentSelection, editor, rangeRef]);
+ if (!isEqual(editor.selection, currentSelection)) {
+ Transforms.select(editor, currentSelection);
+ }
+ }, [currentSelection, editor, id, rangeRef]);
const decorate: EditableProps['decorate'] = useCallback(
(entry: [Node, Path]) => {
@@ -48,48 +54,6 @@ export function useTextSelections(id: string, editor: ReactEditor) {
[editor, currentSelection]
);
- const onMouseDown: MouseEventHandler = useCallback(
- (e) => {
- const range = getCollapsedRange(id, editor.selection as TextSelection);
- dispatch(
- rangeSelectionActions.setRange({
- ...range,
- isDragging: true,
- })
- );
- },
- [dispatch, editor, id]
- );
-
- const onMouseMove: MouseEventHandler = useCallback(
- (e) => {
- if (!rangeRef.current) return;
- const { isDragging, anchor } = rangeRef.current;
- if (!isDragging || !anchor || ReactEditor.isFocused(editor)) return;
-
- const isForward = selectionIsForward(anchor.selection);
- if (!isForward) {
- Transforms.select(editor, getEditorEndPoint(editor));
- }
- ReactEditor.focus(editor);
- },
- [editor, rangeRef]
- );
-
- const onMouseUp: MouseEventHandler = useCallback(
- (e) => {
- if (!rangeRef.current) return;
- const { isDragging } = rangeRef.current;
- if (!isDragging) return;
- dispatch(
- rangeSelectionActions.setRange({
- isDragging: false,
- })
- );
- },
- [dispatch, rangeRef]
- );
-
const setLastActiveSelection = useCallback(
(lastActiveSelection: Range) => {
const selection = lastActiveSelection as TextSelection;
@@ -102,12 +66,33 @@ export function useTextSelections(id: string, editor: ReactEditor) {
ReactEditor.deselect(editor);
}, [editor]);
+ const onMouseMove = useCallback(
+ (e: MouseEvent) => {
+ if (!rangeRef.current) return;
+ const { isDragging, isForward, anchor } = rangeRef.current;
+ if (!isDragging || !anchor) return;
+ if (ReactEditor.isFocused(editor)) {
+ return;
+ }
+
+ if (anchor.id === id) {
+ Transforms.select(editor, anchor.selection);
+ } else if (!isForward) {
+ const endSelection = getNodeEndSelection(slateValueToDelta(editor.children));
+ Transforms.select(editor, {
+ anchor: endSelection.anchor,
+ focus: editor.selection?.focus || endSelection.focus,
+ });
+ }
+ ReactEditor.focus(editor);
+ },
+ [editor, id, rangeRef]
+ );
+
return {
decorate,
- onMouseDown,
- onMouseMove,
- onMouseUp,
onBlur,
+ onMouseMove,
setLastActiveSelection,
};
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/TurnInto.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/TurnInto.hooks.ts
new file mode 100644
index 0000000000..7087cb9a73
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/TurnInto.hooks.ts
@@ -0,0 +1,49 @@
+import { useAppDispatch } from '$app/stores/store';
+import { useCallback, useContext } from 'react';
+import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
+import { BlockData, BlockType, NestedBlock } from '$app/interfaces/document';
+import { blockConfig } from '$app/constants/document/config';
+import { turnToBlockThunk } from '$app_reducers/document/async-actions';
+
+export function useTurnInto({ node, onClose }: { node: NestedBlock; onClose?: () => void }) {
+ const dispatch = useAppDispatch();
+
+ const controller = useContext(DocumentControllerContext);
+
+ const turnIntoBlock = useCallback(
+ async (type: BlockType, isSelected: boolean, data?: BlockData) => {
+ if (!controller || isSelected) {
+ onClose?.();
+ return;
+ }
+
+ const config = blockConfig[type];
+ await dispatch(
+ turnToBlockThunk({
+ id: node.id,
+ controller,
+ type,
+ data: {
+ ...config.defaultData,
+ delta: node?.data?.delta || [],
+ ...data,
+ },
+ })
+ );
+ onClose?.();
+ },
+ [onClose, controller, dispatch, node]
+ );
+
+ const turnIntoHeading = useCallback(
+ (level: number, isSelected: boolean) => {
+ turnIntoBlock(BlockType.HeadingBlock, isSelected, { level });
+ },
+ [turnIntoBlock]
+ );
+
+ return {
+ turnIntoBlock,
+ turnIntoHeading,
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/index.tsx
new file mode 100644
index 0000000000..11af496572
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/index.tsx
@@ -0,0 +1,138 @@
+import React, { useMemo } from 'react';
+import { BlockType } from '$app/interfaces/document';
+
+import {
+ ArrowRight,
+ Check,
+ DataObject,
+ FormatListBulleted,
+ FormatListNumbered,
+ FormatQuote,
+ Lightbulb,
+ TextFields,
+ Title,
+ Functions,
+} from '@mui/icons-material';
+import Popover, { PopoverProps } from '@mui/material/Popover';
+import { ListItemIcon, ListItemText, MenuItem } from '@mui/material';
+import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
+import { useTurnInto } from '$app/components/document/_shared/TurnInto/TurnInto.hooks';
+
+const TurnIntoPopover = ({
+ id,
+ onClose,
+ ...props
+}: {
+ id: string;
+ onClose?: () => void;
+} & PopoverProps) => {
+ const { node } = useSubscribeNode(id);
+ const { turnIntoHeading, turnIntoBlock } = useTurnInto({ node, onClose });
+
+ const options: {
+ type: BlockType;
+ title: string;
+ icon: React.ReactNode;
+ selected?: boolean;
+ onClick?: (type: BlockType, isSelected: boolean) => void;
+ }[] = useMemo(
+ () => [
+ {
+ type: BlockType.TextBlock,
+ title: 'Text',
+ icon: ,
+ },
+ {
+ type: BlockType.HeadingBlock,
+ title: 'Heading 1',
+ icon: ,
+ selected: node?.data?.level === 1,
+ onClick: (type: BlockType, isSelected: boolean) => {
+ turnIntoHeading(1, isSelected);
+ },
+ },
+ {
+ type: BlockType.HeadingBlock,
+ title: 'Heading 2',
+ icon: ,
+ selected: node?.data?.level === 2,
+ onClick: (type: BlockType, isSelected: boolean) => {
+ turnIntoHeading(2, isSelected);
+ },
+ },
+ {
+ type: BlockType.HeadingBlock,
+ title: 'Heading 3',
+ icon: ,
+ selected: node?.data?.level === 3,
+ onClick: (type: BlockType, isSelected: boolean) => {
+ turnIntoHeading(3, isSelected);
+ },
+ },
+ {
+ type: BlockType.TodoListBlock,
+ title: 'To-do list',
+ icon: ,
+ },
+ {
+ type: BlockType.BulletedListBlock,
+ title: 'Bulleted list',
+ icon: ,
+ },
+ {
+ type: BlockType.NumberedListBlock,
+ title: 'Numbered list',
+ icon: ,
+ },
+ {
+ type: BlockType.ToggleListBlock,
+ title: 'Toggle list',
+ icon: ,
+ },
+ {
+ type: BlockType.CodeBlock,
+ title: 'Code',
+ icon: ,
+ },
+ {
+ type: BlockType.QuoteBlock,
+ title: 'Quote',
+ icon: ,
+ },
+ {
+ type: BlockType.CalloutBlock,
+ title: 'Callout',
+ icon: ,
+ },
+ // {
+ // type: BlockType.EquationBlock,
+ // title: 'Block Equation',
+ // icon: ,
+ // },
+ ],
+ [node?.data?.level, turnIntoHeading]
+ );
+
+ return (
+
+ {options.map((option) => {
+ const isSelected = option.type === node.type && option.selected !== false;
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export default TurnIntoPopover;
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 ff141e22ef..2601d14ccd 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts
@@ -1,44 +1,9 @@
-import { BlockData, BlockType } from '$app/interfaces/document';
+import { BlockConfig, BlockType, SplitRelationship, TextAction, TextActionMenuProps } 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
*/
-export const blockConfig: Record<
- string,
- {
- /**
- * Whether the block can have children
- */
- canAddChild: boolean;
- /**
- * 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;
- };
- }
-> = {
+export const blockConfig: Record = {
[BlockType.TextBlock]: {
canAddChild: true,
defaultData: {
@@ -169,5 +134,49 @@ export const blockConfig: Record<
* ```
*/
markdownRegexps: [/^(```)$/],
+
+ textActionMenuProps: {
+ excludeItems: [TextAction.Code],
+ },
},
};
+
+export const defaultTextActionProps: TextActionMenuProps = {
+ customItems: [
+ TextAction.Turn,
+ TextAction.Bold,
+ TextAction.Italic,
+ TextAction.Underline,
+ TextAction.Strikethrough,
+ TextAction.Code,
+ TextAction.Equation,
+ ],
+ excludeItems: [],
+};
+
+export const multiLineTextActionProps: TextActionMenuProps = {
+ customItems: [TextAction.Bold, TextAction.Italic, TextAction.Underline, TextAction.Strikethrough, TextAction.Code],
+};
+
+export const multiLineTextActionGroups = [
+ [
+ TextAction.Bold,
+ TextAction.Italic,
+ TextAction.Underline,
+ TextAction.Strikethrough,
+ TextAction.Code,
+ TextAction.Equation,
+ ],
+];
+
+export const textActionGroups = [
+ [TextAction.Turn],
+ [
+ TextAction.Bold,
+ TextAction.Italic,
+ TextAction.Underline,
+ TextAction.Strikethrough,
+ TextAction.Code,
+ TextAction.Equation,
+ ],
+];
diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/document/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/document/toolbar.ts
deleted file mode 100644
index 61c9a88e06..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/constants/document/toolbar.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-
-export const iconSize = { width: 18, height: 18 };
-
-export const command: Record = {
- bold: {
- title: 'Bold',
- key: '⌘ + B',
- },
- underlined: {
- title: 'Underlined',
- key: '⌘ + U',
- },
- italic: {
- title: 'Italic',
- key: '⌘ + I',
- },
- code: {
- title: 'Mark as code',
- key: '⌘ + E',
- },
- strikethrough: {
- title: 'Strike through',
- key: '⌘ + Shift + S or ⌘ + Shift + X',
- },
-};
diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
index 9f15a21213..f0a795d6ce 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
@@ -1,6 +1,6 @@
import { Editor } from 'slate';
import { RegionGrid } from '$app/utils/region_grid';
-import { ReactEditor } from "slate-react";
+import { ReactEditor } from 'slate-react';
export enum BlockType {
PageBlock = 'page',
@@ -11,6 +11,7 @@ export enum BlockType {
NumberedListBlock = 'numbered_list',
ToggleListBlock = 'toggle_list',
CodeBlock = 'code',
+ EquationBlock = 'math_equation',
EmbedBlock = 'embed',
QuoteBlock = 'quote',
CalloutBlock = 'callout',
@@ -87,7 +88,7 @@ export interface NestedBlock {
}
export interface TextDelta {
insert: string;
- attributes?: Record;
+ attributes?: Record;
}
export enum BlockActionType {
@@ -131,16 +132,21 @@ export interface DocumentState {
children: Record;
}
+export interface RectSelectionState {
+ selection: string[];
+ isDragging: boolean;
+}
export interface RangeSelectionState {
- isDragging?: boolean,
- anchor?: PointState,
- focus?: PointState,
+ anchor?: PointState;
+ focus?: PointState;
+ isForward?: boolean;
+ isDragging: boolean;
+ selection: string[];
}
-
export interface PointState {
- id: string,
- selection: TextSelection
+ id: string;
+ selection: TextSelection;
}
export enum ChangeType {
@@ -161,3 +167,62 @@ export interface BlockPBValue {
}
export type TextBlockKeyEventHandlerParams = [React.KeyboardEvent, ReactEditor & Editor];
+
+export enum SplitRelationship {
+ NextSibling,
+ FirstChild,
+}
+export enum TextAction {
+ Turn = 'turn',
+ Bold = 'bold',
+ Italic = 'italic',
+ Underline = 'underlined',
+ Strikethrough = 'strikethrough',
+ Code = 'code',
+ Equation = 'equation',
+}
+export interface TextActionMenuProps {
+ /**
+ * The custom items that will be covered in the default items
+ */
+ customItems?: TextAction[];
+ /**
+ * The items that will be excluded from the default items
+ */
+ excludeItems?: TextAction[];
+}
+
+export interface BlockConfig {
+ /**
+ * Whether the block can have children
+ */
+ canAddChild: boolean;
+ /**
+ * 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;
+ };
+
+ /**
+ * The props that will be passed to the text action menu
+ */
+ textActionMenuProps?: TextActionMenuProps;
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/text/backspace.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/text/backspace.ts
index 5feb8d75aa..10207b5799 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/text/backspace.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/text/backspace.ts
@@ -4,6 +4,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
import { outdentNodeThunk } from './outdent';
import { turnToTextBlockThunk } from '$app_reducers/document/async-actions/blocks/text/turn_to';
import { mergeToPrevLineThunk } from '$app_reducers/document/async-actions/blocks/text/merge';
+import { ReactEditor } from 'slate-react';
/**
* 1. If current node is not text block, turn it to text block
@@ -14,8 +15,8 @@ import { mergeToPrevLineThunk } from '$app_reducers/document/async-actions/block
*/
export const backspaceNodeThunk = createAsyncThunk(
'document/backspaceNode',
- async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
- const { id, controller } = payload;
+ async (payload: { id: string; controller: DocumentController; editor: ReactEditor }, thunkAPI) => {
+ const { id, controller, editor } = payload;
const { dispatch, getState } = thunkAPI;
const state = (getState() as { document: DocumentState }).document;
const node = state.nodes[id];
@@ -33,6 +34,7 @@ export const backspaceNodeThunk = createAsyncThunk(
// merge to previous line when parent is root
if (parentIsRoot || nextNodeId) {
// merge to previous line
+ ReactEditor.deselect(editor);
await dispatch(mergeToPrevLineThunk({ id, controller, deleteCurrentNode: true }));
return;
}
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 534cf6b24d..f5e931773c 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
@@ -1,11 +1,11 @@
-import { DocumentState } from '$app/interfaces/document';
+import { DocumentState, SplitRelationship } from '$app/interfaces/document';
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { setCursorBeforeThunk } from '../../cursor';
import { newBlock } from '$app/utils/document/blocks/common';
-import { blockConfig, SplitRelationship } from '$app/constants/document/config';
+import { blockConfig } from '$app/constants/document/config';
import { getSplitDelta } from '@/appflowy_app/utils/document/blocks/text/delta';
-import { ReactEditor } from "slate-react";
+import { ReactEditor } from 'slate-react';
export const splitNodeThunk = createAsyncThunk(
'document/splitNode',
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/format.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/format.ts
new file mode 100644
index 0000000000..94078f5d09
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/format.ts
@@ -0,0 +1,89 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { RootState } from '$app/stores/store';
+import { TextAction, TextDelta, TextSelection } from '$app/interfaces/document';
+import { getAfterRangeDelta, getBeforeRangeDelta, getRangeDelta } from '$app/utils/document/blocks/text/delta';
+import { DocumentController } from '$app/stores/effects/document/document_controller';
+
+export const getFormatActiveThunk = createAsyncThunk(
+ 'document/getFormatActive',
+ async (format, thunkAPI) => {
+ const { getState } = thunkAPI;
+ const state = getState() as RootState;
+ const { document } = state;
+ const { selection, anchor, focus } = state.documentRangeSelection;
+
+ const match = (delta: TextDelta[], format: TextAction) => {
+ return delta.every((op) => op.attributes?.[format] === true);
+ };
+ return selection.every((id) => {
+ const node = document.nodes[id];
+ let delta = node.data?.delta as TextDelta[];
+ if (!delta) return false;
+
+ if (id === anchor?.id) {
+ delta = getRangeDelta(delta, anchor.selection);
+ } else if (id === focus?.id) {
+ delta = getRangeDelta(delta, focus.selection);
+ }
+ return match(delta, format);
+ });
+ }
+);
+
+export const toggleFormatThunk = createAsyncThunk(
+ 'document/toggleFormat',
+ async (payload: { format: TextAction; controller: DocumentController; isActive: boolean }, thunkAPI) => {
+ const { getState } = thunkAPI;
+ const { format, controller, isActive } = payload;
+ const state = getState() as RootState;
+ const { document } = state;
+ const { selection, anchor, focus } = state.documentRangeSelection;
+ const ids = Array.from(new Set(selection));
+
+ const toggle = (delta: TextDelta[], format: TextAction) => {
+ return delta.map((op) => {
+ const attributes = {
+ ...op.attributes,
+ [format]: isActive ? undefined : true,
+ };
+ return {
+ insert: op.insert,
+ attributes: attributes,
+ };
+ });
+ };
+
+ const splitDelta = (delta: TextDelta[], selection: TextSelection) => {
+ const before = getBeforeRangeDelta(delta, selection);
+ const after = getAfterRangeDelta(delta, selection);
+ let middle = getRangeDelta(delta, selection);
+
+ middle = toggle(middle, format);
+
+ return [...before, ...middle, ...after];
+ };
+
+ const actions = ids.map((id) => {
+ const node = document.nodes[id];
+ let delta = node.data?.delta as TextDelta[];
+ if (!delta) return controller.getUpdateAction(node);
+
+ if (id === anchor?.id) {
+ delta = splitDelta(delta, anchor.selection);
+ } else if (id === focus?.id) {
+ delta = splitDelta(delta, focus.selection);
+ } else {
+ delta = toggle(delta, format);
+ }
+
+ return controller.getUpdateAction({
+ ...node,
+ data: {
+ ...node.data,
+ delta,
+ },
+ });
+ });
+ await controller.applyActions(actions);
+ }
+);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range_selection.ts
index 996fccafe8..34ccbb0bfe 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range_selection.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range_selection.ts
@@ -1,8 +1,10 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
-import { DocumentState, RangeSelectionState, TextSelection } from '$app/interfaces/document';
+import { DocumentState, TextSelection } from '$app/interfaces/document';
import { rangeSelectionActions } from '$app_reducers/document/slice';
-import { getNodeEndSelection, selectionIsForward } from '$app/utils/document/blocks/text/delta';
+import { getNodeBeginSelection, getNodeEndSelection } from '$app/utils/document/blocks/text/delta';
import { isEqual } from '$app/utils/tool';
+import { RootState } from '$app/stores/store';
+import { getNodesInRange } from '$app/utils/document/blocks/common';
const amendAnchorNodeThunk = createAsyncThunk(
'document/amendAnchorNode',
@@ -15,22 +17,18 @@ const amendAnchorNodeThunk = createAsyncThunk(
const { id } = payload;
const { getState, dispatch } = thunkAPI;
const nodes = (getState() as { document: DocumentState }).document.nodes;
- const range = (getState() as { documentRangeSelection: RangeSelectionState }).documentRangeSelection;
- const { anchor: anchorNode, isDragging, focus: focusNode } = range;
+
+ const state = getState() as RootState;
+ const { isDragging, isForward, ...range } = state.documentRangeSelection;
+ const { anchor: anchorNode, focus: focusNode } = range;
if (!isDragging || !anchorNode || anchorNode.id !== id) return;
const isCollapsed = focusNode?.id === id && anchorNode?.id === id;
if (isCollapsed) return;
const selection = anchorNode.selection;
- const isForward = selectionIsForward(selection);
const node = nodes[id];
- const focus = isForward
- ? getNodeEndSelection(node.data.delta).anchor
- : {
- path: [0, 0],
- offset: 0,
- };
+ const focus = isForward ? getNodeEndSelection(node.data.delta).anchor : getNodeBeginSelection().anchor;
if (isEqual(focus, selection.focus)) return;
const newSelection = {
anchor: selection.anchor,
@@ -58,29 +56,64 @@ export const syncRangeSelectionThunk = createAsyncThunk(
thunkAPI
) => {
const { getState, dispatch } = thunkAPI;
- const range = (getState() as { documentRangeSelection: RangeSelectionState }).documentRangeSelection;
+ const state = getState() as RootState;
+ const range = state.documentRangeSelection;
+ const isDragging = range.isDragging;
const { id, selection } = payload;
+
const updateRange = {
focus: {
id,
selection,
},
};
- const isAnchor = range.anchor?.id === id;
- if (isAnchor) {
+
+ if (!isDragging && range.anchor?.id === id) {
Object.assign(updateRange, {
anchor: {
id,
- selection,
+ selection: { ...selection },
+ },
+ });
+ dispatch(rangeSelectionActions.setRange(updateRange));
+ return;
+ }
+ if (!range.anchor || range.anchor.id === id) {
+ Object.assign(updateRange, {
+ anchor: {
+ id,
+ selection: {
+ anchor: !range.anchor ? selection.anchor : range.anchor.selection.anchor,
+ focus: selection.focus,
+ },
},
});
}
+
dispatch(rangeSelectionActions.setRange(updateRange));
const anchorId = range.anchor?.id;
- if (!isAnchor && anchorId) {
+ // more than one node is selected
+ if (anchorId && anchorId !== id) {
dispatch(amendAnchorNodeThunk({ id: anchorId }));
}
}
);
+
+export const setRangeSelectionThunk = createAsyncThunk('document/setRangeSelection', async (payload, thunkAPI) => {
+ const { getState, dispatch } = thunkAPI;
+ const state = getState() as RootState;
+ const { anchor, focus, isForward } = state.documentRangeSelection;
+ const document = state.document;
+ if (!anchor || !focus || isForward === undefined) return;
+ const rangeIds = getNodesInRange(
+ {
+ startId: anchor.id,
+ endId: focus.id,
+ },
+ isForward,
+ document
+ );
+ dispatch(rangeSelectionActions.setSelection(rangeIds));
+});
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
index e91baf3869..5e307af906 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
@@ -1,16 +1,29 @@
-import { DocumentState, Node, RangeSelectionState } from '@/appflowy_app/interfaces/document';
+import {
+ DocumentState,
+ Node,
+ PointState,
+ RangeSelectionState,
+ RectSelectionState,
+} from '@/appflowy_app/interfaces/document';
import { BlockEventPayloadPB } from '@/services/backend';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { parseValue, matchChange } from '$app/utils/document/subscribe';
+import { getNodesInRange } from '$app/utils/document/blocks/common';
const initialState: DocumentState = {
nodes: {},
children: {},
};
-const rectSelectionInitialState: string[] = [];
+const rectSelectionInitialState: RectSelectionState = {
+ selection: [],
+ isDragging: false,
+};
-const rangeSelectionInitialState: RangeSelectionState = {};
+const rangeSelectionInitialState: RangeSelectionState = {
+ isDragging: false,
+ selection: [],
+};
export const documentSlice = createSlice({
name: 'document',
@@ -35,7 +48,6 @@ export const documentSlice = createSlice({
state.nodes = nodes;
state.children = children;
},
-
/**
This function listens for changes in the data layer triggered by the data API,
and updates the UI state accordingly.
@@ -67,14 +79,18 @@ export const rectSelectionSlice = createSlice({
reducers: {
// update block selections
updateSelections: (state, action: PayloadAction) => {
- return action.payload;
+ state.selection = action.payload;
},
// set block selected
setSelectionById: (state, action: PayloadAction) => {
const id = action.payload;
- if (state.includes(id)) return;
- state.push(id);
+ if (state.selection.includes(id)) return;
+ state.selection = [...state.selection, id];
+ },
+
+ setDragging: (state, action: PayloadAction) => {
+ state.isDragging = action.payload;
},
},
});
@@ -83,13 +99,27 @@ export const rangeSelectionSlice = createSlice({
name: 'documentRangeSelection',
initialState: rangeSelectionInitialState,
reducers: {
- setRange: (state, action: PayloadAction) => {
+ setRange: (
+ state,
+ action: PayloadAction<{
+ anchor?: PointState;
+ focus?: PointState;
+ }>
+ ) => {
return {
...state,
...action.payload,
};
},
-
+ setSelection: (state, action: PayloadAction) => {
+ state.selection = action.payload;
+ },
+ setDragging: (state, action: PayloadAction) => {
+ state.isDragging = action.payload;
+ },
+ setForward: (state, action: PayloadAction) => {
+ state.isForward = action.payload;
+ },
clearRange: (state, _: PayloadAction) => {
return rangeSelectionInitialState;
},
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/code/decorate.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/code/decorate.ts
index 7944d7ac65..81e7ff7423 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/code/decorate.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/code/decorate.ts
@@ -28,7 +28,7 @@ import 'prismjs/components/prism-php';
import 'prismjs/components/prism-sql';
import 'prismjs/components/prism-visual-basic';
-import { BaseRange, NodeEntry, Text, Path } from 'slate';
+import { BaseRange, NodeEntry, Text, Path, Range, Editor } from 'slate';
const push_string = (
token: string | Prism.Token,
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 a53b109999..b01c7b4d91 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
@@ -151,9 +151,54 @@ export function getCollapsedRange(id: string, selection: TextSelection): RangeSe
anchor: clone(point),
focus: clone(point),
isDragging: false,
+ selection: [],
};
}
+export function iterateNodes(
+ range: {
+ startId: string;
+ endId: string;
+ },
+ isForward: boolean,
+ document: DocumentState,
+ callback: (nodeId?: string) => boolean
+) {
+ const { startId, endId } = range;
+ let currentId = startId;
+ while (currentId && currentId !== endId) {
+ if (isForward) {
+ currentId = getNextLineId(document, currentId) || '';
+ } else {
+ currentId = getPrevLineId(document, currentId) || '';
+ }
+ if (callback(currentId)) {
+ break;
+ }
+ }
+}
+export function getNodesInRange(
+ range: {
+ startId: string;
+ endId: string;
+ },
+ isForward: boolean,
+ document: DocumentState
+) {
+ const nodeIds: string[] = [];
+ nodeIds.push(range.startId);
+ iterateNodes(range, isForward, document, (nodeId) => {
+ if (nodeId) {
+ nodeIds.push(nodeId);
+ return false;
+ } else {
+ return true;
+ }
+ });
+ nodeIds.push(range.endId);
+ return nodeIds;
+}
+
export function nodeInRange(
id: string,
range: {
@@ -163,17 +208,13 @@ export function nodeInRange(
isForward: boolean,
document: DocumentState
) {
- const { startId, endId } = range;
- let currentId = startId;
- while (currentId && currentId !== id && currentId !== endId) {
- if (isForward) {
- currentId = getNextLineId(document, currentId) || '';
- } else {
- currentId = getPrevLineId(document, currentId) || '';
+ let match = false;
+ iterateNodes(range, isForward, document, (nodeId) => {
+ if (nodeId === id) {
+ match = true;
+ return true;
}
- }
- if (currentId === id) {
- return true;
- }
- return false;
+ return false;
+ });
+ return match;
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/selection.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/selection.ts
new file mode 100644
index 0000000000..3c8cf5b3da
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/selection.ts
@@ -0,0 +1,22 @@
+export function isPointInBlock(target: HTMLElement | null) {
+ let node = target;
+ while (node) {
+ if (node.getAttribute('data-block-id')) {
+ return true;
+ }
+ node = node.parentElement;
+ }
+ return false;
+}
+
+export function getBlockIdByPoint(target: HTMLElement | null) {
+ let node = target;
+ while (node) {
+ const id = node.getAttribute('data-block-id');
+ if (id) {
+ return id;
+ }
+ node = node.parentElement;
+ }
+ return null;
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/delta.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/delta.ts
index e5c259e383..077f06ad67 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/delta.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/delta.ts
@@ -1,4 +1,4 @@
-import { Editor, Element, Location, Text } from 'slate';
+import { Editor, Element, Location, Text, Range } from 'slate';
import { SelectionPoint, TextDelta, TextSelection } from '$app/interfaces/document';
import * as Y from 'yjs';
import { getDeltaFromSlateNodes } from '$app/utils/document/blocks/common';
@@ -14,6 +14,86 @@ export function getDelta(editor: Editor, at: Location): TextDelta[] {
});
}
+export function getBeforeRangeDelta(delta: TextDelta[], range: TextSelection): TextDelta[] {
+ const anchor = Range.start(range);
+ const sliceNodes = delta.slice(0, anchor.path[1] + 1);
+ const sliceEnd = sliceNodes[sliceNodes.length - 1];
+ const sliceEndText = sliceEnd.insert.slice(0, anchor.offset);
+ const sliceEndAttributes = sliceEnd.attributes;
+ const sliceEndNode =
+ sliceEndText.length > 0
+ ? {
+ insert: sliceEndText,
+ attributes: sliceEndAttributes,
+ }
+ : null;
+ const sliceMiddleNodes = sliceNodes.slice(0, sliceNodes.length - 1);
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ return [...sliceMiddleNodes, sliceEndNode].filter((item) => item);
+}
+
+export function getAfterRangeDelta(delta: TextDelta[], range: TextSelection): TextDelta[] {
+ const focus = Range.end(range);
+ const sliceNodes = delta.slice(focus.path[1], delta.length);
+ const sliceStart = sliceNodes[0];
+ const sliceStartText = sliceStart.insert.slice(focus.offset);
+ const sliceStartAttributes = sliceStart.attributes;
+ const sliceStartNode =
+ sliceStartText.length > 0
+ ? {
+ insert: sliceStartText,
+ attributes: sliceStartAttributes,
+ }
+ : null;
+ const sliceMiddleNodes = sliceNodes.slice(1, sliceNodes.length);
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ return [sliceStartNode, ...sliceMiddleNodes].filter((item) => item);
+}
+
+export function getRangeDelta(delta: TextDelta[], range: TextSelection): TextDelta[] {
+ const anchor = Range.start(range);
+ const focus = Range.end(range);
+ const sliceNodes = delta.slice(anchor.path[1], focus.path[1] + 1);
+ if (anchor.path[1] === focus.path[1]) {
+ return sliceNodes.map((item) => {
+ const { insert, attributes } = item;
+ const text = insert.slice(anchor.offset, focus.offset);
+ return {
+ insert: text,
+ attributes,
+ };
+ });
+ }
+ const sliceStart = sliceNodes[0];
+ const sliceEnd = sliceNodes[sliceNodes.length - 1];
+ const sliceStartText = sliceStart.insert.slice(anchor.offset);
+ const sliceEndText = sliceEnd.insert.slice(0, focus.offset);
+ const sliceStartAttributes = sliceStart.attributes;
+ const sliceEndAttributes = sliceEnd.attributes;
+ const sliceStartNode =
+ sliceStartText.length > 0
+ ? {
+ insert: sliceStartText,
+ attributes: sliceStartAttributes,
+ }
+ : null;
+
+ const sliceEndNode =
+ sliceEndText.length > 0
+ ? {
+ insert: sliceEndText,
+ attributes: sliceEndAttributes,
+ }
+ : null;
+ const sliceMiddleNodes = sliceNodes.slice(1, sliceNodes.length - 1);
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ return [sliceStartNode, ...sliceMiddleNodes, sliceEndNode].filter((item) => item);
+}
/**
* get the selection between the beginning of the editor and the point
* form 0 to point
@@ -290,7 +370,8 @@ export function getPointOfCurrentLineBeginning(editor: Editor) {
return beginPoint;
}
-export function selectionIsForward(selection: TextSelection) {
+export function selectionIsForward(selection: TextSelection | null) {
+ if (!selection) return false;
const { anchor, focus } = selection;
if (!anchor || !focus) return false;
return anchor.path[1] < focus.path[1] || (anchor.path[1] === focus.path[1] && anchor.offset < focus.offset);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/format.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/format.ts
deleted file mode 100644
index fd36928b76..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/format.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import {
- Editor,
- Transforms,
- Text,
- Node
-} from 'slate';
-
-export function toggleFormat(editor: Editor, format: string) {
- const isActive = isFormatActive(editor, format)
- Transforms.setNodes(
- editor,
- { [format]: isActive ? null : true },
- { match: Text.isText, split: true }
- )
-}
-
-export const isFormatActive = (editor: Editor, format: string) => {
- const [match] = Editor.nodes(editor, {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- match: (n: Node) => n[format] === true,
- mode: 'all',
- })
- return !!match
-}
\ No newline at end of file
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/hotkey.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/hotkey.ts
index 32d2adcd78..046573a32f 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/hotkey.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/hotkey.ts
@@ -1,5 +1,4 @@
import isHotkey from 'is-hotkey';
-import { toggleFormat } from './format';
import { Editor, Range } from 'slate';
import { getAfterRangeAt, getBeforeRangeAt, pointInBegin, pointInEnd } from './delta';
import { keyBoardEventKeyMap } from '$app/constants/document/text_block';
@@ -13,16 +12,6 @@ const HOTKEYS: Record = {
'mod+shift+S': 'strikethrough',
};
-export function triggerHotkey(event: React.KeyboardEvent, editor: Editor) {
- for (const hotkey in HOTKEYS) {
- if (isHotkey(hotkey, event)) {
- event.preventDefault();
- const format = HOTKEYS[hotkey];
- toggleFormat(editor, format);
- }
- }
-}
-
export function canHandleBackspaceKey(event: React.KeyboardEvent, editor: Editor) {
const isBackspaceKey = isHotkey('backspace', event);
const selection = editor.selection;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/toolbar.ts
deleted file mode 100644
index 52681474d5..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/blocks/text/toolbar.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Editor, Range } from 'slate';
-export function calcToolbarPosition(editor: Editor, toolbarDom: HTMLDivElement, blockRect: DOMRect) {
- const { selection } = editor;
-
- if (!selection || Range.isCollapsed(selection) || Editor.string(editor, selection) === '') {
- return;
- }
-
- const domSelection = window.getSelection();
- let domRange;
- if (domSelection?.rangeCount === 0) {
- return;
- } else {
- domRange = domSelection?.getRangeAt(0);
- }
-
- const rect = domRange?.getBoundingClientRect() || { top: 0, left: 0, width: 0, height: 0 };
-
- const top = `${-toolbarDom.offsetHeight - 5 + (rect.top - blockRect.y)}px`;
- const left = `${rect.left - blockRect.x - toolbarDom.offsetWidth / 2 + rect.width / 2}px`;
-
- return {
- top,
- left,
- }
-
-}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts
new file mode 100644
index 0000000000..1ce16134f6
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts
@@ -0,0 +1,19 @@
+export function calcToolbarPosition(toolbarDom: HTMLDivElement) {
+ const domSelection = window.getSelection();
+ let domRange;
+ if (domSelection?.rangeCount === 0) {
+ return;
+ } else {
+ domRange = domSelection?.getRangeAt(0);
+ }
+
+ const rect = domRange?.getBoundingClientRect() || { top: 0, left: 0, width: 0, height: 0 };
+
+ let top = rect.top - toolbarDom.offsetHeight;
+ let left = rect.left - toolbarDom.offsetWidth / 2 + rect.width / 2;
+
+ return {
+ top: top + 'px',
+ left: left + 'px',
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
index 301c241081..db09ea8959 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx
@@ -3,9 +3,15 @@ import { createTheme, ThemeProvider } from '@mui/material';
import Root from '../components/document/Root';
import { DocumentControllerContext } from '../stores/effects/document/document_controller';
-const theme = createTheme({
+const muiTheme = createTheme({
typography: {
fontFamily: ['Poppins'].join(','),
+ fontSize: 14,
+ },
+ palette: {
+ primary: {
+ main: '#00BCF0',
+ },
},
});
@@ -14,7 +20,7 @@ export const DocumentPage = () => {
if (!documentId || !documentData || !controller) return null;
return (
-
+
diff --git a/frontend/appflowy_tauri/src/services/backend/index.ts b/frontend/appflowy_tauri/src/services/backend/index.ts
index d9d87d088c..07a9c14cca 100644
--- a/frontend/appflowy_tauri/src/services/backend/index.ts
+++ b/frontend/appflowy_tauri/src/services/backend/index.ts
@@ -4,3 +4,5 @@ export * from "./models/flowy-folder2";
export * from "./models/flowy-document2";
export * from "./models/flowy-net";
export * from "./models/flowy-error";
+export * from "./models/flowy-config";
+
diff --git a/frontend/appflowy_tauri/src/styles/template.css b/frontend/appflowy_tauri/src/styles/template.css
index d25f3384b2..5001b5650f 100644
--- a/frontend/appflowy_tauri/src/styles/template.css
+++ b/frontend/appflowy_tauri/src/styles/template.css
@@ -24,7 +24,6 @@ body {
@apply bg-[transparent]
}
-
.btn {
@apply rounded-xl border border-gray-500 px-4 py-3;
}