diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockRangeSelection.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockRangeSelection.hooks.ts
index edc6cc9fa4..a222e401d8 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockRangeSelection.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockRangeSelection.hooks.ts
@@ -30,6 +30,7 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
const reset = useCallback(() => {
dispatch(rangeActions.clearRange());
+ setForward(true);
}, [dispatch]);
// display caret color
@@ -85,7 +86,6 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
const handleDragStart = useCallback(
(e: MouseEvent) => {
- // reset the range
reset();
// skip if the target is not a block
const blockId = getBlockIdByPoint(e.target as HTMLElement);
@@ -150,6 +150,8 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
const handleDragEnd = useCallback(() => {
if (!isDragging) return;
+ setFocus(null);
+ anchorRef.current = null;
dispatch(rangeActions.setDragging(false));
}, [dispatch, isDragging]);
@@ -164,7 +166,7 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
document.removeEventListener('mouseup', handleDragEnd);
container.removeEventListener('keydown', onKeyDown, true);
};
- }, [handleDragStart, handleDragEnd, handleDraging, container, onKeyDown]);
+ }, [reset, handleDragStart, handleDragEnd, handleDraging, container, onKeyDown]);
return null;
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/RangeKeyDown.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/RangeKeyDown.hooks.ts
index 8e8eb67659..a841e8ec1a 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/RangeKeyDown.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/RangeKeyDown.hooks.ts
@@ -121,7 +121,9 @@ export function useRangeKeyDown() {
return;
}
const { anchor, focus } = rangeRef.current;
- if (anchor?.id === focus?.id) {
+ if (!anchor || !focus) return;
+
+ if (anchor.id === focus.id) {
return;
}
e.stopPropagation();
@@ -131,7 +133,9 @@ export function useRangeKeyDown() {
return;
}
const lastEvent = filteredEvents[lastIndex];
- lastEvent?.handler(e);
+ if (!lastEvent) return;
+ e.preventDefault();
+ lastEvent.handler(e);
},
[interceptEvents, rangeRef]
);
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 a7c65bd5da..e35786b190 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
@@ -5,6 +5,7 @@ import { useChange } from '$app/components/document/_shared/EditorHooks/useChang
import { useKeyDown } from './useKeyDown';
import CodeEditor from '$app/components/document/_shared/SlateEditor/CodeEditor';
import { useSelection } from '$app/components/document/_shared/EditorHooks/useSelection';
+import { useSubscribeDecorate } from '$app/components/document/_shared/SubscribeSelection.hooks';
export default function CodeBlock({
node,
@@ -16,7 +17,8 @@ export default function CodeBlock({
const onKeyDown = useKeyDown(id);
const className = props.className ? ` ${props.className}` : '';
const { value, onChange } = useChange(node);
- const { onSelectionChange, selection, lastSelection } = useSelection(id);
+ const selectionProps = useSelection(id);
+
return (
@@ -28,9 +30,7 @@ export default function CodeBlock({
placeholder={placeholder}
language={language}
onKeyDown={onKeyDown}
- onSelectionChange={onSelectionChange}
- selection={selection}
- lastSelection={lastSelection}
+ {...selectionProps}
/>
);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
index 9c2b95fd8a..a7f6bc9bcb 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
@@ -16,6 +16,7 @@ import DividerBlock from '$app/components/document/DividerBlock';
import CalloutBlock from '$app/components/document/CalloutBlock';
import BlockOverlay from '$app/components/document/Overlay/BlockOverlay';
import CodeBlock from '$app/components/document/CodeBlock';
+import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks';
function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes
) {
const { node, childIds, isSelected, ref } = useNode(id);
@@ -60,13 +61,15 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes
- {renderBlock()}
-
- {isSelected ? (
-
- ) : null}
-
+
+
+ {renderBlock()}
+
+ {isSelected ? (
+
+ ) : null}
+
+
);
}
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 4280e429f9..ea38d29c92 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
@@ -5,6 +5,7 @@ import TextActionMenu from '$app/components/document/TextActionMenu';
import BlockSlash from '$app/components/document/BlockSlash';
import { useCopy } from '$app/components/document/_shared/CopyPasteHooks/useCopy';
import { usePaste } from '$app/components/document/_shared/CopyPasteHooks/usePaste';
+import LinkEditPopover from '$app/components/document/_shared/TextLink/LinkEditPopover';
export default function Overlay({ container }: { container: HTMLDivElement }) {
useCopy(container);
@@ -15,6 +16,7 @@ export default function Overlay({ container }: { container: HTMLDivElement }) {
+
>
);
}
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
index 4d00d48393..b72827689a 100644
--- 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
@@ -6,7 +6,7 @@ import { debounce } from '$app/utils/tool';
export function useMenuStyle(container: HTMLDivElement) {
const ref = useRef(null);
- const id = useAppSelector((state) => state.documentRange.focus?.id);
+ const id = useAppSelector((state) => state.documentRange.caret?.id);
const [isScrolling, setIsScrolling] = useState(false);
const reCalculatePosition = useCallback(() => {
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
index 82350c3a1b..66edf30113 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/index.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/index.tsx
@@ -2,6 +2,7 @@ import { useMenuStyle } from './index.hooks';
import { useAppSelector } from '$app/stores/store';
import TextActionMenuList from '$app/components/document/TextActionMenu/menu';
import BlockPortal from '$app/components/document/BlockPortal';
+import { useMemo } from 'react';
const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
const { ref, id } = useMenuStyle(container);
@@ -14,7 +15,7 @@ const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
style={{
opacity: 0,
}}
- className='absolute mt-[-6px] inline-flex h-[32px] min-w-[100px] items-stretch overflow-hidden rounded-[8px] bg-[#333] leading-tight shadow-lg transition-opacity duration-100'
+ className='absolute mt-[-6px] inline-flex h-[32px] min-w-[100px] items-stretch overflow-hidden rounded-[8px] bg-black leading-tight text-white shadow-lg transition-opacity duration-100'
onMouseDown={(e) => {
// prevent toolbar from taking focus away from editor
e.preventDefault();
@@ -27,16 +28,24 @@ const TextActionComponent = ({ container }: { container: HTMLDivElement }) => {
);
};
const TextActionMenu = ({ container }: { container: HTMLDivElement }) => {
- const canShow = useAppSelector((state) => {
- const { isDragging, focus, anchor, ranges } = state.documentRange;
+ const range = useAppSelector((state) => state.documentRange);
+ const canShow = useMemo(() => {
+ const { isDragging, focus, anchor, ranges, caret } = range;
+ // don't show if dragging
if (isDragging) return false;
- if (!focus || !anchor) return false;
- const isSameLine = anchor.id === focus.id;
- const anchorRange = ranges[anchor.id];
- if (!anchorRange) return false;
- const isCollapsed = isSameLine && anchorRange.length === 0;
- return !isCollapsed;
- });
+ // don't show if no focus or anchor
+ if (!caret) return false;
+ const isSameLine = anchor?.id === focus?.id;
+
+ // show toolbar if range has multiple nodes
+ if (!isSameLine) return true;
+ const caretRange = ranges[caret.id];
+ // don't show if no caret range
+ if (!caretRange) return false;
+ // show toolbar if range is not collapsed
+ return caretRange.length > 0;
+ }, [range]);
+
if (!canShow) return null;
return ;
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
index cdbb725f94..0f8b322278 100644
--- 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
@@ -7,6 +7,7 @@ import { getFormatActiveThunk, toggleFormatThunk } from '$app_reducers/document/
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';
+import { newLinkThunk } from '$app_reducers/document/async-actions/link';
const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) => {
const dispatch = useAppDispatch();
@@ -25,6 +26,7 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
[TextAction.Underline]: 'Underline',
[TextAction.Strikethrough]: 'Strike through',
[TextAction.Code]: 'Mark as Code',
+ [TextAction.Link]: 'Add Link',
}),
[]
);
@@ -49,6 +51,26 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
[controller, dispatch, isActive]
);
+ const addLink = useCallback(() => {
+ dispatch(newLinkThunk());
+ }, [dispatch]);
+
+ const formatClick = useCallback(
+ (format: TextAction) => {
+ switch (format) {
+ case TextAction.Bold:
+ case TextAction.Italic:
+ case TextAction.Underline:
+ case TextAction.Strikethrough:
+ case TextAction.Code:
+ return toggleFormat(format);
+ case TextAction.Link:
+ return addLink();
+ }
+ },
+ [addLink, toggleFormat]
+ );
+
useEffect(() => {
void (async () => {
const isActive = await isFormatActive();
@@ -58,7 +80,7 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
return (
- toggleFormat(format)}>
+ formatClick(format)}>
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatIcon.tsx
index d8c0024213..89ac65768b 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatIcon.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatIcon.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material';
import { TextAction } from '$app/interfaces/document';
+import LinkIcon from '@mui/icons-material/AddLink';
export const iconSize = { width: 18, height: 18 };
export default function FormatIcon({ icon }: { icon: string }) {
@@ -15,6 +16,18 @@ export default function FormatIcon({ icon }: { icon: string }) {
return ;
case TextAction.Strikethrough:
return ;
+ case TextAction.Link:
+ return (
+
+ );
default:
return null;
}
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
index f20ff69f69..c13332eadd 100644
--- 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
@@ -15,13 +15,14 @@ export function useTextActionMenu() {
const isSingleLine = useMemo(() => {
return range.focus?.id === range.anchor?.id;
}, [range]);
- const focusId = range.focus?.id;
+ const focusId = range.caret?.id;
const { node } = useSubscribeNode(focusId || '');
const items = useMemo(() => {
+ if (!node) return [];
if (isSingleLine) {
- const config = blockConfig[node?.type];
+ const config = blockConfig[node.type];
const { customItems, excludeItems } = {
...defaultTextActionProps,
...config.textActionMenuProps,
@@ -30,7 +31,7 @@ export function useTextActionMenu() {
} else {
return multiLineTextActionProps.customItems || [];
}
- }, [isSingleLine, node?.type]);
+ }, [isSingleLine, node]);
// the groups have default items, so we need to filter the items if this node has excluded items
const groupItems: TextAction[][] = useMemo(() => {
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
index 081f96f951..07ae25151e 100644
--- 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
@@ -11,6 +11,7 @@ function TextActionMenuList() {
switch (action) {
case TextAction.Turn:
return isSingleLine && focusId ? : null;
+ case TextAction.Link:
case TextAction.Bold:
case TextAction.Italic:
case TextAction.Underline:
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 9401d1aac3..88efbbaf20 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
@@ -13,20 +13,12 @@ interface Props {
}
function TextBlock({ node, childIds, placeholder }: Props) {
const { value, onChange } = useChange(node);
- const { onSelectionChange, selection, lastSelection } = useSelection(node.id);
+ const selectionProps = useSelection(node.id);
const { onKeyDown } = useKeyDown(node.id);
return (
<>
-
+
>
);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useKeyDown.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useKeyDown.ts
index 166fe6bee8..cf94a19494 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useKeyDown.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useKeyDown.ts
@@ -89,7 +89,6 @@ export function useKeyDown(id: string) {
const onKeyDown = useCallback(
(e: React.KeyboardEvent) => {
e.stopPropagation();
-
const filteredEvents = interceptEvents.filter((event) => event.canHandle(e));
filteredEvents.forEach((event) => event.handler(e));
},
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts
index 7100f2cf3e..ebd9866d5d 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts
@@ -74,8 +74,10 @@ export function useTurnIntoBlockEvents(id: string) {
[BlockType.HeadingBlock]: () => {
const flag = getFlag();
if (!flag) return;
+ const level = flag.match(/#/g)?.length;
+ if (!level || level > 3) return;
return {
- level: flag.match(/#/g)?.length,
+ level,
...getTurnIntoBlockDelta(),
};
},
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useSelection.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useSelection.ts
index 7d6093f54d..208f75a1b4 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useSelection.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useSelection.ts
@@ -2,12 +2,17 @@ import { useCallback, useEffect, useState } from 'react';
import { RangeStatic } from 'quill';
import { useAppDispatch } from '$app/stores/store';
import { rangeActions } from '$app_reducers/document/slice';
-import { useFocused, useRangeRef } from '$app/components/document/_shared/SubscribeSelection.hooks';
+import {
+ useFocused,
+ useRangeRef,
+ useSubscribeDecorate,
+} from '$app/components/document/_shared/SubscribeSelection.hooks';
import { storeRangeThunk } from '$app_reducers/document/async-actions/range';
export function useSelection(id: string) {
const rangeRef = useRangeRef();
- const { focusCaret, lastSelection } = useFocused(id);
+ const { focusCaret } = useFocused(id);
+ const decorateProps = useSubscribeDecorate(id);
const [selection, setSelection] = useState(undefined);
const dispatch = useAppDispatch();
@@ -21,7 +26,6 @@ export function useSelection(id: string) {
const onSelectionChange = useCallback(
(range: RangeStatic | null, _oldRange: RangeStatic | null, _source?: string) => {
if (!range) return;
-
dispatch(
rangeActions.setCaret({
id,
@@ -36,20 +40,19 @@ export function useSelection(id: string) {
useEffect(() => {
if (rangeRef.current && rangeRef.current?.isDragging) return;
- const caret = focusCaret;
- if (!caret) {
+ if (!focusCaret) {
+ setSelection(undefined);
return;
}
-
setSelection({
- index: caret.index,
- length: caret.length,
+ index: focusCaret.index,
+ length: focusCaret.length,
});
}, [rangeRef, focusCaret]);
return {
onSelectionChange,
selection,
- lastSelection,
+ ...decorateProps,
};
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Message/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Message/index.tsx
new file mode 100644
index 0000000000..99eac15767
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/Message/index.tsx
@@ -0,0 +1,47 @@
+import React, { useCallback, useMemo, useState } from 'react';
+import { Portal, Snackbar } from '@mui/material';
+import { TransitionProps } from '@mui/material/transitions';
+import Slide, { SlideProps } from '@mui/material/Slide';
+
+function SlideTransition(props: SlideProps) {
+ return ;
+}
+
+interface MessageProps {
+ message?: string;
+ key?: string;
+ duration?: number;
+}
+export function useMessage() {
+ const [state, setState] = useState();
+ const show = useCallback((message: MessageProps) => {
+ setState(message);
+ }, []);
+ const hide = useCallback(() => {
+ setState(undefined);
+ }, []);
+
+ const contentHolder = useMemo(() => {
+ const open = !!state;
+ return (
+
+
+
+ );
+ }, [hide, state]);
+
+ return {
+ show,
+ hide,
+ contentHolder,
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/CodeEditor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/CodeEditor.tsx
index 6feeaa68a8..ed476f83d9 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/CodeEditor.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/CodeEditor.tsx
@@ -3,10 +3,11 @@ import { CodeEditorProps } from '$app/interfaces/document';
import { Editable, Slate } from 'slate-react';
import { useEditor } from '$app/components/document/_shared/SlateEditor/useEditor';
import { decorateCode } from '$app/components/document/_shared/SlateEditor/decorateCode';
-import { CodeLeaf, CodeBlockElement } from '$app/components/document/_shared/SlateEditor/CodeElements';
+import { CodeBlockElement } from '$app/components/document/_shared/SlateEditor/CodeElements';
+import TextLeaf from '$app/components/document/_shared/SlateEditor/TextLeaf';
function CodeEditor({ language, ...props }: CodeEditorProps) {
- const { editor, onChange, value, onDOMBeforeInput, decorate, ref, onKeyDown, onBlur } = useEditor({
+ const { editor, onChange, value, ref, ...editableProps } = useEditor({
...props,
isCodeBlock: true,
});
@@ -15,16 +16,14 @@ function CodeEditor({ language, ...props }: CodeEditorProps) {
{
const codeRange = decorateCode(entry, language);
- const range = decorate?.(entry) || [];
+ const range = editableProps.decorate?.(entry) || [];
return [...range, ...codeRange];
}}
- renderLeaf={CodeLeaf}
+ renderLeaf={(leafProps) => }
renderElement={CodeBlockElement}
- onKeyDown={onKeyDown}
- onDOMBeforeInput={onDOMBeforeInput}
- onBlur={onBlur}
/>
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/CodeElements.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/CodeElements.tsx
index 8169420bfa..c3111a5a42 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/CodeElements.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/CodeElements.tsx
@@ -1,46 +1,4 @@
-import { RenderLeafProps, RenderElementProps } from 'slate-react';
-import { BaseText } from 'slate';
-
-interface CodeLeafProps extends RenderLeafProps {
- leaf: BaseText & {
- bold?: boolean;
- italic?: boolean;
- underline?: boolean;
- strikethrough?: boolean;
- prism_token?: string;
- selection_high_lighted?: boolean;
- };
-}
-
-export const CodeLeaf = (props: CodeLeafProps) => {
- const { attributes, children, leaf } = props;
-
- let newChildren = children;
- if (leaf.bold) {
- newChildren = {children};
- }
-
- if (leaf.italic) {
- newChildren = {newChildren};
- }
-
- if (leaf.underline) {
- newChildren = {newChildren};
- }
-
- const className = [
- 'token',
- leaf.prism_token && leaf.prism_token,
- leaf.strikethrough && 'line-through',
- leaf.selection_high_lighted && 'bg-main-secondary',
- ].filter(Boolean);
-
- return (
-
- {newChildren}
-
- );
-};
+import { RenderElementProps } from 'slate-react';
export const CodeBlockElement = (props: RenderElementProps) => {
return (
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextEditor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextEditor.tsx
index 21683b6e9d..2a87497053 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextEditor.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextEditor.tsx
@@ -6,19 +6,16 @@ import TextLeaf from '$app/components/document/_shared/SlateEditor/TextLeaf';
import { TextElement } from '$app/components/document/_shared/SlateEditor/TextElement';
function TextEditor({ placeholder = "Type '/' for commands", ...props }: EditorProps) {
- const { editor, onChange, value, onDOMBeforeInput, decorate, ref, onKeyDown, onBlur } = useEditor(props);
+ const { editor, onChange, value, ref, ...editableProps } = useEditor(props);
return (
}
placeholder={placeholder}
- onBlur={onBlur}
renderElement={TextElement}
+ {...editableProps}
/>
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextLeaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextLeaf.tsx
index b463547f9e..d06a87e083 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextLeaf.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextLeaf.tsx
@@ -1,35 +1,33 @@
-import { RenderLeafProps } from 'slate-react';
+import { ReactEditor, RenderLeafProps } from 'slate-react';
import { BaseText } from 'slate';
-import { useRef } from 'react';
+import { useCallback, useRef } from 'react';
+import TextLink from '../TextLink';
+import { converToIndexLength } from '$app/utils/document/slate_editor';
+import LinkHighLight from '$app/components/document/_shared/TextLink/LinkHighLight';
+interface Attributes {
+ bold?: boolean;
+ italic?: boolean;
+ underline?: boolean;
+ strikethrough?: boolean;
+ code?: string;
+ selection_high_lighted?: boolean;
+ href?: string;
+ prism_token?: string;
+ link_selection_lighted?: boolean;
+ link_placeholder?: string;
+}
interface TextLeafProps extends RenderLeafProps {
- leaf: BaseText & {
- bold?: boolean;
- italic?: boolean;
- underline?: boolean;
- strikethrough?: boolean;
- code?: string;
- selection_high_lighted?: boolean;
- };
+ leaf: BaseText & Attributes;
+ isCodeBlock?: boolean;
+ editor: ReactEditor;
}
const TextLeaf = (props: TextLeafProps) => {
- const { attributes, children, leaf } = props;
-
+ const { attributes, children, leaf, isCodeBlock, editor } = props;
const ref = useRef(null);
let newChildren = children;
- if (leaf.bold) {
- newChildren = {children};
- }
-
- if (leaf.italic) {
- newChildren = {newChildren};
- }
-
- if (leaf.underline) {
- newChildren = {newChildren};
- }
if (leaf.code) {
newChildren = (
@@ -45,12 +43,46 @@ const TextLeaf = (props: TextLeafProps) => {
);
}
+ const getSelection = useCallback(
+ (node: Element) => {
+ const slateNode = ReactEditor.toSlateNode(editor, node);
+ const path = ReactEditor.findPath(editor, slateNode);
+ const selection = converToIndexLength(editor, {
+ anchor: { path, offset: 0 },
+ focus: { path, offset: leaf.text.length },
+ });
+ return selection;
+ },
+ [editor, leaf]
+ );
+
+ if (leaf.href) {
+ newChildren = (
+
+ {newChildren}
+
+ );
+ }
+
const className = [
+ isCodeBlock && 'token',
+ leaf.prism_token && leaf.prism_token,
leaf.strikethrough && 'line-through',
leaf.selection_high_lighted && 'bg-main-secondary',
+ leaf.link_selection_lighted && 'text-link bg-main-secondary',
leaf.code && 'inline-code',
+ leaf.bold && 'font-bold',
+ leaf.italic && 'italic',
+ leaf.underline && 'underline',
].filter(Boolean);
+ if (leaf.link_placeholder && leaf.text) {
+ newChildren = (
+
+ {newChildren}
+
+ );
+ }
return (
{newChildren}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useEditor.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useEditor.ts
index 66517b6143..9becdd81b8 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useEditor.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useEditor.ts
@@ -9,7 +9,7 @@ import {
indent,
outdent,
} from '$app/utils/document/slate_editor';
-import { focusNodeByIndex } from '$app/utils/document/node';
+import { focusNodeByIndex, getWordIndices } from '$app/utils/document/node';
import { Keyboard } from '$app/constants/document/keyboard';
import Delta from 'quill-delta';
import isHotkey from 'is-hotkey';
@@ -20,13 +20,13 @@ export function useEditor({
onSelectionChange,
selection,
value: delta,
- lastSelection,
+ decorateSelection,
onKeyDown,
isCodeBlock,
+ linkDecorateSelection,
}: EditorProps) {
- const editor = useSlateYjs({ delta });
+ const { editor } = useSlateYjs({ delta });
const ref = useRef(null);
-
const newValue = useMemo(() => [], []);
const onSelectionChangeHandler = useCallback(
(slateSelection: Selection) => {
@@ -42,7 +42,7 @@ export function useEditor({
onChange?.(convertToDelta(slateValue), oldContents);
onSelectionChangeHandler(editor.selection);
},
- [delta, editor.selection, onChange, onSelectionChangeHandler]
+ [delta, editor, onChange, onSelectionChangeHandler]
);
const onDOMBeforeInput = useCallback((e: InputEvent) => {
@@ -54,27 +54,50 @@ export function useEditor({
}
}, []);
+ const getDecorateRange = useCallback(
+ (
+ path: number[],
+ selection:
+ | {
+ index: number;
+ length: number;
+ }
+ | undefined,
+ value: Record
+ ) => {
+ if (!selection) return null;
+ const range = convertToSlateSelection(selection.index, selection.length, editor.children) as BaseRange;
+ if (range && !Range.isCollapsed(range)) {
+ const intersection = Range.intersection(range, Editor.range(editor, path));
+ if (intersection) {
+ return {
+ ...intersection,
+ ...value,
+ };
+ }
+ }
+ return null;
+ },
+ [editor]
+ );
+
const decorate = useCallback(
(entry: NodeEntry) => {
const [node, path] = entry;
- if (!lastSelection) return [];
- const slateSelection = convertToSlateSelection(lastSelection.index, lastSelection.length, editor.children);
- if (slateSelection && !Range.isCollapsed(slateSelection as BaseRange)) {
- const intersection = Range.intersection(slateSelection, Editor.range(editor, path));
- if (!intersection) {
- return [];
- }
- const range = {
+ const ranges: Range[] = [
+ getDecorateRange(path, decorateSelection, {
selection_high_lighted: true,
- ...intersection,
- };
+ }),
+ getDecorateRange(path, linkDecorateSelection?.selection, {
+ link_selection_lighted: true,
+ link_placeholder: linkDecorateSelection?.placeholder,
+ }),
+ ].filter((range) => range !== null) as Range[];
- return [range];
- }
- return [];
+ return ranges;
},
- [editor, lastSelection]
+ [decorateSelection, linkDecorateSelection, getDecorateRange]
);
const onKeyDownRewrite = useCallback(
@@ -116,14 +139,53 @@ export function useEditor({
[editor]
);
+ // This is a hack to fix the bug that the editor decoration is updated cause selection is lost
+ const onMouseDownCapture = useCallback(
+ (event: React.MouseEvent) => {
+ editor.deselect();
+ requestAnimationFrame(() => {
+ const range = document.caretRangeFromPoint(event.clientX, event.clientY);
+ if (!range) return;
+ const selection = window.getSelection();
+ if (!selection) return;
+ selection.removeAllRanges();
+ selection.addRange(range);
+ });
+ },
+ [editor]
+ );
+
+ // double click to select a word
+ // This is a hack to fix the bug that mouse down event deselect the selection
+ const onDoubleClick = useCallback((event: React.MouseEvent) => {
+ const selection = window.getSelection();
+ if (!selection) return;
+ const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
+ if (!range) return;
+ const node = range.startContainer;
+ const offset = range.startOffset;
+ const wordIndices = getWordIndices(node, offset);
+ if (wordIndices.length === 0) return;
+ range.setStart(node, wordIndices[0].startIndex);
+ range.setEnd(node, wordIndices[0].endIndex);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }, []);
+
useEffect(() => {
- if (!selection || !ref.current) return;
+ if (!ref.current) return;
+ const isFocused = ReactEditor.isFocused(editor);
+ if (!selection) {
+ isFocused && editor.deselect();
+ return;
+ }
const slateSelection = convertToSlateSelection(selection.index, selection.length, editor.children);
if (!slateSelection) return;
- const isFocused = ReactEditor.isFocused(editor);
if (isFocused && JSON.stringify(slateSelection) === JSON.stringify(editor.selection)) return;
- focusNodeByIndex(ref.current, selection.index, selection.length);
- Transforms.select(editor, slateSelection);
+ const isSuccess = focusNodeByIndex(ref.current, selection.index, selection.length);
+ if (!isSuccess) {
+ Transforms.select(editor, slateSelection);
+ }
}, [editor, selection]);
return {
@@ -135,5 +197,7 @@ export function useEditor({
ref,
onKeyDown: onKeyDownRewrite,
onBlur,
+ onMouseDownCapture,
+ onDoubleClick,
};
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useSlateYjs.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useSlateYjs.ts
index f18384d0f1..8bf42dcf05 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useSlateYjs.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useSlateYjs.ts
@@ -1,5 +1,5 @@
import Delta from 'quill-delta';
-import { useEffect, useMemo, useRef } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import * as Y from 'yjs';
import { convertToSlateValue } from '$app/utils/document/slate_editor';
import { slateNodesToInsertDelta, withYjs, YjsEditor } from '@slate-yjs/core';
@@ -7,14 +7,14 @@ import { withReact } from 'slate-react';
import { createEditor } from 'slate';
export function useSlateYjs({ delta }: { delta?: Delta }) {
- const yTextRef = useRef();
+ const [yText, setYText] = useState(undefined);
const sharedType = useMemo(() => {
const yDoc = new Y.Doc();
const sharedType = yDoc.get('content', Y.XmlText) as Y.XmlText;
const value = convertToSlateValue(delta || new Delta());
const insertDelta = slateNodesToInsertDelta(value);
sharedType.applyDelta(insertDelta);
- yTextRef.current = insertDelta[0].insert as Y.Text;
+ setYText(insertDelta[0].insert as Y.Text);
return sharedType;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -25,19 +25,17 @@ export function useSlateYjs({ delta }: { delta?: Delta }) {
useEffect(() => {
YjsEditor.connect(editor);
return () => {
- yTextRef.current = undefined;
YjsEditor.disconnect(editor);
};
}, [editor]);
useEffect(() => {
- const yText = yTextRef.current;
if (!yText) return;
const oldContents = new Delta(yText.toDelta());
const diffDelta = oldContents.diff(delta || new Delta());
if (diffDelta.ops.length === 0) return;
yText.applyDelta(diffDelta.ops);
- }, [delta, editor]);
+ }, [delta, editor, yText]);
- return editor;
+ return { editor };
}
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 a4aaf27920..fd966fdba8 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
@@ -1,5 +1,5 @@
import { store, useAppSelector } from '@/appflowy_app/stores/store';
-import { useEffect, useMemo, useRef } from 'react';
+import { createContext, useEffect, useMemo, useRef } from 'react';
import { Node } from '$app/interfaces/document';
/**
@@ -35,3 +35,5 @@ export function useSubscribeNode(id: string) {
export function getBlock(id: string) {
return store.getState().document.nodes[id];
}
+
+export const NodeIdContext = createContext('');
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeSelection.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeSelection.hooks.ts
index 27aa9bd1ee..10438647e7 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeSelection.hooks.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeSelection.hooks.ts
@@ -2,6 +2,25 @@ import { useAppSelector } from '$app/stores/store';
import { RangeState, RangeStatic } from '$app/interfaces/document';
import { useMemo, useRef } from 'react';
+export function useSubscribeDecorate(id: string) {
+ const decorateSelection = useAppSelector((state) => {
+ return state.documentRange.ranges[id];
+ });
+
+ const linkDecorateSelection = useAppSelector((state) => {
+ const linkPopoverState = state.documentLinkPopover;
+ if (!linkPopoverState.open || linkPopoverState.id !== id) return;
+ return {
+ selection: linkPopoverState.selection,
+ placeholder: linkPopoverState.title,
+ };
+ });
+
+ return {
+ decorateSelection,
+ linkDecorateSelection,
+ };
+}
export function useFocused(id: string) {
const caretRef = useRef();
const focusCaret = useAppSelector((state) => {
@@ -13,23 +32,14 @@ export function useFocused(id: string) {
return null;
});
- const lastSelection = useAppSelector((state) => {
- return state.documentRange.ranges[id];
- });
-
const focused = useMemo(() => {
return focusCaret && focusCaret?.id === id;
}, [focusCaret, id]);
- const memoizedLastSelection = useMemo(() => {
- return lastSelection;
- }, [JSON.stringify(lastSelection)]);
-
return {
focused,
caretRef,
focusCaret,
- lastSelection: memoizedLastSelection,
};
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/EditLink.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/EditLink.tsx
new file mode 100644
index 0000000000..94da916a1a
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/EditLink.tsx
@@ -0,0 +1,38 @@
+import React, { useEffect, useState } from 'react';
+
+function EditLink({
+ autoFocus,
+ text,
+ value,
+ onChange,
+}: {
+ autoFocus?: boolean;
+ text: string;
+ value: string;
+ onChange?: (newValue: string) => void;
+}) {
+ const [val, setVal] = useState(value);
+
+ useEffect(() => {
+ onChange?.(val);
+ }, [val, onChange]);
+
+ return (
+
+
{text}
+
+ {
+ const newValue = e.target.value;
+ setVal(newValue);
+ }}
+ value={val}
+ />
+
+
+ );
+}
+
+export default EditLink;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/EditLinkToolbar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/EditLinkToolbar.tsx
new file mode 100644
index 0000000000..8455635b28
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/EditLinkToolbar.tsx
@@ -0,0 +1,89 @@
+import React, { useEffect, useRef } from 'react';
+import BlockPortal from '$app/components/document/BlockPortal';
+import { getNode } from '$app/utils/document/node';
+import LanguageIcon from '@mui/icons-material/Language';
+import CopyIcon from '@mui/icons-material/CopyAll';
+import { copyText } from '$app/utils/document/copy_paste';
+import { useMessage } from '$app/components/document/_shared/Message';
+
+const iconSize = {
+ width: '1rem',
+ height: '1rem',
+};
+function EditLinkToolbar({
+ blockId,
+ linkElement,
+ onMouseEnter,
+ onMouseLeave,
+ href,
+ editing,
+ onEdit,
+}: {
+ blockId: string;
+ linkElement: HTMLAnchorElement;
+ href: string;
+ onMouseEnter: () => void;
+ onMouseLeave: () => void;
+ editing: boolean;
+ onEdit: () => void;
+}) {
+ const { show, contentHolder } = useMessage();
+ const ref = useRef(null);
+ useEffect(() => {
+ const toolbarDom = ref.current;
+ if (!toolbarDom) return;
+
+ const linkRect = linkElement.getBoundingClientRect();
+ const node = getNode(blockId);
+ if (!node) return;
+ const nodeRect = node.getBoundingClientRect();
+ const top = linkRect.top - nodeRect.top + linkRect.height + 4;
+ const left = linkRect.left - nodeRect.left;
+ toolbarDom.style.top = `${top}px`;
+ toolbarDom.style.left = `${left}px`;
+ toolbarDom.style.opacity = '1';
+ });
+ return (
+ <>
+ {editing && (
+
+
+
+
+
+
+
{href}
+
{
+ try {
+ await copyText(href);
+ show({ message: 'Copied!', duration: 6000 });
+ } catch {
+ show({ message: 'Copy failed!', duration: 6000 });
+ }
+ }}
+ className={'mr-2 cursor-pointer'}
+ >
+
+
+
+ Edit
+
+
+
+
+ )}
+ {contentHolder}
+ >
+ );
+}
+
+export default EditLinkToolbar;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/LinkButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/LinkButton.tsx
new file mode 100644
index 0000000000..369dd25867
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/LinkButton.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import Button from '@mui/material/Button';
+import { DeleteOutline } from '@mui/icons-material';
+
+function LinkButton({ icon, title, onClick }: { icon: React.ReactNode; title: string; onClick: () => void }) {
+ return (
+
+
+
+ );
+}
+
+export default LinkButton;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/LinkEditPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/LinkEditPopover.tsx
new file mode 100644
index 0000000000..069090e861
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/LinkEditPopover.tsx
@@ -0,0 +1,131 @@
+import React, { useCallback, useContext } from 'react';
+import Popover from '@mui/material/Popover';
+import { Divider } from '@mui/material';
+import { DeleteOutline, Done } from '@mui/icons-material';
+import EditLink from '$app/components/document/_shared/TextLink/EditLink';
+import { useAppDispatch, useAppSelector } from '$app/stores/store';
+import { linkPopoverActions, rangeActions } from '$app_reducers/document/slice';
+import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
+import { updateLinkThunk } from '$app_reducers/document/async-actions';
+import { formatLinkThunk } from '$app_reducers/document/async-actions/link';
+import LinkButton from '$app/components/document/_shared/TextLink/LinkButton';
+
+function LinkEditPopover() {
+ const dispatch = useAppDispatch();
+ const controller = useContext(DocumentControllerContext);
+ const popoverState = useAppSelector((state) => state.documentLinkPopover);
+ const { anchorPosition, id, selection, title = '', href = '', open = false } = popoverState;
+
+ const onClose = useCallback(() => {
+ dispatch(linkPopoverActions.closeLinkPopover());
+ }, [dispatch]);
+
+ const onExited = useCallback(() => {
+ if (!id || !selection) return;
+ const newSelection = {
+ index: selection.index,
+ length: title.length,
+ };
+ dispatch(
+ rangeActions.setRange({
+ id,
+ rangeStatic: newSelection,
+ })
+ );
+ dispatch(
+ rangeActions.setCaret({
+ id,
+ ...newSelection,
+ })
+ );
+ }, [id, selection, title, dispatch]);
+
+ const onChange = useCallback(
+ (newVal: { href?: string; title: string }) => {
+ if (!id) return;
+ if (newVal.title === title && newVal.href === href) return;
+ dispatch(
+ updateLinkThunk({
+ id,
+ href: newVal.href,
+ title: newVal.title,
+ })
+ );
+ },
+ [dispatch, href, id, title]
+ );
+
+ const onDone = useCallback(async () => {
+ if (!controller) return;
+ await dispatch(
+ formatLinkThunk({
+ controller,
+ })
+ );
+ onClose();
+ }, [controller, dispatch, onClose]);
+
+ return (
+ e.stopPropagation()}
+ open={open}
+ disableAutoFocus={true}
+ anchorReference='anchorPosition'
+ anchorPosition={anchorPosition}
+ TransitionProps={{
+ onExited,
+ }}
+ onClose={onClose}
+ anchorOrigin={{
+ vertical: 'bottom',
+ horizontal: 'center',
+ }}
+ transformOrigin={{
+ vertical: 'top',
+ horizontal: 'center',
+ }}
+ PaperProps={{
+ sx: {
+ width: 500,
+ },
+ }}
+ >
+
+
{
+ onChange({
+ href: link,
+ title,
+ });
+ }}
+ />
+
+ onChange({
+ href,
+ title: text,
+ })
+ }
+ />
+
+ }
+ onClick={() => {
+ onChange({
+ title,
+ });
+ onDone();
+ }}
+ />
+ } onClick={onDone} />
+
+
+ );
+}
+
+export default LinkEditPopover;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/LinkHighLight.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/LinkHighLight.tsx
new file mode 100644
index 0000000000..65f618a71b
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/LinkHighLight.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+function LinkHighLight({ children, leaf, title }: { leaf: { text: string }; title: string; children: React.ReactNode }) {
+ return (
+ <>
+ {leaf.text === title || isOverlappingPrefix(leaf.text, title) ? (
+ {title}
+ ) : null}
+
+
+ {children}
+
+ >
+ );
+}
+
+export default LinkHighLight;
+
+function isOverlappingPrefix(first: string, second: string): boolean {
+ if (first.length === 0 || second.length === 0) return false;
+ let i = 0;
+ while (i < first.length) {
+ const chars = first.substring(i);
+ if (chars.length > second.length) return false;
+ if (second.startsWith(chars)) return true;
+ i++;
+ }
+ return false;
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/TextLink.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/TextLink.hooks.ts
new file mode 100644
index 0000000000..7d9e0495e3
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/TextLink.hooks.ts
@@ -0,0 +1,27 @@
+import { useCallback, useMemo, useRef, useState } from 'react';
+import { debounce } from '$app/utils/tool';
+
+export function useTextLink(id: string) {
+ const [editing, setEditing] = useState(false);
+ const ref = useRef(null);
+
+ const show = useMemo(() => debounce(() => setEditing(true), 500), []);
+ const hide = useMemo(() => debounce(() => setEditing(false), 500), []);
+
+ const onMouseEnter = useCallback(() => {
+ hide.cancel();
+ show();
+ }, [hide, show]);
+
+ const onMouseLeave = useCallback(() => {
+ show.cancel();
+ hide();
+ }, [hide, show]);
+
+ return {
+ editing,
+ onMouseEnter,
+ onMouseLeave,
+ ref,
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/index.tsx
new file mode 100644
index 0000000000..29a4aa83b6
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextLink/index.tsx
@@ -0,0 +1,76 @@
+import React, { useCallback, useContext } from 'react';
+import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks';
+import { useTextLink } from '$app/components/document/_shared/TextLink/TextLink.hooks';
+import EditLinkToolbar from '$app/components/document/_shared/TextLink/EditLinkToolbar';
+import { useAppDispatch } from '$app/stores/store';
+import { linkPopoverActions } from '$app_reducers/document/slice';
+
+function TextLink({
+ getSelection,
+ title,
+ href,
+ children,
+}: {
+ getSelection: (node: Element) => {
+ index: number;
+ length: number;
+ } | null;
+ children: React.ReactNode;
+ href: string;
+ title: string;
+}) {
+ const blockId = useContext(NodeIdContext);
+ const { editing, ref, onMouseEnter, onMouseLeave } = useTextLink(blockId);
+ const dispatch = useAppDispatch();
+
+ const onEdit = useCallback(() => {
+ if (!ref.current) return;
+ const selection = getSelection(ref.current);
+ if (!selection) return;
+ const rect = ref.current?.getBoundingClientRect();
+ if (!rect) return;
+ dispatch(
+ linkPopoverActions.setLinkPopover({
+ anchorPosition: {
+ top: rect.top + rect.height,
+ left: rect.left + rect.width / 2,
+ },
+ id: blockId,
+ selection,
+ title,
+ href,
+ open: true,
+ })
+ );
+ }, [blockId, dispatch, getSelection, href, ref, title]);
+ if (!blockId) return null;
+
+ return (
+ <>
+
+ {children}
+
+ {ref.current && (
+
+ )}
+ >
+ );
+}
+
+export default TextLink;
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 2601d14ccd..2e868927df 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts
@@ -144,6 +144,7 @@ export const blockConfig: Record = {
export const defaultTextActionProps: TextActionMenuProps = {
customItems: [
TextAction.Turn,
+ TextAction.Link,
TextAction.Bold,
TextAction.Italic,
TextAction.Underline,
@@ -154,29 +155,24 @@ export const defaultTextActionProps: TextActionMenuProps = {
excludeItems: [],
};
+const groupKeys = {
+ comment: [],
+ format: [
+ TextAction.Bold,
+ TextAction.Italic,
+ TextAction.Underline,
+ TextAction.Strikethrough,
+ TextAction.Code,
+ TextAction.Equation,
+ ],
+ link: [TextAction.Link],
+ turn: [TextAction.Turn],
+};
+
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 multiLineTextActionGroups = [groupKeys.format];
-export const textActionGroups = [
- [TextAction.Turn],
- [
- TextAction.Bold,
- TextAction.Italic,
- TextAction.Underline,
- TextAction.Strikethrough,
- TextAction.Code,
- TextAction.Equation,
- ],
-];
+export const textActionGroups = [groupKeys.turn, groupKeys.format, groupKeys.link];
diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
index 1fd766ad43..6a0b337a6a 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
@@ -184,6 +184,7 @@ export enum TextAction {
Strikethrough = 'strikethrough',
Code = 'code',
Equation = 'equation',
+ Link = 'href',
}
export interface TextActionMenuProps {
/**
@@ -253,7 +254,14 @@ export interface EditorProps {
placeholder?: string;
value?: Delta;
selection?: RangeStaticNoId;
- lastSelection?: RangeStaticNoId;
+ decorateSelection?: RangeStaticNoId;
+ linkDecorateSelection?: {
+ selection?: {
+ index: number;
+ length: number;
+ };
+ placeholder?: string;
+ };
onSelectionChange?: (range: RangeStaticNoId | null, oldRange: RangeStaticNoId | null, source?: Sources) => void;
onChange?: (delta: Delta, oldDelta: Delta, source?: Sources) => void;
onKeyDown?: (event: React.KeyboardEvent) => void;
@@ -264,3 +272,15 @@ export interface BlockCopyData {
text: string;
html: string;
}
+
+export interface LinkPopoverState {
+ anchorPosition?: { top: number; left: number };
+ id?: string;
+ selection?: {
+ index: number;
+ length: number;
+ };
+ open?: boolean;
+ href?: string;
+ title?: string;
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts
index 44f677efa5..f1142856e5 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts
@@ -1,4 +1,4 @@
-import { DocumentState, BlockData } from '$app/interfaces/document';
+import { BlockData, DocumentState } from '$app/interfaces/document';
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { createAsyncThunk } from '@reduxjs/toolkit';
import Delta, { Op } from 'quill-delta';
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
index b60b06e8cc..a2474e3967 100644
--- 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
@@ -12,7 +12,7 @@ export const getFormatActiveThunk = createAsyncThunk(
const { document, documentRange } = state;
const { ranges } = documentRange;
const match = (delta: Delta, format: TextAction) => {
- return delta.ops.every((op) => op.attributes?.[format] === true);
+ return delta.ops.every((op) => op.attributes?.[format]);
};
return Object.entries(ranges).every(([id, range]) => {
const node = document.nodes[id];
@@ -36,15 +36,16 @@ export const toggleFormatThunk = createAsyncThunk(
const { payload: active } = await dispatch(getFormatActiveThunk(format));
isActive = !!active;
}
+ const formatValue = isActive ? undefined : true;
const state = getState() as RootState;
const { document } = state;
const { ranges } = state.documentRange;
- const toggle = (delta: Delta, format: TextAction) => {
+ const toggle = (delta: Delta, format: TextAction, value: string | boolean | undefined) => {
const newOps = delta.ops.map((op) => {
const attributes = {
...op.attributes,
- [format]: isActive ? undefined : true,
+ [format]: value,
};
return {
insert: op.insert,
@@ -62,7 +63,7 @@ export const toggleFormatThunk = createAsyncThunk(
const beforeDelta = delta.slice(0, index);
const afterDelta = delta.slice(index + length);
const rangeDelta = delta.slice(index, index + length);
- const toggleFormatDelta = toggle(rangeDelta, format);
+ const toggleFormatDelta = toggle(rangeDelta, format, formatValue);
const newDelta = beforeDelta.concat(toggleFormatDelta).concat(afterDelta);
return controller.getUpdateAction({
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/index.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/index.ts
index 73eb214085..9813c45c34 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/index.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/index.ts
@@ -2,3 +2,4 @@ export * from './blocks';
export * from './turn_to';
export * from './keydown';
export * from './range';
+export { updateLinkThunk } from '$app_reducers/document/async-actions/link';
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/keydown.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/keydown.ts
index a6c8d51561..5c04afd6f5 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/keydown.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/keydown.ts
@@ -60,6 +60,7 @@ export const backspaceDeleteActionForBlockThunk = createAsyncThunk(
controller,
})
);
+ dispatch(rangeActions.clearRange());
dispatch(rangeActions.setCaret(caret));
return;
}
@@ -99,7 +100,6 @@ export const enterActionForBlockThunk = createAsyncThunk(
const children = state.document.children[node.children];
const needMoveChildren = blockConfig[node.type].splitProps?.nextLineRelationShip === SplitRelationship.NextSibling;
- console.log('needMoveChildren', needMoveChildren);
const moveChildrenAction = needMoveChildren
? controller.getMoveChildrenAction(
children.map((id) => state.document.nodes[id]),
@@ -150,6 +150,7 @@ export const upDownActionForBlockThunk = createAsyncThunk(
if (!newCaret) {
return;
}
+ dispatch(rangeActions.clearRange());
dispatch(rangeActions.setCaret(newCaret));
}
);
@@ -193,6 +194,7 @@ export const leftActionForBlockThunk = createAsyncThunk(
if (!newCaret) {
return;
}
+ dispatch(rangeActions.clearRange());
dispatch(rangeActions.setCaret(newCaret));
}
);
@@ -238,6 +240,8 @@ export const rightActionForBlockThunk = createAsyncThunk(
if (!newCaret) {
return;
}
+ dispatch(rangeActions.clearRange());
+
dispatch(rangeActions.setCaret(newCaret));
}
);
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/link.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/link.ts
new file mode 100644
index 0000000000..fcb540a9c5
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/link.ts
@@ -0,0 +1,103 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { DocumentController } from '$app/stores/effects/document/document_controller';
+import Delta from 'quill-delta';
+import { linkPopoverActions, rangeActions } from '$app_reducers/document/slice';
+import { RootState } from '$app/stores/store';
+
+export const formatLinkThunk = createAsyncThunk<
+ boolean,
+ {
+ controller: DocumentController;
+ }
+>('document/formatLink', async (payload, thunkAPI) => {
+ const { controller } = payload;
+ const { getState } = thunkAPI;
+ const state = getState() as RootState;
+ const linkPopover = state.documentLinkPopover;
+ if (!linkPopover) return false;
+ const { selection, id, href, title = '' } = linkPopover;
+ if (!selection || !id) return false;
+ const document = state.document;
+ const node = document.nodes[id];
+ const nodeDelta = new Delta(node.data?.delta);
+ const index = selection.index || 0;
+ const length = selection.length || 0;
+ const regex = new RegExp(/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/);
+ if (href !== undefined && !regex.test(href)) {
+ return false;
+ }
+
+ const diffDelta = new Delta().retain(index).delete(length).insert(title, {
+ href,
+ });
+
+ const newDelta = nodeDelta.compose(diffDelta);
+
+ const updateAction = controller.getUpdateAction({
+ ...node,
+ data: {
+ ...node.data,
+ delta: newDelta.ops,
+ },
+ });
+ await controller.applyActions([updateAction]);
+ return true;
+});
+
+export const updateLinkThunk = createAsyncThunk<
+ void,
+ {
+ id: string;
+ href?: string;
+ title: string;
+ }
+>('document/updateLink', async (payload, thunkAPI) => {
+ const { id, href, title } = payload;
+ const { dispatch } = thunkAPI;
+
+ dispatch(
+ linkPopoverActions.updateLinkPopover({
+ id,
+ href,
+ title,
+ })
+ );
+});
+
+export const newLinkThunk = createAsyncThunk('document/newLink', async (payload, thunkAPI) => {
+ const { getState, dispatch } = thunkAPI;
+ const { documentRange, document } = getState() as RootState;
+
+ const { caret } = documentRange;
+ if (!caret) return;
+ const { index, length, id } = caret;
+
+ const block = document.nodes[id];
+ const delta = new Delta(block.data.delta).slice(index, index + length);
+ const op = delta.ops.find((op) => op.attributes?.href);
+ const href = op?.attributes?.href as string;
+
+ const domSelection = window.getSelection();
+ if (!domSelection) return;
+ const domRange = domSelection.rangeCount > 0 ? domSelection.getRangeAt(0) : null;
+ if (!domRange) return;
+ const title = domSelection.toString();
+ const { top, left, height, width } = domRange.getBoundingClientRect();
+ dispatch(rangeActions.clearRange());
+ dispatch(
+ linkPopoverActions.setLinkPopover({
+ anchorPosition: {
+ top: top + height,
+ left: left + width / 2,
+ },
+ id,
+ selection: {
+ index,
+ length,
+ },
+ title,
+ href,
+ open: true,
+ })
+ );
+});
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range.ts
index 8892ec0891..ecd567249e 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range.ts
@@ -104,8 +104,7 @@ export const deleteRangeAndInsertThunk = createAsyncThunk(
const { getState, dispatch } = thunkAPI;
const state = getState() as RootState;
const rangeState = state.documentRange;
- // if no range, just return
- if (rangeState.caret && rangeState.caret.length === 0) return;
+
const actions = [];
// get merge actions
const mergeActions = getMergeEndDeltaToStartActionsByRange(state, controller, insertDelta);
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 5262b0e4ee..71d27b93c6 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
@@ -5,6 +5,7 @@ import {
SlashCommandState,
RangeState,
RangeStatic,
+ LinkPopoverState,
} from '@/appflowy_app/interfaces/document';
import { BlockEventPayloadPB } from '@/services/backend';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
@@ -29,6 +30,8 @@ const slashCommandInitialState: SlashCommandState = {
isSlashCommand: false,
};
+const linkPopoverState: LinkPopoverState = {};
+
export const documentSlice = createSlice({
name: 'document',
initialState: initialState,
@@ -158,7 +161,11 @@ export const rangeSlice = createSlice({
setDragging: (state, action: PayloadAction) => {
state.isDragging = action.payload;
},
- setCaret: (state, action: PayloadAction) => {
+ setCaret: (state, action: PayloadAction) => {
+ if (!action.payload) {
+ state.caret = undefined;
+ return;
+ }
const id = action.payload.id;
state.ranges[id] = {
index: action.payload.index,
@@ -167,10 +174,7 @@ export const rangeSlice = createSlice({
state.caret = action.payload;
},
clearRange: (state, _: PayloadAction) => {
- state.isDragging = false;
- state.ranges = {};
- state.anchor = undefined;
- state.focus = undefined;
+ return rangeInitialState;
},
},
});
@@ -197,14 +201,46 @@ export const slashCommandSlice = createSlice({
},
});
+export const linkPopoverSlice = createSlice({
+ name: 'documentLinkPopover',
+ initialState: linkPopoverState,
+ reducers: {
+ setLinkPopover: (state, action: PayloadAction) => {
+ return {
+ ...state,
+ ...action.payload,
+ };
+ },
+ updateLinkPopover: (state, action: PayloadAction) => {
+ const { id } = action.payload;
+ if (!state.open || state.id !== id) return;
+ return {
+ ...state,
+ ...action.payload,
+ };
+ },
+ closeLinkPopover: (state, _: PayloadAction) => {
+ return {
+ ...state,
+ open: false,
+ };
+ },
+ resetLinkPopover: (state, _: PayloadAction) => {
+ return linkPopoverState;
+ },
+ },
+});
+
export const documentReducers = {
[documentSlice.name]: documentSlice.reducer,
[rectSelectionSlice.name]: rectSelectionSlice.reducer,
[rangeSlice.name]: rangeSlice.reducer,
[slashCommandSlice.name]: slashCommandSlice.reducer,
+ [linkPopoverSlice.name]: linkPopoverSlice.reducer,
};
export const documentActions = documentSlice.actions;
export const rectSelectionActions = rectSelectionSlice.actions;
export const rangeActions = rangeSlice.actions;
export const slashCommandActions = slashCommandSlice.actions;
+export const linkPopoverActions = linkPopoverSlice.actions;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/action.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/action.ts
index 888ba64f07..89e6779fd5 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/action.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/action.ts
@@ -57,7 +57,6 @@ export function getMiddleIdsByRange(rangeState: RangeState, document: DocumentSt
export function getAfterMergeCaretByRange(rangeState: RangeState, insertDelta?: Delta) {
const { anchor, focus, ranges } = rangeState;
if (!anchor || !focus) return;
- if (anchor.id === focus.id) return;
const isForward = anchor.point.y < focus.point.y;
const startId = isForward ? anchor.id : focus.id;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/copy_paste.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/copy_paste.ts
index 3004070b54..29a998663e 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/copy_paste.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/copy_paste.ts
@@ -80,3 +80,7 @@ export function getAppendBlockDeltaAction(
},
});
}
+
+export function copyText(text: string) {
+ return navigator.clipboard.writeText(text);
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/node.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/node.ts
index 28378d43a2..aa289a5c9e 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/node.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/node.ts
@@ -12,7 +12,7 @@ export function exclude(node: Element) {
return isPlaceholder;
}
-function findFirstTextNode(node: Node): Node | null {
+export function findFirstTextNode(node: Node): Node | null {
if (isTextNode(node)) {
return node;
}
@@ -45,7 +45,7 @@ export function setCursorAtStartOfNode(node: Node): void {
selection?.addRange(range);
}
-function findLastTextNode(node: Node): Node | null {
+export function findLastTextNode(node: Node): Node | null {
if (isTextNode(node)) {
return node;
}
@@ -174,7 +174,7 @@ export function findTextNode(
return { remainingIndex };
}
-export function focusNodeByIndex(node: Element, index: number, length: number) {
+export function getRangeByIndex(node: Element, index: number, length: number) {
const textBoxNode = node.querySelector(`[role="textbox"]`);
if (!textBoxNode) return;
const anchorNode = findTextNode(textBoxNode, index);
@@ -185,10 +185,16 @@ export function focusNodeByIndex(node: Element, index: number, length: number) {
const range = document.createRange();
range.setStart(anchorNode.node, anchorNode.offset || 0);
range.setEnd(focusNode.node, focusNode.offset || 0);
+ return range;
+}
+export function focusNodeByIndex(node: Element, index: number, length: number) {
+ const range = getRangeByIndex(node, index, length);
+ if (!range) return false;
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
+ return true;
}
export function getNodeTextBoxByBlockId(blockId: string) {
@@ -229,3 +235,31 @@ export function findParent(node: Element, parentSelector: string) {
}
return null;
}
+
+export function getWordIndices(startContainer: Node, startOffset: number) {
+ const textNode = startContainer;
+ const textContent = textNode.textContent || '';
+
+ const wordRegex = /\b\w+\b/g;
+ let match;
+ const wordIndices = [];
+
+ while ((match = wordRegex.exec(textContent)) !== null) {
+ const word = match[0];
+ const wordIndex = match.index;
+ const wordEndIndex = wordIndex + word.length;
+
+ // If the startOffset is greater than the wordIndex and less than the wordEndIndex, then the startOffset is
+ if (startOffset > wordIndex && startOffset <= wordEndIndex) {
+ wordIndices.push({
+ word: word,
+ startIndex: wordIndex,
+ endIndex: wordEndIndex,
+ });
+ break;
+ }
+ }
+
+ // If there are no matches, then the startOffset is greater than the last wordEndIndex
+ return wordIndices;
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts
index a024bfd208..93546f33b2 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts
@@ -9,7 +9,6 @@ export function calcToolbarPosition(toolbarDom: HTMLDivElement, node: Element, c
const nodeRect = node.getBoundingClientRect();
const rect = domRange?.getBoundingClientRect() || { top: 0, left: 0, width: 0, height: 0 };
-
const top = rect.top - nodeRect.top - toolbarDom.offsetHeight;
let left = rect.left - nodeRect.left - toolbarDom.offsetWidth / 2 + rect.width / 2;
@@ -25,7 +24,6 @@ export function calcToolbarPosition(toolbarDom: HTMLDivElement, node: Element, c
left = rightBound - toolbarDom.offsetWidth - nodeRect.left - rightThreshold;
}
-
return {
top,
left,
diff --git a/frontend/appflowy_tauri/src/styles/template.css b/frontend/appflowy_tauri/src/styles/template.css
index 5001b5650f..5887901b76 100644
--- a/frontend/appflowy_tauri/src/styles/template.css
+++ b/frontend/appflowy_tauri/src/styles/template.css
@@ -20,8 +20,8 @@ body {
@apply bg-[#E0F8FF]
}
-#appflowy-block-doc ::selection {
- @apply bg-[transparent]
+div[role="textbox"] ::selection {
+ @apply bg-transparent;
}
.btn {