From e18a35989f0307e04a1ea735043331679f2dd832 Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Thu, 1 Sep 2022 10:44:21 +0800 Subject: [PATCH] feat: Link feature add search function --- libs/components/common/src/lib/index.ts | 1 + .../common/src/lib/text/slate-utils.ts | 52 ++++ .../src/editor/block/block-helper.ts | 13 +- libs/components/editor-plugins/src/index.ts | 3 +- .../editor-plugins/src/menu/index.ts | 1 + .../src/menu/inline-menu/utils.ts | 2 + .../src/menu/link-menu/LinkMenu.tsx | 263 ++++++++++++++++++ .../src/menu/link-menu/Plugin.tsx | 33 +++ .../src/menu/link-menu/index.ts | 1 + 9 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 libs/components/editor-plugins/src/menu/link-menu/LinkMenu.tsx create mode 100644 libs/components/editor-plugins/src/menu/link-menu/Plugin.tsx create mode 100644 libs/components/editor-plugins/src/menu/link-menu/index.ts 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/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts index 03701d0646..1dc10e8042 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, @@ -1230,6 +1231,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-core/src/editor/block/block-helper.ts b/libs/components/editor-core/src/editor/block/block-helper.ts index 012633b813..032f496050 100644 --- a/libs/components/editor-core/src/editor/block/block-helper.ts +++ b/libs/components/editor-core/src/editor/block/block-helper.ts @@ -5,6 +5,7 @@ import type { } from '@toeverything/components/common'; import { BaseRange, + Location, Node, Path, Point, @@ -41,7 +42,8 @@ type TextUtilsFunctions = | 'blur' | 'setSelection' | 'insertNodes' - | 'getNodeByPath'; + | 'getNodeByPath' + | 'wrapLink'; type ExtendedTextUtils = SlateUtils & { setLinkModalVisible: (visible: boolean) => void; @@ -412,4 +414,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-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/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/utils.ts b/libs/components/editor-plugins/src/menu/inline-menu/utils.ts index a6595b3bc9..a086905da5 100644 --- a/libs/components/editor-plugins/src/menu/inline-menu/utils.ts +++ b/libs/components/editor-plugins/src/menu/inline-menu/utils.ts @@ -185,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({ 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..f2d2af19fe --- /dev/null +++ b/libs/components/editor-plugins/src/menu/link-menu/LinkMenu.tsx @@ -0,0 +1,263 @@ +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 showDoubleLink = () => { + const { anchorNode } = editor.selection.currentSelectInfo; + resetState('', anchorNode.id); + }; + editor.plugins.observe('showAddLink', showDoubleLink); + return () => editor.plugins.unobserve('showAddLink', showDoubleLink); + }, [editor, resetState]); + + const handleSelected = async (id: string) => { + if (curBlockId) { + } + }; + + 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', + width: '354px', +})); + +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';