diff --git a/apps/ligo-virgo/src/pages/AppRoutes.tsx b/apps/ligo-virgo/src/pages/AppRoutes.tsx
index c27ae6a76f..855f717b28 100644
--- a/apps/ligo-virgo/src/pages/AppRoutes.tsx
+++ b/apps/ligo-virgo/src/pages/AppRoutes.tsx
@@ -1,15 +1,9 @@
import { Navigate, Route, Routes } from 'react-router-dom';
import { Login } from './account';
-import Agenda from './agenda';
import { LigoVirgoRootContainer } from './AppContainer';
-import Recent from './recent';
import { RoutePrivate } from './RoutePrivate';
import { RoutePublicAutoLogin } from './RoutePublicAutoLogin';
-import Search from './search';
-import Settings from './settings';
-import Shared from './shared';
-import Starred from './starred';
import { PageNotFound } from './status/page-not-found';
import { WorkspaceNotFound } from './status/workspace-not-found';
import { Tools } from './tools';
@@ -25,13 +19,6 @@ export function LigoVirgoRoutes() {
path="/error/workspace"
element={}
/>
-
- } />
- } />
- } />
- } />
- } />
- } />
} />
Recent;
-}
diff --git a/apps/ligo-virgo/src/pages/search/index.tsx b/apps/ligo-virgo/src/pages/search/index.tsx
deleted file mode 100644
index 135fd58187..0000000000
--- a/apps/ligo-virgo/src/pages/search/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Search() {
- return Search;
-}
diff --git a/apps/ligo-virgo/src/pages/settings/index.tsx b/apps/ligo-virgo/src/pages/settings/index.tsx
deleted file mode 100644
index 803a6e29c8..0000000000
--- a/apps/ligo-virgo/src/pages/settings/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Settings() {
- return Settings;
-}
diff --git a/apps/ligo-virgo/src/pages/shared/index.tsx b/apps/ligo-virgo/src/pages/shared/index.tsx
deleted file mode 100644
index 051e879ca0..0000000000
--- a/apps/ligo-virgo/src/pages/shared/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Shared() {
- return Shared;
-}
diff --git a/apps/ligo-virgo/src/pages/starred/index.tsx b/apps/ligo-virgo/src/pages/starred/index.tsx
deleted file mode 100644
index d2f36fe762..0000000000
--- a/apps/ligo-virgo/src/pages/starred/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Starred() {
- return Starred;
-}
diff --git a/apps/ligo-virgo/src/pages/workspace/Home.tsx b/apps/ligo-virgo/src/pages/workspace/Home.tsx
index d0ac1f3872..3ae0da1da5 100644
--- a/apps/ligo-virgo/src/pages/workspace/Home.tsx
+++ b/apps/ligo-virgo/src/pages/workspace/Home.tsx
@@ -31,6 +31,7 @@ export function WorkspaceHome() {
})
);
}
+
navigate(`/${workspace_id}/${user_initial_page_id}`);
};
navigate_to_user_initial_page();
diff --git a/apps/ligo-virgo/src/pages/workspace/WorkspaceContainer.tsx b/apps/ligo-virgo/src/pages/workspace/WorkspaceContainer.tsx
index 8e83c601a0..eb424f88c7 100644
--- a/apps/ligo-virgo/src/pages/workspace/WorkspaceContainer.tsx
+++ b/apps/ligo-virgo/src/pages/workspace/WorkspaceContainer.tsx
@@ -1,12 +1,9 @@
-import { Route, Routes, useParams } from 'react-router';
-
import { useUserAndSpaces } from '@toeverything/datasource/state';
-
+import { Route, Routes, useParams } from 'react-router';
import { WorkspaceRootContainer } from './Container';
import { Page } from './docs';
import { Edgeless } from './Edgeless';
import { WorkspaceHome } from './Home';
-import Labels from './labels';
import Pages from './pages';
export function WorkspaceContainer() {
@@ -23,7 +20,6 @@ export function WorkspaceContainer() {
return (
}>
- } />
} />
{
- const matches = useMediaQuery('(max-width: 1024px)');
- const navigate = useNavigate();
- const { i18n } = useTranslation();
- const changeLanguage = (event: any) => {
- i18n.changeLanguage(event);
- };
+import 'github-markdown-css/github-markdown-light.css';
+
+import AboutText from './about.mdx';
+import { AFFiNEFooter, AFFiNEHeader, AFFiNEImage } from './common';
+import KeepUpdate from './keeupdate.png';
+
+export const AboutUs = () => {
return (
<>
diff --git a/apps/venus/src/app/App.tsx b/apps/venus/src/app/App.tsx
index df1e8be681..347e0cbf32 100644
--- a/apps/venus/src/app/App.tsx
+++ b/apps/venus/src/app/App.tsx
@@ -12,7 +12,7 @@ import { LogoIcon } from '@toeverything/components/icons';
import { useMediaQuery } from '@mui/material';
import CollaborationImage from './collaboration.png';
-import { AFFiNEFooter, AFFiNEHeader, AFFiNEImage } from './Common';
+import { AFFiNEFooter, AFFiNEHeader, AFFiNEImage } from './common';
import { GitHub } from './Icons';
import PageImage from './page.png';
import ShapeImage from './shape.png';
diff --git a/apps/venus/src/app/Common.tsx b/apps/venus/src/app/common/Footer.tsx
similarity index 78%
rename from apps/venus/src/app/Common.tsx
rename to apps/venus/src/app/common/Footer.tsx
index 87347f4fa1..f51d7f78ed 100644
--- a/apps/venus/src/app/Common.tsx
+++ b/apps/venus/src/app/common/Footer.tsx
@@ -1,26 +1,17 @@
+/* eslint-disable max-lines */
/* eslint-disable @typescript-eslint/naming-convention */
import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
import { Box, Button, Grid, Typography } from '@mui/joy';
-import Option from '@mui/joy/Option';
-import Select from '@mui/joy/Select';
-import { styled } from '@mui/joy/styles';
-// eslint-disable-next-line no-restricted-imports
-import { useMediaQuery } from '@mui/material';
import GitHubIcon from '@mui/icons-material/GitHub';
import RedditIcon from '@mui/icons-material/Reddit';
import TelegramIcon from '@mui/icons-material/Telegram';
-import { options } from './i18n';
-import { DiscordIcon, GitHub } from './Icons';
-// eslint-disable-next-line no-restricted-imports
-import LogoImage from './logo.png';
+import TwitterIcon from '@mui/icons-material/Twitter';
-export const AFFiNEImage = styled('img')({
- maxWidth: '100%',
- objectFit: 'contain',
-});
+import { DiscordIcon, GitHub } from '../Icons';
+import LogoImage from '../logo.png';
+import { AFFiNEImage } from './index';
export const AFFiNEFooter = ({
keepupdate = true,
@@ -199,6 +190,52 @@ export const AFFiNEFooter = ({
+
+
+
);
};
-
-export const AFFiNEHeader = () => {
- const matches = useMediaQuery('(max-width: 1024px)');
- const navigate = useNavigate();
- const { i18n } = useTranslation();
-
- const changeLanguage = (event: any) => {
- i18n.changeLanguage(event);
- };
- const matchesIPAD = useMediaQuery('(max-width: 768px)');
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/venus/src/app/common/Header.tsx b/apps/venus/src/app/common/Header.tsx
new file mode 100644
index 0000000000..a1fb6aa995
--- /dev/null
+++ b/apps/venus/src/app/common/Header.tsx
@@ -0,0 +1,95 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+
+import { Button, Grid } from '@mui/joy';
+import Option from '@mui/joy/Option';
+import Select from '@mui/joy/Select';
+// eslint-disable-next-line no-restricted-imports
+import { useMediaQuery } from '@mui/material';
+
+import { options } from '../i18n';
+import { GitHub } from '../Icons';
+
+export const AFFiNEHeader = () => {
+ const matches = useMediaQuery('(max-width: 1024px)');
+ const navigate = useNavigate();
+ const { i18n } = useTranslation();
+
+ const changeLanguage = (event: any) => {
+ i18n.changeLanguage(event);
+ };
+ const matchesIPAD = useMediaQuery('(max-width: 768px)');
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/venus/src/app/common/index.tsx b/apps/venus/src/app/common/index.tsx
new file mode 100644
index 0000000000..ea2457a837
--- /dev/null
+++ b/apps/venus/src/app/common/index.tsx
@@ -0,0 +1,10 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import { styled } from '@mui/joy/styles';
+
+export const AFFiNEImage = styled('img')({
+ maxWidth: '100%',
+ objectFit: 'contain',
+});
+
+export { AFFiNEFooter } from './Footer';
+export { AFFiNEHeader } from './Header';
diff --git a/libs/components/common/src/lib/index.ts b/libs/components/common/src/lib/index.ts
index f42927223e..cc94e2a4b4 100644
--- a/libs/components/common/src/lib/index.ts
+++ b/libs/components/common/src/lib/index.ts
@@ -19,6 +19,7 @@ declare module 'slate' {
}
}
+export { default as isUrl } from 'is-url';
export { BlockPreview, StyledBlockPreview } from './block-preview';
export { default as Button } from './button';
export { CollapsibleTitle } from './collapsible-title';
diff --git a/libs/components/common/src/lib/text/EditableText.tsx b/libs/components/common/src/lib/text/EditableText.tsx
index b2b2229fec..87c111544a 100644
--- a/libs/components/common/src/lib/text/EditableText.tsx
+++ b/libs/components/common/src/lib/text/EditableText.tsx
@@ -41,7 +41,12 @@ import { InlineDate, withDate } from './plugins/date';
import { DoubleLinkComponent } from './plugins/DoubleLink';
import { LinkComponent, LinkModal, withLinks, wrapLink } from './plugins/link';
import { InlineRefLink } from './plugins/reflink';
-import { Contents, isSelectAll, SlateUtils } from './slate-utils';
+import {
+ Contents,
+ isSelectAll,
+ SelectionStartAndEnd,
+ SlateUtils,
+} from './slate-utils';
import {
getCommentsIdsOnTextNode,
getExtraPropertiesFromEditorOutmostNode,
@@ -88,8 +93,12 @@ export interface TextProps {
/** Backspace event */
handleBackSpace?: ({
isCollAndStart,
+ splitContents,
+ selection,
}: {
isCollAndStart: boolean;
+ splitContents: Contents;
+ selection: SelectionStartAndEnd;
}) => boolean | undefined | Promise;
/** Whether markdown is supported */
supportMarkdown?: boolean;
@@ -464,7 +473,13 @@ export const Text = forwardRef((props, ref) => {
if (!isCool) {
hideInlineMenu && hideInlineMenu();
}
- preventBindIfNeeded(handleBackSpace)(e, { isCollAndStart });
+ const selection = utils.current.getSelectionStartAndEnd();
+ const splitContents = utils.current.getSplitContentsBySelection();
+ preventBindIfNeeded(handleBackSpace)(e, {
+ isCollAndStart,
+ selection,
+ splitContents,
+ });
};
const onTab = (e: KeyboardEvent) => {
diff --git a/libs/components/common/src/lib/text/plugins/DoubleLink.tsx b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx
index e4bcb9bec2..42f50740f3 100644
--- a/libs/components/common/src/lib/text/plugins/DoubleLink.tsx
+++ b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx
@@ -6,6 +6,7 @@ import { RenderElementProps } from 'slate-react';
export type DoubleLinkElement = {
type: 'link';
+ linkType: 'doubleLink';
workspaceId: string;
blockId: string;
children: Descendant[];
@@ -25,15 +26,20 @@ export const DoubleLinkComponent = (props: RenderElementProps) => {
[doubleLinkElement, navigate]
);
+ const displayValue = doubleLinkElement.children
+ .map((item: any) => item.text)
+ .join('');
+
return (
-
-
-
- {children}
+
+
+
+
+ {children}
+ {displayValue}
+
);
diff --git a/libs/components/common/src/lib/text/plugins/link.tsx b/libs/components/common/src/lib/text/plugins/link.tsx
index 619239c64d..971d8dc62b 100644
--- a/libs/components/common/src/lib/text/plugins/link.tsx
+++ b/libs/components/common/src/lib/text/plugins/link.tsx
@@ -1,44 +1,42 @@
-import React, {
- useEffect,
- useMemo,
- useRef,
- useState,
- useCallback,
- KeyboardEvent,
- MouseEvent,
- memo,
-} from 'react';
-import { createPortal } from 'react-dom';
-import { useNavigate } from 'react-router-dom';
-
-import isUrl from 'is-url';
-import style9 from 'style9';
-import {
- Editor,
- Transforms,
- Element as SlateElement,
- Descendant,
- Range as SlateRange,
- Node,
-} from 'slate';
-import { ReactEditor } from 'slate-react';
-import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import EditIcon from '@mui/icons-material/Edit';
import LinkOffIcon from '@mui/icons-material/LinkOff';
+import OpenInNewIcon from '@mui/icons-material/OpenInNew';
+import { LinkIcon } from '@toeverything/components/icons';
import {
MuiTooltip as Tooltip,
- styled,
muiTooltipClasses,
+ styled,
type MuiTooltipProps,
} from '@toeverything/components/ui';
import {
getRelativeUrlForInternalPageUrl,
isInternalPageUrl,
} from '@toeverything/utils';
-
+import isUrl from 'is-url';
+import React, {
+ KeyboardEvent,
+ memo,
+ MouseEvent,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import { createPortal } from 'react-dom';
+import { useNavigate } from 'react-router-dom';
+import {
+ Descendant,
+ Editor,
+ Element as SlateElement,
+ Node,
+ Range as SlateRange,
+ Transforms,
+} from 'slate';
+import { ReactEditor } from 'slate-react';
+import style9 from 'style9';
import { getRandomString } from '../utils';
-import { colors } from '../../colors';
-import { LinkIcon } from '@toeverything/components/icons';
+
export type LinkElement = {
type: 'link';
url: string;
@@ -47,13 +45,20 @@ export type LinkElement = {
};
export const withLinks = (editor: ReactEditor) => {
- const { isInline } = editor;
+ const { isInline, isVoid } = editor;
editor.isInline = element => {
// @ts-ignore
return element.type === 'link' ? true : isInline(element);
};
+ editor.isVoid = element => {
+ // @ts-ignore
+ return element.type === 'link' && element.linkType === 'doubleLink'
+ ? true
+ : isVoid(element);
+ };
+
return editor;
};
diff --git a/libs/components/common/src/lib/text/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts
index f44d573e64..2f7684b547 100644
--- a/libs/components/common/src/lib/text/slate-utils.ts
+++ b/libs/components/common/src/lib/text/slate-utils.ts
@@ -2,6 +2,7 @@
import {
Descendant,
Editor,
+ Element as SlateElement,
Location,
Node as SlateNode,
Path,
@@ -405,11 +406,11 @@ Editor.before = function (
if (element) {
if (isInlineAndVoid(editor, element)) {
// Inline entities need to be drilled out
- // target = Editor.before(editor, target);
- target = {
- path: [0, path[1] - 1],
- offset: 0,
- };
+ target = Editor.before(editor, target);
+ // target = {
+ // path: [0, path[1] - 1],
+ // offset: 0,
+ // };
} else if (editor.isInline(element) && !editor.isVoid(element)) {
// Inline styles such as hyperlinks need to drill directly into it
const inlineTextLength = element?.children?.[0]?.text?.length;
@@ -503,7 +504,7 @@ Editor.after = function (
return target;
};
-type SelectionStartAndEnd = {
+export type SelectionStartAndEnd = {
selectionStart: Point;
selectionEnd: Point;
};
@@ -517,6 +518,10 @@ export type Contents = {
content: Descendant[];
isEmpty: boolean;
};
+ contentSelection: {
+ content: Descendant[];
+ isEmpty: boolean;
+ };
};
class SlateUtils {
@@ -572,6 +577,7 @@ class SlateUtils {
anchor: point1,
focus: point2,
});
+
if (!fragment.length) {
console.error('Debug information:', point1, point2, fragment);
throw new Error('Failed to get content between!');
@@ -602,7 +608,7 @@ class SlateUtils {
for (let i = 0; i < fragmentChildren.length; i++) {
const child = fragmentChildren[i];
if ('type' in child && child.type === 'link') {
- i !== fragmentChildren.length - 1 && textChildren.push(child);
+ textChildren.push(child);
continue;
}
if (!('text' in child)) {
@@ -638,6 +644,10 @@ class SlateUtils {
content: this.getContentBetween(selectionEnd, end),
isEmpty: Point.equals(end, selectionEnd),
},
+ contentSelection: {
+ content: this.getContentBetween(selectionStart, selectionEnd),
+ isEmpty: false,
+ },
} as Contents;
}
@@ -680,10 +690,7 @@ class SlateUtils {
}
public getStart() {
- return Editor.start(this.editor, {
- path: [0, 0],
- offset: 0,
- });
+ return Editor.start(this.editor, [0]);
}
public getEnd() {
@@ -713,7 +720,10 @@ class SlateUtils {
if (!this.editor) {
return undefined;
}
- const { selectionEnd } = this.getSelectionStartAndEnd();
+ const selectionEnd = this.getSelectionStartAndEnd()?.selectionEnd;
+ if (!selectionEnd) {
+ return undefined;
+ }
return this.getStringBetween(this.getStart(), selectionEnd);
}
@@ -1252,6 +1262,57 @@ class SlateUtils {
});
}
+ public wrapLink(url: string, preSelection?: Location) {
+ if (!ReactEditor.isFocused(this.editor) && preSelection) {
+ Transforms.select(this.editor, preSelection);
+ }
+ if (this.isLinkActive()) {
+ this.unwrapLink();
+ }
+ const { selection } = this.editor;
+ const isCollapsed = selection && this.isCollapsed();
+ const link = {
+ type: 'link',
+ url: url,
+ children: isCollapsed ? [{ text: url }] : [],
+ id: getRandomString('link'),
+ };
+
+ if (isCollapsed) {
+ Transforms.insertNodes(this.editor, link);
+ } else {
+ Transforms.wrapNodes(this.editor, link, { split: true });
+ Transforms.collapse(this.editor, { edge: 'end' });
+ }
+ requestAnimationFrame(() => {
+ ReactEditor.focus(this.editor);
+ });
+ }
+
+ public isLinkActive() {
+ const [link] = Editor.nodes(this.editor, {
+ match: n =>
+ !Editor.isEditor(n) &&
+ SlateElement.isElement(n) &&
+ // @ts-expect-error
+ n.type === 'link',
+ });
+ return !!link;
+ }
+
+ public unwrapLink() {
+ Transforms.unwrapNodes(this.editor, {
+ match: n => {
+ return (
+ !Editor.isEditor(n) &&
+ SlateElement.isElement(n) &&
+ // @ts-expect-error
+ n.type === 'link'
+ );
+ },
+ });
+ }
+
/** todo improve if selection is collapsed */
public getCommentsIdsBySelection() {
const commentedTextNodes = Editor.nodes(this.editor, {
diff --git a/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx b/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx
index c45321a138..ddf1375428 100644
--- a/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx
+++ b/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx
@@ -132,25 +132,31 @@ export const NumberedView = ({ block, editor }: CreateView) => {
// Move children to new block
const children = await block.children();
- await block.removeChildren();
- const next_node = await editor.createBlock(
+ const nextBlock = await editor.createBlock(
Protocol.Block.Type.numbered
);
- if (!next_node) {
+ if (!nextBlock) {
throw new Error('Failed to create todo block');
}
- await next_node.append(...children);
- await next_node.setProperties({
+ if (editor.getRootBlockId() === block.id) {
+ // If the block is the root block,
+ // new block can not append as next sibling,
+ // all new blocks should be append as children.
+ await block.insert(0, [nextBlock]);
+ editor.selectionManager.activeNodeByNodeId(nextBlock.id);
+ return true;
+ }
+ await block.removeChildren();
+ await nextBlock.append(...children);
+ await nextBlock.setProperties({
text: { value: after } as ContentColumnValue,
});
await block.setProperties({
text: { value: before } as ContentColumnValue,
});
- await block.after(next_node);
-
- editor.selectionManager.activeNodeByNodeId(next_node.id);
-
+ await block.after(nextBlock);
+ editor.selectionManager.activeNodeByNodeId(nextBlock.id);
return true;
};
const on_tab: TextProps['handleTab'] = async ({ isShiftKey }) => {
diff --git a/libs/components/editor-blocks/src/blocks/todo/TodoView.tsx b/libs/components/editor-blocks/src/blocks/todo/TodoView.tsx
index 6c9b6634cd..26d3e1ba4d 100644
--- a/libs/components/editor-blocks/src/blocks/todo/TodoView.tsx
+++ b/libs/components/editor-blocks/src/blocks/todo/TodoView.tsx
@@ -88,13 +88,21 @@ export const TodoView = ({ block, editor }: CreateView) => {
Boolean
) as AsyncBlock[];
- const next_node = await editor.createBlock(Protocol.Block.Type.todo);
- if (!next_node) {
+ const nextBlock = await editor.createBlock(Protocol.Block.Type.todo);
+ if (!nextBlock) {
throw new Error('Failed to create todo block');
}
+ if (editor.getRootBlockId() === block.id) {
+ // If the block is the root block,
+ // new block can not append as next sibling,
+ // all new blocks should be append as children.
+ await block.insert(0, [nextBlock]);
+ editor.selectionManager.activeNodeByNodeId(nextBlock.id);
+ return true;
+ }
await block.removeChildren();
- await next_node.append(...children);
- await next_node.setProperties({
+ await nextBlock.append(...children);
+ await nextBlock.setProperties({
text: { value: after } as ContentColumnValue,
});
@@ -102,9 +110,9 @@ export const TodoView = ({ block, editor }: CreateView) => {
text: { value: before } as ContentColumnValue,
collapsed: { value: false },
});
- await block.after(next_node);
+ await block.after(nextBlock);
- editor.selectionManager.activeNodeByNodeId(next_node.id);
+ editor.selectionManager.activeNodeByNodeId(nextBlock.id);
return true;
};
diff --git a/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx b/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx
index e42a85c5ed..72faaef8a7 100644
--- a/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx
+++ b/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx
@@ -108,7 +108,7 @@ const findLowestCommonAncestor = async (
export const TextManage = forwardRef(
(props, ref) => {
- const { block, editor, ...otherOptions } = props;
+ const { block, editor, handleEnter, ...otherOptions } = props;
const defaultRef = useRef(null);
// Maybe there is a better way
const textRef =
@@ -446,6 +446,38 @@ export const TextManage = forwardRef(
}
}
};
+
+ const onTextEnter: TextProps['handleEnter'] = async props => {
+ const { splitContents } = props;
+ if (splitContents) {
+ const { contentBeforeSelection, contentAfterSelection } =
+ splitContents;
+ // after[after.length - 1];
+ if (!contentBeforeSelection.isEmpty) {
+ const beforeSelection = contentBeforeSelection.content;
+ const lastItem: any =
+ beforeSelection.length > 0
+ ? beforeSelection[beforeSelection.length - 1]
+ : null;
+ if (lastItem?.linkType === 'doubleLink') {
+ contentBeforeSelection.content.push({ text: '' });
+ }
+ }
+ if (!contentAfterSelection.isEmpty) {
+ const afterSelection = contentAfterSelection.content;
+ const firstItem: any =
+ afterSelection.length > 0 ? afterSelection[0] : null;
+ if (firstItem?.linkType === 'doubleLink') {
+ contentAfterSelection.content.splice(0, 0, {
+ text: '',
+ });
+ }
+ }
+ }
+
+ return handleEnter && handleEnter(props);
+ };
+
if (!properties || !properties.text) {
return <>>;
}
@@ -467,6 +499,7 @@ export const TextManage = forwardRef(
handleUndo={onUndo}
handleRedo={onRedo}
handleEsc={onKeyboardEsc}
+ handleEnter={onTextEnter}
{...otherOptions}
/>
);
diff --git a/libs/components/editor-core/src/editor/block/block-helper.ts b/libs/components/editor-core/src/editor/block/block-helper.ts
index b9e7c12af9..9e3090b527 100644
--- a/libs/components/editor-core/src/editor/block/block-helper.ts
+++ b/libs/components/editor-core/src/editor/block/block-helper.ts
@@ -5,13 +5,14 @@ import type {
} from '@toeverything/components/common';
import {
BaseRange,
+ Location,
Node,
Path,
Point,
Selection as SlateSelection,
} from 'slate';
-import { Editor } from '../editor';
import { AsyncBlock } from '../block';
+import { Editor } from '../editor';
import { SelectBlock } from '../selection';
type TextUtilsFunctions =
@@ -44,6 +45,7 @@ type TextUtilsFunctions =
| 'setSelection'
| 'insertNodes'
| 'getNodeByPath'
+ | 'wrapLink'
| 'getNodeByRange'
| 'convertLeaf2Html';
@@ -160,7 +162,7 @@ export class BlockHelper {
if (properties.text.value.length === 0) {
return properties;
}
- let text_value = properties.text.value;
+ const text_value = properties.text.value;
const {
text: { value: originTextValue, ...otherTextProperties },
@@ -517,4 +519,13 @@ export class BlockHelper {
console.warn('Could find the block text utils');
return undefined;
}
+
+ public wrapLink(blockId: string, url: string, preSelection?: Location) {
+ const text_utils = this._blockTextUtilsMap[blockId];
+ if (text_utils) {
+ return text_utils.wrapLink(url, preSelection);
+ }
+ console.warn('Could find the block text utils');
+ return undefined;
+ }
}
diff --git a/libs/components/editor-core/src/editor/types.ts b/libs/components/editor-core/src/editor/types.ts
index 86c795b615..9fe247fc08 100644
--- a/libs/components/editor-core/src/editor/types.ts
+++ b/libs/components/editor-core/src/editor/types.ts
@@ -13,8 +13,6 @@ import type {
BlockFlavors,
ReturnEditorBlock,
} from '@toeverything/datasource/db-service';
-import type { IdList, SelectionInfo, SelectionManager } from './selection';
-
import { Point } from '@toeverything/utils';
import { Observable } from 'rxjs';
import type { AsyncBlock } from './block';
@@ -23,6 +21,7 @@ import type { BlockCommands } from './commands/block-commands';
import type { DragDropManager } from './drag-drop';
import { MouseManager } from './mouse';
import { ScrollManager } from './scroll';
+import type { IdList, SelectionInfo, SelectionManager } from './selection';
// import { BrowserClipboard } from './clipboard/browser-clipboard';
@@ -114,6 +113,7 @@ export interface Virgo {
getGroupBlockByPoint: (point: Point) => Promise;
isEdgeless: boolean;
mouseManager: MouseManager;
+ getHooks: () => HooksRunner & PluginHooks;
}
export interface Plugin {
diff --git a/libs/components/editor-core/src/editor/views/base-view.ts b/libs/components/editor-core/src/editor/views/base-view.ts
index d4867f2b59..87bd55ebbc 100644
--- a/libs/components/editor-core/src/editor/views/base-view.ts
+++ b/libs/components/editor-core/src/editor/views/base-view.ts
@@ -4,7 +4,7 @@ import {
BlockDecoration,
MapOperation,
} from '@toeverything/datasource/jwt';
-import type { ComponentType, ReactElement } from 'react';
+import { ComponentType, ReactElement } from 'react';
import type { EventData } from '../block';
import { AsyncBlock } from '../block';
import { HTML2BlockResult } from '../clipboard';
@@ -108,7 +108,11 @@ export abstract class BaseView {
// Whether the component is empty
isEmpty(block: AsyncBlock): boolean {
const text = block.getProperty('text');
- const result = !text?.value?.[0]?.text;
+ // const result = !text?.value?.[0]?.text;
+ const result =
+ text?.value?.findIndex(
+ (item: any) => item.text || item.children?.length
+ ) === -1;
// Assert that the text is really empty
if (
diff --git a/libs/components/editor-core/src/render-block/Context.tsx b/libs/components/editor-core/src/render-block/Context.tsx
index e5c0197eee..9783aa2754 100644
--- a/libs/components/editor-core/src/render-block/Context.tsx
+++ b/libs/components/editor-core/src/render-block/Context.tsx
@@ -1,9 +1,10 @@
import { genErrorObj } from '@toeverything/utils';
-import { createContext, PropsWithChildren, useContext } from 'react';
+import type { ComponentType, PropsWithChildren } from 'react';
+import { createContext, useContext } from 'react';
import { RenderBlockProps } from './RenderBlock';
type BlockRenderProps = {
- blockRender: (args: RenderBlockProps) => JSX.Element;
+ blockRender: ComponentType;
};
export const BlockRenderContext = createContext(
diff --git a/libs/components/editor-plugins/src/index.ts b/libs/components/editor-plugins/src/index.ts
index 4c6ce9a1e1..568031f5a3 100644
--- a/libs/components/editor-plugins/src/index.ts
+++ b/libs/components/editor-plugins/src/index.ts
@@ -6,7 +6,7 @@ import {
GroupMenuPlugin,
InlineMenuPlugin,
LeftMenuPlugin,
- SelectionGroupPlugin,
+ LinkMenuPlugin,
} from './menu';
import { FullTextSearchPlugin } from './search';
import { TemplatePlugin } from './template';
@@ -24,4 +24,5 @@ export const plugins: PluginCreator[] = [
// SelectionGroupPlugin,
AddCommentPlugin,
GroupMenuPlugin,
+ LinkMenuPlugin,
];
diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/DoubleLinkMenu.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/DoubleLinkMenu.tsx
index a81e8b3122..90f5e10c43 100644
--- a/libs/components/editor-plugins/src/menu/double-link-menu/DoubleLinkMenu.tsx
+++ b/libs/components/editor-plugins/src/menu/double-link-menu/DoubleLinkMenu.tsx
@@ -107,14 +107,14 @@ export const DoubleLinkMenu = ({
icon: AddIcon,
},
});
- !inAddNewPage &&
- items.push({
- content: {
- id: ADD_NEW_PAGE,
- content: 'Add new page in...',
- icon: AddIcon,
- },
- });
+ // !inAddNewPage &&
+ // items.push({
+ // content: {
+ // id: ADD_NEW_PAGE,
+ // content: 'Add new page in...',
+ // icon: AddIcon,
+ // },
+ // });
return items;
}, [searchResultBlocks, inAddNewPage]);
@@ -137,7 +137,8 @@ export const DoubleLinkMenu = ({
const resetState = useCallback(
(preNodeId: string, nextNodeId: string) => {
- editor.blockHelper.removeDoubleLinkSearchSlash(preNodeId);
+ preNodeId &&
+ editor.blockHelper.removeDoubleLinkSearchSlash(preNodeId);
setCurBlockId(nextNodeId);
setSearchText('');
setIsOpen(true);
@@ -184,16 +185,19 @@ export const DoubleLinkMenu = ({
}
}
const { type, anchorNode } = editor.selection.currentSelectInfo;
+ if (!anchorNode) {
+ return;
+ }
if (
!isOpen ||
(type === 'Range' &&
- anchorNode &&
anchorNode.id !== curBlockId &&
editor.blockHelper.isSelectionCollapsed(anchorNode.id))
) {
- const text = editor.blockHelper.getBlockTextBeforeSelection(
- anchorNode.id
- );
+ const text =
+ editor.blockHelper.getBlockTextBeforeSelection(
+ anchorNode.id
+ ) || '';
if (text.endsWith('[[')) {
resetState(curBlockId, anchorNode.id);
}
@@ -206,7 +210,7 @@ export const DoubleLinkMenu = ({
}
}
},
- [editor, isOpen, curBlockId, hideMenu]
+ [editor, isOpen, curBlockId, hideMenu, resetState]
);
const handleKeyup = useCallback(
@@ -251,6 +255,20 @@ export const DoubleLinkMenu = ({
};
}, [handleKeyup, handleKeyDown, hooks]);
+ useEffect(() => {
+ const showDoubleLink = () => {
+ const { anchorNode } = editor.selection.currentSelectInfo;
+ editor.blockHelper.insertNodes(anchorNode.id, [{ text: '[[' }], {
+ select: true,
+ });
+ setTimeout(() => {
+ resetState('', anchorNode.id);
+ }, 0);
+ };
+ editor.plugins.observe('showDoubleLink', showDoubleLink);
+ return () => editor.plugins.unobserve('showDoubleLink', showDoubleLink);
+ }, [editor, resetState]);
+
const insertDoubleLink = useCallback(
async (pageId: string) => {
editor.blockHelper.setSelectDoubleLinkSearchSlash(curBlockId);
@@ -328,46 +346,48 @@ export const DoubleLinkMenu = ({
...doubleLinkMenuStyle,
}}
>
- hideMenu()}>
-
- {({ TransitionProps }) => (
-
-
- {inAddNewPage && (
-
-
-
- )}
-
-
-
- )}
-
-
+ {isOpen && (
+ hideMenu()}>
+
+ {({ TransitionProps }) => (
+
+
+ {inAddNewPage && (
+
+
+
+ )}
+
+
+
+ )}
+
+
+ )}
);
};
diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/Plugin.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/Plugin.tsx
index 3933bf5f1c..cb1a6e29df 100644
--- a/libs/components/editor-plugins/src/menu/double-link-menu/Plugin.tsx
+++ b/libs/components/editor-plugins/src/menu/double-link-menu/Plugin.tsx
@@ -3,7 +3,7 @@ import { BasePlugin } from '../../base-plugin';
import { PluginRenderRoot } from '../../utils';
import { DoubleLinkMenu } from './DoubleLinkMenu';
-const PLUGIN_NAME = 'reference-menu';
+const PLUGIN_NAME = 'doublelink-menu';
export class DoubleLinkMenuPlugin extends BasePlugin {
private _root?: PluginRenderRoot;
diff --git a/libs/components/editor-plugins/src/menu/index.ts b/libs/components/editor-plugins/src/menu/index.ts
index 190e3a5109..5c67719c87 100644
--- a/libs/components/editor-plugins/src/menu/index.ts
+++ b/libs/components/editor-plugins/src/menu/index.ts
@@ -4,4 +4,5 @@ export { GroupMenuPlugin } from './group-menu';
export { InlineMenuPlugin } from './inline-menu';
export { LeftMenuPlugin } from './left-menu/LeftMenuPlugin';
export { MENU_WIDTH as menuWidth } from './left-menu/menu-config';
+export { LinkMenuPlugin } from './link-menu';
export { SelectionGroupPlugin } from './selection-group-menu';
diff --git a/libs/components/editor-plugins/src/menu/inline-menu/menu-item/IconItem.tsx b/libs/components/editor-plugins/src/menu/inline-menu/menu-item/IconItem.tsx
index c49efd82a9..bd3a13bf8d 100644
--- a/libs/components/editor-plugins/src/menu/inline-menu/menu-item/IconItem.tsx
+++ b/libs/components/editor-plugins/src/menu/inline-menu/menu-item/IconItem.tsx
@@ -1,14 +1,10 @@
-import React, { useCallback } from 'react';
-import style9 from 'style9';
-
import { Tooltip } from '@toeverything/components/ui';
import { uaHelper } from '@toeverything/utils';
-import {
- inlineMenuNamesKeys,
- MacInlineMenuShortcuts,
- WinInlineMenuShortcuts,
-} from '../config';
+import React, { useCallback } from 'react';
+import style9 from 'style9';
+import { inlineMenuNamesKeys, WinInlineMenuShortcuts } from '../config';
import type { IconItemType, WithEditorSelectionType } from '../types';
+
type MenuIconItemProps = IconItemType & WithEditorSelectionType;
export const MenuIconItem = ({
@@ -27,6 +23,7 @@ export const MenuIconItem = ({
editor,
type: nameKey,
anchorNodeId: selectionInfo?.anchorNode?.id,
+ setShow,
});
}
if ([inlineMenuNamesKeys.comment].includes(nameKey)) {
diff --git a/libs/components/editor-plugins/src/menu/inline-menu/types.ts b/libs/components/editor-plugins/src/menu/inline-menu/types.ts
index bdd7bee447..124c7a03ee 100644
--- a/libs/components/editor-plugins/src/menu/inline-menu/types.ts
+++ b/libs/components/editor-plugins/src/menu/inline-menu/types.ts
@@ -1,5 +1,5 @@
import type { SvgIconProps } from '@toeverything/components/ui';
-import type { Virgo, SelectionInfo } from '@toeverything/framework/virgo';
+import type { SelectionInfo, Virgo } from '@toeverything/framework/virgo';
import { inlineMenuNames, INLINE_MENU_UI_TYPES } from './config';
export type WithEditorSelectionType = {
@@ -14,10 +14,12 @@ export type ClickItemHandler = ({
type,
editor,
anchorNodeId,
+ setShow,
}: {
type: InlineMenuNamesType;
editor: Virgo;
anchorNodeId: string;
+ setShow?: React.Dispatch>;
}) => void;
export type IconItemType = {
diff --git a/libs/components/editor-plugins/src/menu/inline-menu/utils.ts b/libs/components/editor-plugins/src/menu/inline-menu/utils.ts
index 78f03fbcf9..bb35d31f5a 100644
--- a/libs/components/editor-plugins/src/menu/inline-menu/utils.ts
+++ b/libs/components/editor-plugins/src/menu/inline-menu/utils.ts
@@ -107,6 +107,7 @@ const common_handler_for_inline_menu: ClickItemHandler = ({
editor,
anchorNodeId,
type,
+ setShow,
}) => {
switch (type) {
case inlineMenuNamesKeys.text:
@@ -184,6 +185,8 @@ const common_handler_for_inline_menu: ClickItemHandler = ({
editor,
blockId: anchorNodeId,
});
+ // editor.plugins.emit('showAddLink');
+ // setShow(false);
break;
case inlineMenuNamesKeys.code:
toggle_text_format({
@@ -438,6 +441,10 @@ const common_handler_for_inline_menu: ClickItemHandler = ({
blockType: Protocol.Block.Type.file,
});
break;
+ case inlineMenuNamesKeys.backlinks:
+ editor.plugins.emit('showDoubleLink');
+ setShow(false);
+ break;
default: // do nothing
}
};
diff --git a/libs/components/editor-plugins/src/menu/link-menu/LinkMenu.tsx b/libs/components/editor-plugins/src/menu/link-menu/LinkMenu.tsx
new file mode 100644
index 0000000000..ddd178e449
--- /dev/null
+++ b/libs/components/editor-plugins/src/menu/link-menu/LinkMenu.tsx
@@ -0,0 +1,259 @@
+import { CommonListItem, isUrl } from '@toeverything/components/common';
+import { LinkIcon } from '@toeverything/components/icons';
+import {
+ ListButton,
+ MuiClickAwayListener,
+ MuiGrow as Grow,
+ MuiPaper as Paper,
+ MuiPopper as Popper,
+ styled,
+} from '@toeverything/components/ui';
+import { PluginHooks, Virgo } from '@toeverything/framework/virgo';
+import {
+ ChangeEvent,
+ KeyboardEvent,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import { useParams } from 'react-router-dom';
+import { QueryBlocks, QueryResult } from '../../search';
+import { DoubleLinkMenuContainer } from '../double-link-menu/Container';
+
+const ADD_NEW_SUB_PAGE = 'AddNewSubPage';
+const ADD_NEW_PAGE = 'AddNewPage';
+
+export type LinkMenuProps = {
+ editor: Virgo;
+ hooks: PluginHooks;
+};
+
+type LinkMenuStyle = {
+ left: number;
+ top: number;
+ height: number;
+};
+
+const normalizeUrl = (url: string) => {
+ // eslint-disable-next-line no-restricted-globals
+ return /^https?/.test(url) ? url : `${location.protocol}//${url}`;
+};
+
+export const LinkMenu = ({ editor, hooks }: LinkMenuProps) => {
+ const { page_id: curPageId } = useParams();
+ const [isOpen, setIsOpen] = useState(false);
+ const [anchorEl, setAnchorEl] = useState(null);
+ const dialogRef = useRef();
+ const inputEl = useRef();
+ const [linkMenuStyle, setLinkMenuStyle] = useState({
+ left: 0,
+ top: 0,
+ height: 0,
+ });
+ const url = '';
+
+ const [curBlockId, setCurBlockId] = useState();
+ const [searchText, setSearchText] = useState();
+ const [searchResultBlocks, setSearchResultBlocks] = useState(
+ []
+ );
+
+ const menuTypes = useMemo(() => {
+ return Object.values(searchResultBlocks)
+ .map(({ id }) => id)
+ .concat([ADD_NEW_SUB_PAGE, ADD_NEW_PAGE]);
+ }, [searchResultBlocks]);
+
+ const menuItems: CommonListItem[] = useMemo(() => {
+ const items: CommonListItem[] = [];
+ if (searchResultBlocks?.length > 0) {
+ items.push({
+ renderCustom: () => {
+ return ;
+ },
+ });
+ items.push(
+ ...(searchResultBlocks?.map(
+ block =>
+ ({
+ block: {
+ ...block,
+ content: block.content || 'Untitled',
+ },
+ } as CommonListItem)
+ ) || [])
+ );
+ }
+ return items;
+ }, [searchResultBlocks]);
+
+ useEffect(() => {
+ const text = searchText;
+ QueryBlocks(editor, text, result => {
+ result = result.filter(item => item.id !== curPageId);
+ setSearchResultBlocks(result);
+ });
+ }, [editor, searchText, curPageId]);
+
+ const hideMenu = useCallback(() => {
+ setIsOpen(false);
+ editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId);
+ editor.scrollManager.unLock();
+ }, [curBlockId, editor]);
+
+ const resetState = useCallback(
+ (preNodeId: string, nextNodeId: string) => {
+ setCurBlockId(nextNodeId);
+ setSearchText('');
+ setIsOpen(true);
+ editor.scrollManager.lock();
+ const clientRect =
+ editor.selection.currentSelectInfo?.browserSelection
+ ?.getRangeAt(0)
+ ?.getBoundingClientRect();
+ if (clientRect) {
+ const rectTop = clientRect.top;
+ const { top, left } = editor.container.getBoundingClientRect();
+ setLinkMenuStyle({
+ top: rectTop - top,
+ left: clientRect.left - left,
+ height: clientRect.height,
+ });
+ setAnchorEl(dialogRef.current);
+ }
+ },
+ [editor]
+ );
+
+ useEffect(() => {
+ const showLinkMenu = () => {
+ const { anchorNode } = editor.selection.currentSelectInfo;
+ resetState('', anchorNode.id);
+ };
+ editor.plugins.observe('showAddLink', showLinkMenu);
+ return () => editor.plugins.unobserve('showAddLink', showLinkMenu);
+ }, [editor, resetState]);
+
+ const handleSelected = async (id: string) => {};
+
+ const handleFilterChange = useCallback(
+ async (e: ChangeEvent) => {
+ const text = e.target.value;
+
+ await setSearchText(text);
+ },
+ []
+ );
+
+ const addLinkUrlToText = () => {
+ const newUrl = inputEl.current.value;
+ if (newUrl && newUrl !== url && isUrl(normalizeUrl(newUrl))) {
+ editor.blockHelper.wrapLink(curBlockId, newUrl);
+ hideMenu();
+ return;
+ }
+ };
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ addLinkUrlToText();
+ }
+ if (e.key === 'Escape') {
+ hideMenu();
+ }
+ };
+
+ return (
+
+ {isOpen && (
+
hideMenu()}>
+
+ {({ TransitionProps }) => (
+
+
+
+
+
+
+
+
+
+ {menuItems.length > 0 && (
+
+ )}
+
+
+ )}
+
+
+ )}
+
+ );
+};
+
+const LinkModalContainer = styled('div')(({ theme }) => ({
+ display: 'flex',
+ borderRadius: '4px',
+ boxShadow: theme.affine.shadows.shadow1,
+ backgroundColor: '#fff',
+ alignItems: 'center',
+ zIndex: '1',
+}));
+
+const LinkModalContainerIcon = styled('div')(({ theme }) => ({
+ display: 'flex',
+ width: '16px',
+ margin: '0 16px 0 4px',
+ color: '#4C6275',
+}));
+
+const LinkModalContainerInput = styled('input')(({ theme }) => ({
+ flex: '1',
+ outline: 'none',
+ border: 'none',
+ padding: '8px',
+ fontFamily: 'Helvetica,Arial,"Microsoft Yahei",SimHei,sans-serif',
+ '::-webkit-input-placeholder': {
+ color: '#98acbd',
+ },
+ color: '#4C6275',
+}));
diff --git a/libs/components/editor-plugins/src/menu/link-menu/Plugin.tsx b/libs/components/editor-plugins/src/menu/link-menu/Plugin.tsx
new file mode 100644
index 0000000000..e0f7140985
--- /dev/null
+++ b/libs/components/editor-plugins/src/menu/link-menu/Plugin.tsx
@@ -0,0 +1,33 @@
+import { StrictMode } from 'react';
+import { BasePlugin } from '../../base-plugin';
+import { PluginRenderRoot } from '../../utils';
+import { LinkMenu } from './LinkMenu';
+
+const PLUGIN_NAME = 'reference-menu';
+
+export class LinkMenuPlugin extends BasePlugin {
+ private _root?: PluginRenderRoot;
+
+ public static override get pluginName(): string {
+ return PLUGIN_NAME;
+ }
+
+ protected override _onRender(): void {
+ this._root = new PluginRenderRoot({
+ name: PLUGIN_NAME,
+ render: this.editor.reactRenderRoot.render,
+ });
+ this._root.mount();
+
+ this._root?.render(
+
+
+
+ );
+ }
+
+ public override dispose() {
+ this._root?.unmount();
+ super.dispose();
+ }
+}
diff --git a/libs/components/editor-plugins/src/menu/link-menu/index.ts b/libs/components/editor-plugins/src/menu/link-menu/index.ts
new file mode 100644
index 0000000000..1b3746630c
--- /dev/null
+++ b/libs/components/editor-plugins/src/menu/link-menu/index.ts
@@ -0,0 +1 @@
+export { LinkMenuPlugin } from './Plugin';
diff --git a/libs/components/editor-plugins/src/placeholder/PlaceholderPanel.tsx b/libs/components/editor-plugins/src/placeholder/PlaceholderPanel.tsx
index 89b66256f7..16c483f5a5 100644
--- a/libs/components/editor-plugins/src/placeholder/PlaceholderPanel.tsx
+++ b/libs/components/editor-plugins/src/placeholder/PlaceholderPanel.tsx
@@ -1,14 +1,12 @@
+import { Virgo } from '@toeverything/framework/virgo';
import { useCallback, useEffect, useState } from 'react';
-import { useNavigate, useParams } from 'react-router';
-import { Virgo, BlockEditor } from '@toeverything/framework/virgo';
-import { debounce } from '@toeverything/utils';
-import { MuiBox as Box, styled, BaseButton } from '@toeverything/components/ui';
+import { BaseButton, styled } from '@toeverything/components/ui';
import {
services,
- type ReturnUnobserve,
TemplateFactory,
TemplateMeta,
+ type ReturnUnobserve,
} from '@toeverything/datasource/db-service';
const PlaceholderPanelContainer = styled('div')({
@@ -48,7 +46,6 @@ export const PlaceholderPanel = (props: PlaceholderPanelProps) => {
y: 0,
});
const [open, setOpen] = useState(false);
- // const navigate = useNavigate();
const ifPageChildrenExist = useCallback(async () => {
const rootId = await editor.getRootBlockId();
diff --git a/libs/components/editor-plugins/src/search/Search.tsx b/libs/components/editor-plugins/src/search/Search.tsx
index b689345f64..6092862416 100644
--- a/libs/components/editor-plugins/src/search/Search.tsx
+++ b/libs/components/editor-plugins/src/search/Search.tsx
@@ -5,7 +5,7 @@ import {
styled,
TransitionsModal,
} from '@toeverything/components/ui';
-import { BlockEditor, Virgo } from '@toeverything/framework/virgo';
+import { BlockEditor, PluginHooks, Virgo } from '@toeverything/framework/virgo';
import { throttle } from '@toeverything/utils';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
@@ -31,7 +31,7 @@ const styles = style9.create({
export type QueryResult = Awaited>;
-const query_blocks = (
+const queryBlocksExec = (
editor: Virgo,
search: string,
callback: (result: QueryResult) => void
@@ -39,45 +39,58 @@ const query_blocks = (
(editor as BlockEditor).search(search).then(pages => callback(pages));
};
-export const QueryBlocks = throttle(query_blocks, 500);
+export const QueryBlocks = throttle(queryBlocksExec, 500);
type SearchProps = {
- onClose: () => void;
editor: Virgo;
+ hooks: PluginHooks;
};
export const Search = (props: SearchProps) => {
- const { workspace_id } = useParams();
+ const { workspace_id: workspaceId } = useParams();
const navigate = useNavigate();
- const [open, set_open] = useState(true);
- const [search, set_search] = useState('');
- const [result, set_result] = useState([]);
+ const [open, setOpen] = useState(false);
+ const [search, setSearch] = useState();
+ const [result, setResult] = useState([]);
useEffect(() => {
- QueryBlocks(props.editor, search, result => {
- set_result(result);
- });
+ search !== undefined &&
+ QueryBlocks(props.editor, search, result => {
+ setResult(result);
+ });
}, [props.editor, search]);
- const handle_navigate = useCallback(
- (id: string) => navigate(`/${workspace_id}/${id}`),
- [navigate, workspace_id]
+ const handleNavigate = useCallback(
+ (id: string) => navigate(`/${workspaceId}/${id}`),
+ [navigate, workspaceId]
);
+ const handleSearch = useCallback(() => {
+ setOpen(true);
+ setSearch('');
+ }, []);
+
+ // useEffect(() => {
+ // const sub = props.hooks.get(HookType.ON_SEARCH).subscribe(handleSearch);
+
+ // return () => {
+ // sub.unsubscribe();
+ // };
+ // }, [props, handleSearch]);
+
return (
{
- set_open(false);
- props.onClose();
+ setOpen(false);
}}
>
set_search(e.target.value)}
+ onChange={e => setSearch(e.target.value)}
/>
{
{
- handle_navigate(block.id);
- props.onClose();
+ handleNavigate(block.id);
}}
/>
))}
diff --git a/libs/components/editor-plugins/src/search/index.tsx b/libs/components/editor-plugins/src/search/index.tsx
index 41a13695c4..b74e3572a8 100644
--- a/libs/components/editor-plugins/src/search/index.tsx
+++ b/libs/components/editor-plugins/src/search/index.tsx
@@ -1,59 +1,40 @@
import { StrictMode } from 'react';
-
-import { HookType } from '@toeverything/framework/virgo';
-
import { BasePlugin } from '../base-plugin';
import { PluginRenderRoot } from '../utils';
import { Search } from './Search';
export class FullTextSearchPlugin extends BasePlugin {
- #root?: PluginRenderRoot;
+ private root?: PluginRenderRoot;
public static override get pluginName(): string {
return 'search';
}
- public override init(): void {
- this.sub.add(
- this.hooks.get(HookType.ON_SEARCH).subscribe(this._handleSearch)
- );
- }
-
protected override _onRender(): void {
- this.#root = new PluginRenderRoot({
+ this.root = new PluginRenderRoot({
name: FullTextSearchPlugin.pluginName,
render: this.editor.reactRenderRoot.render,
});
+ this._renderSearch();
}
- private unmount() {
- if (this.#root) {
- this.editor.setHotKeysScope();
- this.#root.unmount();
- // this.#root = undefined;
- }
- this.sub.unsubscribe();
- }
-
- private _handleSearch = () => {
- this.editor.setHotKeysScope('search');
- this.render_search();
- };
- private render_search() {
- if (this.#root) {
- this.#root.mount();
- this.#root.render(
+ private _renderSearch() {
+ if (this.root) {
+ this.root.mount();
+ this.root.render(
- this.unmount()}
- editor={this.editor}
- />
+
);
}
}
public renderSearch() {
- this.render_search();
+ this._renderSearch();
+ }
+
+ public override dispose() {
+ this.root?.unmount();
+ super.dispose();
}
}
diff --git a/libs/components/layout/src/header/LayoutHeader.tsx b/libs/components/layout/src/header/LayoutHeader.tsx
index c9e7b7b8da..9b104b4442 100644
--- a/libs/components/layout/src/header/LayoutHeader.tsx
+++ b/libs/components/layout/src/header/LayoutHeader.tsx
@@ -1,5 +1,3 @@
-import { useMemo } from 'react';
-
import {
LogoIcon,
SearchIcon,
@@ -8,10 +6,11 @@ import {
} from '@toeverything/components/icons';
import { IconButton, styled } from '@toeverything/components/ui';
import {
+ useCurrentEditors,
useLocalTrigger,
useShowSettingsSidebar,
} from '@toeverything/datasource/state';
-
+import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { EditorBoardSwitcher } from './EditorBoardSwitcher';
import { fsApiSupported } from './FileSystem';
@@ -33,6 +32,14 @@ export const LayoutHeader = () => {
}
}, [isLocalWorkspace, t]);
+ const { currentEditors } = useCurrentEditors();
+
+ const handleSearch = useCallback(() => {
+ for (const key in currentEditors || {}) {
+ currentEditors[key].getHooks().onSearch();
+ }
+ }, [currentEditors]);
+
return (
@@ -49,8 +56,8 @@ export const LayoutHeader = () => {
diff --git a/libs/components/layout/src/workspace-sidebar/page-tree/block-page.ts b/libs/components/layout/src/workspace-sidebar/page-tree/block-page.ts
deleted file mode 100644
index e74dd6eb42..0000000000
--- a/libs/components/layout/src/workspace-sidebar/page-tree/block-page.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-// import type {
-// ArrayOperation,
-// BlockClientInstance,
-// BlockImplInstance,
-// MapOperation,
-// } from '@toeverything/datasource/jwt';
-// import { getDateIsoStringWithTimezone } from '@toeverything/utils';
-
-// export async function getRecentPages(
-// db: BlockClientInstance,
-// userId: string
-// ): Promise {
-// try {
-// const config = await db.getWorkspaceConfig>();
-// const recent_pages = config.get('recent_pages');
-// if (recent_pages?.get(userId)) {
-// return [recent_pages.get(userId) as string];
-// }
-// } catch (e) {
-// console.error(e);
-// }
-
-// return [''];
-// }
-
-// /** Mark the article corresponding to pageId as the most recently accessed article */
-// export async function setRecentPages(
-// db: BlockClientInstance,
-// userId: string,
-// pageId: string
-// ): Promise {
-// try {
-// const config = await db.getWorkspaceConfig>();
-// const recent_pages = config.get('recent_pages');
-// if (recent_pages && recent_pages.get(userId)) {
-// recent_pages.set(userId, pageId);
-// } else {
-// const map = config.createMap();
-// map.set(userId, pageId);
-// config.set('recent_pages', map);
-// }
-// } catch (e) {
-// console.error(e);
-// }
-// }
-
-// export async function setPageTree(
-// db: BlockClientInstance,
-// treeData: TreeItem[]
-// ): Promise {
-// try {
-// const config = await db.getWorkspaceConfig();
-// const array = config.createArray();
-// array.push((treeData as any[]) || []);
-// config.set('page_tree', array);
-// } catch (e) {
-// console.error(e);
-// }
-// }
-
-// async function update_tree_items_title<
-// TreeItem extends { id: string; title: string; children: TreeItem[] }
-// >(
-// db: BlockClientInstance,
-// items: TreeItem[],
-// cache: Record
-// ): Promise {
-// for (const item of items) {
-// if (cache[item.id]) {
-// item.title = cache[item.id];
-// } else {
-// const page = await db.get(item.id as 'page');
-// item.title =
-// page.getDecoration>('text')?.[0]
-// ?.text ||
-// 'Untitled ' +
-// getDateIsoStringWithTimezone(page.created)
-// .slice(11)
-// .replace('T', ' ');
-// cache[item.id] = item.title;
-// }
-
-// if (item.children.length) {
-// item.children = await update_tree_items_title(
-// db,
-// item.children,
-// cache
-// );
-// }
-// }
-
-// return [...items];
-// }
-
-// export async function getPageTree(
-// db: BlockClientInstance
-// ): Promise {
-// try {
-// const config = await db.getWorkspaceConfig();
-// const page_tree = config.get('page_tree') as ArrayOperation;
-// const page_tree_items = page_tree?.slice();
-// if (page_tree && page_tree_items?.length) {
-// const pages = await update_tree_items_title(
-// db,
-// page_tree_items as [],
-// {}
-// );
-// return pages;
-// }
-// } catch (e) {
-// console.error(e);
-// }
-
-// return [];
-// }
-
-// export async function getWorkspaceConfig(
-// db: BlockClientInstance,
-// userId: string,
-// name: string
-// ): Promise {
-// try {
-// // const config = await db.getWorkspaceConfig();
-// // const map = config.get_map(name);
-// // if (map && map.get(userId)) {
-// // return map.get(userId)!;
-// // }
-// } catch (e) {
-// console.error(e);
-// }
-
-// return '';
-// }
-
-// export async function setWorkspaceConfig(
-// db: BlockClientInstance,
-// userId: string,
-// name: string,
-// value: string
-// ): Promise {
-// try {
-// // const config = await db.getWorkspaceConfig();
-// // const map = config.get_map(name);
-// // map.set(userId, value === undefined || value === null ? '' : value);
-// } catch (e) {
-// console.error(e);
-// }
-// }
-
-// export async function createPage(
-// db: BlockClientInstance,
-// title?: string,
-// content?: string,
-// metadata?: Record
-// ): Promise {
-// if (db) {
-// const page: BlockImplInstance = await db.get('page');
-// if (title) {
-// page.getContent().set('title', title);
-// }
-// if (metadata) {
-// Object.keys(metadata).forEach(property =>
-// page.getContent().set(property, metadata[property])
-// );
-// }
-// return page;
-// }
-
-// return undefined;
-// }
diff --git a/libs/components/layout/src/workspace-sidebar/page-tree/index.ts b/libs/components/layout/src/workspace-sidebar/page-tree/index.ts
index 836e68fa95..c6ddcedd64 100644
--- a/libs/components/layout/src/workspace-sidebar/page-tree/index.ts
+++ b/libs/components/layout/src/workspace-sidebar/page-tree/index.ts
@@ -1,5 +1,3 @@
export { PageTree } from './PageTree';
export * from './types';
export { usePageTree } from './use-page-tree';
-
-// export { getRecentPages, setRecentPages } from './block-page';
diff --git a/libs/datasource/jwt-rpc/src/broadcast.ts b/libs/datasource/jwt-rpc/src/broadcast.ts
index b0f344aad7..59c974bf0c 100644
--- a/libs/datasource/jwt-rpc/src/broadcast.ts
+++ b/libs/datasource/jwt-rpc/src/broadcast.ts
@@ -3,7 +3,6 @@ import * as encoding from 'lib0/encoding';
import * as awarenessProtocol from 'y-protocols/awareness';
import * as syncProtocol from 'y-protocols/sync';
import * as Y from 'yjs';
-
import { Message } from './handler';
import { readMessage } from './processor';
import { WebsocketProvider } from './provider';
@@ -14,7 +13,7 @@ export const registerBroadcastSubscriber = (
document: Y.Doc
) => {
const channel = provider.broadcastChannel;
-
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const subscriber = (data: ArrayBuffer, origin: any) => {
if (origin !== provider) {
const encoder = readMessage(provider, new Uint8Array(data), false);