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);