From 919e0d08d561d0cd8da9f52aa3a610252f0aae13 Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Thu, 18 Aug 2022 16:22:45 +0800 Subject: [PATCH 01/12] feat: Intra-line double link interaction --- libs/components/common/src/lib/list/index.tsx | 15 +- .../common/src/lib/text/EditableText.tsx | 58 +-- .../src/lib/text/plugins/DoubleLink.tsx | 29 ++ .../common/src/lib/text/slate-utils.ts | 146 ++++++-- .../src/editor/block/async-block.ts | 45 ++- .../src/editor/block/block-helper.ts | 75 +++- .../editor-core/src/editor/editor.ts | 8 +- libs/components/editor-plugins/src/index.ts | 22 +- .../src/menu/double-link-menu/Container.tsx | 185 ++++++++++ .../menu/double-link-menu/DoubleLinkMenu.tsx | 346 ++++++++++++++++++ .../Plugin.tsx | 7 +- .../src/menu/double-link-menu/index.ts | 1 + .../editor-plugins/src/menu/index.ts | 10 +- .../src/menu/reference-menu/Container.tsx | 190 ---------- .../src/menu/reference-menu/ReferenceMenu.tsx | 148 -------- .../src/menu/reference-menu/index.ts | 1 - libs/components/ui/src/button/ListButton.tsx | 3 +- libs/datasource/jwt/src/index.ts | 30 +- 18 files changed, 859 insertions(+), 460 deletions(-) create mode 100644 libs/components/common/src/lib/text/plugins/DoubleLink.tsx create mode 100644 libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx create mode 100644 libs/components/editor-plugins/src/menu/double-link-menu/DoubleLinkMenu.tsx rename libs/components/editor-plugins/src/menu/{reference-menu => double-link-menu}/Plugin.tsx (79%) create mode 100644 libs/components/editor-plugins/src/menu/double-link-menu/index.ts delete mode 100644 libs/components/editor-plugins/src/menu/reference-menu/Container.tsx delete mode 100644 libs/components/editor-plugins/src/menu/reference-menu/ReferenceMenu.tsx delete mode 100644 libs/components/editor-plugins/src/menu/reference-menu/index.ts diff --git a/libs/components/common/src/lib/list/index.tsx b/libs/components/common/src/lib/list/index.tsx index 8f57a1d871..7de7e35b8e 100644 --- a/libs/components/common/src/lib/list/index.tsx +++ b/libs/components/common/src/lib/list/index.tsx @@ -1,8 +1,4 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router'; -import clsx from 'clsx'; -import style9 from 'style9'; - +import { BackwardUndoIcon } from '@toeverything/components/icons'; import { BaseButton, ListButton, @@ -12,9 +8,11 @@ import { } from '@toeverything/components/ui'; // eslint-disable-next-line @nrwl/nx/enforce-module-boundaries import { BlockSearchItem } from '@toeverything/datasource/jwt'; - +import clsx from 'clsx'; +import { useState } from 'react'; +import { useNavigate } from 'react-router'; +import style9 from 'style9'; import { BlockPreview } from '../block-preview'; -import { BackwardUndoIcon } from '@toeverything/components/icons'; export const commonListContainer = 'commonListContainer'; @@ -28,6 +26,7 @@ export type CommonListItem = { divider?: string; content?: Content; block?: BlockSearchItem; + renderCustom?: (props: CommonListItem) => JSX.Element; }; type MenuItemsProps = { @@ -45,7 +44,7 @@ export const CommonList = (props: MenuItemsProps) => { // ]); // TODO Insert bidirectional link to be developed const JSONUnsupportedBlockTypes = ['page']; - let usedItems = items.filter(item => { + const usedItems = items.filter(item => { return !JSONUnsupportedBlockTypes.includes(item?.content?.id); }); return ( diff --git a/libs/components/common/src/lib/text/EditableText.tsx b/libs/components/common/src/lib/text/EditableText.tsx index 3c9ca6fe5c..7bc6c3904a 100644 --- a/libs/components/common/src/lib/text/EditableText.tsx +++ b/libs/components/common/src/lib/text/EditableText.tsx @@ -1,54 +1,54 @@ /* eslint-disable max-lines */ +import { SearchIcon } from '@toeverything/components/icons'; +import { ErrorBoundary, isEqual } from '@toeverything/utils'; +import isHotkey from 'is-hotkey'; +import isUrl from 'is-url'; import React, { + CSSProperties, + DragEvent, + forwardRef, KeyboardEvent, KeyboardEventHandler, + MouseEvent, + MouseEventHandler, useCallback, useEffect, + useLayoutEffect, useMemo, useRef, useState, - forwardRef, - MouseEventHandler, - useLayoutEffect, - CSSProperties, - MouseEvent, - DragEvent, } from 'react'; -import isHotkey from 'is-hotkey'; import { createEditor, Descendant, - Range, - Element as SlateElement, Editor, - Transforms, + Element as SlateElement, Node, Path, + Range, + Transforms, } from 'slate'; import { Editable, - withReact, - Slate, ReactEditor, + Slate, useSlateStatic, + withReact, } from 'slate-react'; - -import { ErrorBoundary, isEqual, uaHelper } from '@toeverything/utils'; - -import { Contents, SlateUtils, isSelectAll } from './slate-utils'; +import { CustomElement } from '..'; +import { HOTKEYS, INLINE_STYLES } from './constants'; +import { TextWithComments } from './element-leaf/TextWithComments'; +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 { getCommentsIdsOnTextNode, getExtraPropertiesFromEditorOutmostNode, isInterceptCharacter, matchMarkdown, } from './utils'; -import { HOTKEYS, INLINE_STYLES } from './constants'; -import { LinkComponent, LinkModal, withLinks, wrapLink } from './plugins/link'; -import { withDate, InlineDate } from './plugins/date'; -import { CustomElement } from '..'; -import isUrl from 'is-url'; -import { InlineRefLink } from './plugins/reflink'; -import { TextWithComments } from './element-leaf/TextWithComments'; export interface TextProps { /** read only */ @@ -739,6 +739,9 @@ const EditorElement = (props: any) => { switch (element.type) { case 'link': { + if (element.linkType === 'doubleLink') { + return ; + } return ( { } } + if (leaf.doubleLinkSearch) { + customChildren = ( + + + {customChildren} + + ); + } + customChildren = ( {customChildren} diff --git a/libs/components/common/src/lib/text/plugins/DoubleLink.tsx b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx new file mode 100644 index 0000000000..8160731bf2 --- /dev/null +++ b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx @@ -0,0 +1,29 @@ +import { PagesIcon } from '@toeverything/components/icons'; +import React, { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +export const DoubleLinkComponent = ({ attributes, children, element }: any) => { + const navigate = useNavigate(); + + const handleClickLinkText = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + const { workspaceId, blockId } = element; + navigate(`/${workspaceId}/${blockId}`); + }, + [element, navigate] + ); + + return ( + + + + {children} + + + ); +}; diff --git a/libs/components/common/src/lib/text/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts index ec7e219810..9b1fff664c 100644 --- a/libs/components/common/src/lib/text/slate-utils.ts +++ b/libs/components/common/src/lib/text/slate-utils.ts @@ -11,7 +11,6 @@ import { Transforms, } from 'slate'; import { ReactEditor } from 'slate-react'; - import { fontBgColorPalette, fontColorPalette, @@ -21,6 +20,7 @@ import { import { getCommentsIdsOnTextNode, getEditorMarkForCommentId, + getRandomString, MARKDOWN_STYLE_MAP, MatchRes, } from './utils'; @@ -246,7 +246,11 @@ Editor.insertText = function ( const { path, offset } = at; if (text.length > 0) { const marks = Editor.marks(editor); - if (text === '\u0020' && Object.keys(marks).length) { + if ( + text === '\u0020' && + Object.keys(marks).length && + !(marks as any).doubleLinkSearch + ) { // If the input is a space and the mark has content, remove the mark const newPath = [path[0], path[1] + 1]; const newPoint = { @@ -1084,34 +1088,130 @@ class SlateUtils { }); } - public insertReference(reference: string) { - try { - // Transforms.setSelection(this.editor, this.getEndSelection()); - const { anchor, focus } = this.getEndSelection(); - Transforms.insertNodes( + public setDoubleLinkSearchSlash(point: Point) { + const str = Editor.string(this.editor, { + anchor: this.getStart(), + focus: point, + }); + if (str.endsWith('[[')) { + Transforms.select(this.editor, { + anchor: point, + focus: Object.assign({}, point, { + offset: point.offset - 2, + }), + }); + Editor.addMark(this.editor, 'doubleLinkSearch', true); + Transforms.select(this.editor, { + anchor: this.editor.selection.anchor, + focus: this.editor.selection.anchor, + }); + } + } + + public getDoubleLinkSearchSlashText() { + const nodes = Editor.nodes(this.editor, { + at: [], + //@ts-ignore + match: node => !!node.doubleLinkSearch, + }); + const searchNode = nodes.next().value; + if (searchNode && (searchNode[0] as { text?: string }).text) { + return (searchNode[0] as { text?: string }).text; + } + return ''; + } + + public setSelectDoubleLinkSearchSlash() { + const nodes = Editor.nodes(this.editor, { + at: [], + //@ts-ignore + match: node => !!node.doubleLinkSearch, + }); + const searchNode = nodes.next().value; + if (searchNode) { + const text = (searchNode[0] as { text?: string })?.text || ''; + const path = searchNode[1]; + const anchor = Editor.before( this.editor, { - type: 'reflink', - reference, - children: [], + path, + offset: 1, }, - { at: focus || anchor } + { + unit: 'offset', + } ); - - // requestAnimationFrame(() => { - // console.log(this.editor.selection, this.editor.insertNode); - // this.editor.insertNode({ - // type: 'reflink', - // reference, - // children: [{ text: '' }] - // }); - // // Transforms.select(); - // }); - } catch (e) { - console.log(e); + const focus = Editor.after( + this.editor, + { + path, + offset: text.length - 1, + }, + { + unit: 'offset', + } + ); + Transforms.select(this.editor, { anchor, focus }); } } + public removeDoubleLinkSearchSlash(isRemoveSlash?: boolean) { + if (isRemoveSlash) { + const nodes = Editor.nodes(this.editor, { + at: [], + //@ts-ignore + match: node => !!node.doubleLinkSearch, + }); + const searchNode = nodes.next().value; + if (searchNode) { + const text = (searchNode[0] as { text?: string })?.text || ''; + if (text.startsWith('[[')) { + const path = searchNode[1]; + Transforms.delete(this.editor, { + at: { + path, + offset: 0, + }, + distance: text.length, + unit: 'character', + }); + } + } + } + Transforms.setNodes( + this.editor, + { doubleLinkSearch: null } as Partial, + { + at: [], + match: node => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + return !!node.doubleLinkSearch; + }, + } + ); + this.editor.removeMark('doubleLinkSearch'); + } + + public insertDoubleLink( + workspaceId: string, + linkBlockId: string, + children: any[] + ) { + const link = { + type: 'link', + linkType: 'doubleLink', + workspaceId: workspaceId, + blockId: linkBlockId, + children: children, + id: getRandomString('link'), + }; + Transforms.insertNodes(this.editor, link); + requestAnimationFrame(() => { + ReactEditor.focus(this.editor); + }); + } + /** todo improve if selection is collapsed */ public getCommentsIdsBySelection() { const commentedTextNodes = Editor.nodes(this.editor, { diff --git a/libs/components/editor-core/src/editor/block/async-block.ts b/libs/components/editor-core/src/editor/block/async-block.ts index df2f6d972c..9eb156852e 100644 --- a/libs/components/editor-core/src/editor/block/async-block.ts +++ b/libs/components/editor-core/src/editor/block/async-block.ts @@ -1,19 +1,19 @@ /* eslint-disable max-lines */ -import EventEmitter from 'eventemitter3'; import { - ReturnEditorBlock, - UpdateEditorBlock, DefaultColumnsValue, Protocol, + ReturnEditorBlock, + UpdateEditorBlock, } from '@toeverything/datasource/db-service'; import { - isDev, createNoopWithMessage, - lowerFirst, + isDev, last, + lowerFirst, } from '@toeverything/utils'; -import { BlockProvider } from './block-provider'; +import EventEmitter from 'eventemitter3'; import { BaseView, BaseView as BlockView } from './../views/base-view'; +import { BlockProvider } from './block-provider'; type EventType = 'update'; export interface EventData { @@ -154,6 +154,7 @@ export class AsyncBlock { } this.initialized = true; this.raw_data = await this.filterPageInvalidChildren(this.raw_data); + this.raw_data = await this.updateDoubleLinkBlock(this.raw_data); const { workspace, id } = this.raw_data; this.unobserve = await this.services.observe( { workspace, id }, @@ -161,6 +162,7 @@ export class AsyncBlock { const oldData = this.raw_data; this.raw_data = blockData; this.raw_data = await this.filterPageInvalidChildren(blockData); + this.raw_data = await this.updateDoubleLinkBlock(this.raw_data); this.emit('update', { block: this, oldData }); } ); @@ -495,4 +497,35 @@ export class AsyncBlock { getBoundingClientRect() { return this.dom?.getBoundingClientRect(); } + + async updateDoubleLinkBlock(rawData: ReturnEditorBlock) { + const values = rawData.properties?.text?.value || []; + for (let i = 0; i < values.length; i++) { + const item = values[i] as any; + if (item.linkType === 'doubleLink') { + const linkBlock = await this.services.load({ + workspace: item.workspaceId, + id: item.blockId, + }); + + if (linkBlock) { + let children = linkBlock.getProperties().text?.value || []; + if (children.length === 1 && !children[0].text) { + children = [{ text: 'Untitled' }]; + } + if ( + children.map(v => v.text).join('') !== + (item.children || []).map((v: any) => v.text).join('') + ) { + const newItem = { + ...item, + children: children, + }; + values.splice(i, 1, newItem); + } + } + } + } + return rawData; + } } 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 7bd0284ffc..012633b813 100644 --- a/libs/components/editor-core/src/editor/block/block-helper.ts +++ b/libs/components/editor-core/src/editor/block/block-helper.ts @@ -19,6 +19,11 @@ type TextUtilsFunctions = | 'setSearchSlash' | 'removeSearchSlash' | 'getSearchSlashText' + | 'setDoubleLinkSearchSlash' + | 'getDoubleLinkSearchSlashText' + | 'setSelectDoubleLinkSearchSlash' + | 'removeDoubleLinkSearchSlash' + | 'insertDoubleLink' | 'selectionToSlateRange' | 'transformPoint' | 'toggleTextFormatBySelection' @@ -32,7 +37,6 @@ type TextUtilsFunctions = | 'getCommentsIdsBySelection' | 'getCurrentSelection' | 'removeSelection' - | 'insertReference' | 'isCollapsed' | 'blur' | 'setSelection' @@ -149,27 +153,60 @@ export class BlockHelper { } } - public insertReference( - reference: string, + public setDoubleLinkSearchSlash(blockId: string, point: Point) { + const textUtils = this._blockTextUtilsMap[blockId]; + if (textUtils) { + textUtils.setDoubleLinkSearchSlash(point); + } else { + console.warn('Could find the block text utils'); + } + } + + public getDoubleLinkSearchSlashText(blockId: string) { + const textUtils = this._blockTextUtilsMap[blockId]; + if (textUtils) { + return textUtils.getDoubleLinkSearchSlashText(); + } + console.warn('Could find the block text utils'); + return ''; + } + public setSelectDoubleLinkSearchSlash(blockId: string) { + const textUtils = this._blockTextUtilsMap[blockId]; + if (textUtils) { + return textUtils.setSelectDoubleLinkSearchSlash(); + } + console.warn('Could find the block text utils'); + return ''; + } + + public removeDoubleLinkSearchSlash( blockId: string, - selection: Selection, - offset: number + isRemoveSlash?: boolean ) { - const text_utils = this._blockTextUtilsMap[blockId]; - if (text_utils) { - const offsetSelection = window.getSelection(); - offsetSelection.setBaseAndExtent( - selection.anchorNode, - selection.anchorOffset, - selection.focusNode, - selection.focusOffset + offset - ); + const textUtils = this._blockTextUtilsMap[blockId]; + if (textUtils) { + textUtils.removeDoubleLinkSearchSlash(isRemoveSlash); + } else { + console.warn('Could find the block text utils'); + } + } - text_utils.removeSelection(offsetSelection); - text_utils.insertReference(reference); - - // range. - // text_utils.toggleTextFormatBySelection(format, range); + public async insertDoubleLink( + workspaceId: string, + linkBlockId: string, + blockId: string + ) { + const textUtils = this._blockTextUtilsMap[blockId]; + if (textUtils) { + const linkBlock = await this._editor.getBlock({ + workspace: workspaceId, + id: linkBlockId, + }); + let children = linkBlock.getProperties().text?.value || []; + if (children.length === 1 && !children[0].text) { + children = [{ text: 'Untitled' }]; + } + textUtils.insertDoubleLink(workspaceId, linkBlockId, children); } console.warn('Could find the block text utils'); } diff --git a/libs/components/editor-core/src/editor/editor.ts b/libs/components/editor-core/src/editor/editor.ts index e85db2ba65..e7f6458a5f 100644 --- a/libs/components/editor-core/src/editor/editor.ts +++ b/libs/components/editor-core/src/editor/editor.ts @@ -1,17 +1,15 @@ /* eslint-disable max-lines */ -import HotKeys from 'hotkeys-js'; - import type { PatchNode } from '@toeverything/components/ui'; +import { Commands } from '@toeverything/datasource/commands'; import type { BlockFlavors, ReturnEditorBlock, UpdateEditorBlock, } from '@toeverything/datasource/db-service'; import { services } from '@toeverything/datasource/db-service'; - -import { Commands } from '@toeverything/datasource/commands'; import { domToRect, last, Point, sleep } from '@toeverything/utils'; import assert from 'assert'; +import HotKeys from 'hotkeys-js'; import type { WorkspaceAndBlockId } from './block'; import { AsyncBlock } from './block'; import { BlockHelper } from './block/block-helper'; @@ -282,7 +280,7 @@ export class Editor implements Virgo { return await blockView.onCreate(block); } - private async getBlock({ + public async getBlock({ workspace, id, }: WorkspaceAndBlockId): Promise { diff --git a/libs/components/editor-plugins/src/index.ts b/libs/components/editor-plugins/src/index.ts index be7bb9bd97..73cfece84f 100644 --- a/libs/components/editor-plugins/src/index.ts +++ b/libs/components/editor-plugins/src/index.ts @@ -1,15 +1,15 @@ import type { PluginCreator } from '@toeverything/framework/virgo'; -import { - LeftMenuPlugin, - InlineMenuPlugin, - CommandMenuPlugin, - ReferenceMenuPlugin, - SelectionGroupPlugin, - GroupMenuPlugin, -} from './menu'; -import { TemplatePlugin } from './template'; -import { FullTextSearchPlugin } from './search'; import { AddCommentPlugin } from './comment'; +import { + CommandMenuPlugin, + DoubleLinkMenuPlugin, + GroupMenuPlugin, + InlineMenuPlugin, + LeftMenuPlugin, + SelectionGroupPlugin, +} from './menu'; +import { FullTextSearchPlugin } from './search'; +import { TemplatePlugin } from './template'; // import { PlaceholderPlugin } from './placeholder'; // import { BlockPropertyPlugin } from './block-property'; @@ -19,7 +19,7 @@ export const plugins: PluginCreator[] = [ LeftMenuPlugin, InlineMenuPlugin, CommandMenuPlugin, - ReferenceMenuPlugin, + DoubleLinkMenuPlugin, TemplatePlugin, SelectionGroupPlugin, AddCommentPlugin, diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx new file mode 100644 index 0000000000..8714ab519f --- /dev/null +++ b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx @@ -0,0 +1,185 @@ +import React, { useEffect, useState, useCallback, useRef } from 'react'; + +import { Virgo, PluginHooks, HookType } from '@toeverything/framework/virgo'; +import { + CommonList, + CommonListItem, + commonListContainer, +} from '@toeverything/components/common'; +import { domToRect } from '@toeverything/utils'; +import { styled } from '@toeverything/components/ui'; + +import { QueryResult } from '../../search'; + +export type DoubleLinkMenuContainerProps = { + editor: Virgo; + hooks: PluginHooks; + style?: React.CSSProperties; + isShow?: boolean; + blockId: string; + onSelected?: (item: string) => void; + onClose?: () => void; + types?: Array; + items?: CommonListItem[]; +}; + +export const DoubleLinkMenuContainer = ({ + hooks, + isShow = false, + onSelected, + onClose, + types, + style, + items, +}: DoubleLinkMenuContainerProps) => { + const menuRef = useRef(null); + const [currentItem, setCurrentItem] = useState(); + const [needCheckIntoView, setNeedCheckIntoView] = useState(false); + + useEffect(() => { + if (needCheckIntoView) { + if (currentItem && menuRef.current) { + const itemEle = + menuRef.current.querySelector( + `.item-${currentItem}` + ); + const scrollEle = + menuRef.current.querySelector( + `.${commonListContainer}` + ); + if (itemEle) { + const itemRect = domToRect(itemEle); + const scrollRect = domToRect(scrollEle); + if ( + itemRect.top < scrollRect.top || + itemRect.bottom > scrollRect.bottom + ) { + // IMP: may be do it with self function + itemEle.scrollIntoView({ + block: 'nearest', + }); + } + } + } + setNeedCheckIntoView(false); + } + }, [needCheckIntoView, currentItem]); + + useEffect(() => { + if (isShow && types && !currentItem) setCurrentItem(types[0]); + if (!isShow) onClose?.(); + }, [currentItem, isShow, onClose, types]); + + useEffect(() => { + if (isShow && types) { + if (!types.includes(currentItem)) { + setNeedCheckIntoView(true); + if (types.length) { + setCurrentItem(types[0]); + } else { + setCurrentItem(undefined); + } + } + } + }, [isShow, types, currentItem]); + + const handleClickUp = useCallback( + (event: React.KeyboardEvent) => { + if (isShow && types && event.code === 'ArrowUp') { + event.preventDefault(); + if (!currentItem && types.length) { + setCurrentItem(types[types.length - 1]); + } + if (currentItem) { + const idx = types.indexOf(currentItem); + if (idx > 0) { + setNeedCheckIntoView(true); + setCurrentItem(types[idx - 1]); + } + } + } + }, + [isShow, types, currentItem] + ); + + const handleClickDown = useCallback( + (event: React.KeyboardEvent) => { + if (isShow && types && event.code === 'ArrowDown') { + event.preventDefault(); + if (!currentItem && types.length) { + setCurrentItem(types[0]); + } + if (currentItem) { + const idx = types.indexOf(currentItem); + if (idx < types.length - 1) { + setNeedCheckIntoView(true); + setCurrentItem(types[idx + 1]); + } + } + } + }, + [isShow, types, currentItem] + ); + + const handleClickEnter = useCallback( + async (event: React.KeyboardEvent) => { + if (isShow && event.code === 'Enter' && currentItem) { + event.preventDefault(); + onSelected && onSelected(currentItem); + } + }, + [isShow, currentItem, onSelected] + ); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + handleClickUp(event); + handleClickDown(event); + handleClickEnter(event); + }, + [handleClickUp, handleClickDown, handleClickEnter] + ); + + useEffect(() => { + const sub = hooks + .get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE) + .subscribe(handleKeyDown); + + return () => { + sub.unsubscribe(); + }; + }, [hooks, handleKeyDown]); + + return isShow ? ( + + + onSelected?.(type)} + currentItem={currentItem} + setCurrentItem={setCurrentItem} + /> + + + ) : null; +}; + +const RootContainer = styled('div')(({ theme }) => ({ + // position: 'fixed', + zIndex: 1, + maxHeight: '525px', + borderRadius: '10px', + boxShadow: theme.affine.shadows.shadow1, + backgroundColor: '#fff', + padding: '8px 4px', +})); + +const ContentContainer = styled('div')(({ theme }) => ({ + display: 'flex', + overflow: 'hidden', + maxHeight: '493px', +})); 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 new file mode 100644 index 0000000000..1f545809a3 --- /dev/null +++ b/libs/components/editor-plugins/src/menu/double-link-menu/DoubleLinkMenu.tsx @@ -0,0 +1,346 @@ +import { CommonListItem } from '@toeverything/components/common'; +import { AddIcon } from '@toeverything/components/icons'; +import { + ListButton, + MuiClickAwayListener, + MuiGrow as Grow, + MuiOutlinedInput as OutlinedInput, + MuiPaper as Paper, + MuiPopper as Popper, + styled, +} from '@toeverything/components/ui'; +import { services } from '@toeverything/datasource/db-service'; +import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo'; +import { getPageId } from '@toeverything/utils'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { QueryBlocks, QueryResult } from '../../search'; +import { DoubleLinkMenuContainer } from './Container'; + +const ADD_NEW_SUB_PAGE = 'AddNewSubPage'; +const ADD_NEW_PAGE = 'AddNewPage'; + +export type DoubleLinkMenuProps = { + editor: Virgo; + hooks: PluginHooks; + style?: { left: number; top: number }; +}; + +type DoubleMenuStyle = { + left: number; + top: number; + height: number; +}; + +export const DoubleLinkMenu = ({ + editor, + hooks, + style, +}: DoubleLinkMenuProps) => { + const [isShow, setIsShow] = useState(false); + const [blockId, setBlockId] = useState(); + const [searchText, setSearchText] = useState(''); + const [searchBlocks, setSearchBlocks] = useState([]); + const [items, setItems] = useState([]); + const [isNewPage, setIsNewPage] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const ref = useRef(); + const ref1 = useRef(); + const [referenceMenuStyle, setReferenceMenuStyle] = + useState({ + left: 0, + top: 0, + height: 0, + }); + + useEffect(() => { + QueryBlocks(editor, searchText, result => { + setSearchBlocks(result); + ref1?.current?.focus(); + const items: CommonListItem[] = []; + if (searchBlocks?.length > 0) { + if (isNewPage) { + items.push({ + renderCustom: () => { + return ; + }, + }); + } else { + items.push({ + renderCustom: () => { + return ; + }, + }); + } + items.push( + ...(searchBlocks?.map( + block => ({ block } as CommonListItem) + ) || []) + ); + } + + if (items.length > 0) { + items.push({ divider: 'newPage' }); + } + items.push({ + content: { + id: ADD_NEW_SUB_PAGE, + content: 'Add new sub-page', + icon: AddIcon, + }, + }); + items.push({ + content: { + id: ADD_NEW_PAGE, + content: 'Add new page in...', + icon: AddIcon, + }, + }); + + setItems(items); + }); + }, [editor, searchText, isNewPage]); + + const types = useMemo(() => { + return Object.values(searchBlocks) + .map(({ id }) => id) + .concat([ADD_NEW_SUB_PAGE, ADD_NEW_PAGE]); + }, [searchBlocks]); + + const hideMenu = useCallback(() => { + setIsShow(false); + setIsNewPage(false); + editor.blockHelper.removeDoubleLinkSearchSlash(blockId); + editor.scrollManager.unLock(); + }, [blockId, editor.blockHelper, editor.scrollManager]); + + const handleSearch = useCallback( + async (event: React.KeyboardEvent) => { + const { type, anchorNode } = editor.selection.currentSelectInfo; + if ( + type === 'Range' && + anchorNode && + editor.blockHelper.isSelectionCollapsed(anchorNode.id) + ) { + const text = editor.blockHelper.getBlockTextBeforeSelection( + anchorNode.id + ); + + if (text.endsWith('[[')) { + if ( + [ + 'ArrowRight', + 'ArrowLeft', + 'ArrowUp', + 'ArrowDown', + ].includes(event.key) + ) { + return; + } + if (event.key === 'Backspace') { + hideMenu(); + return; + } + setBlockId(anchorNode.id); + editor.blockHelper.removeDoubleLinkSearchSlash(blockId); + setTimeout(() => { + const textSelection = + editor.blockHelper.selectionToSlateRange( + anchorNode.id, + editor.selection.currentSelectInfo + .browserSelection + ); + if (textSelection) { + const { anchor } = textSelection; + editor.blockHelper.setDoubleLinkSearchSlash( + anchorNode.id, + anchor + ); + } + }); + setSearchText(''); + setIsShow(true); + editor.scrollManager.lock(); + const rect = + editor.selection.currentSelectInfo?.browserSelection + ?.getRangeAt(0) + ?.getBoundingClientRect(); + if (rect) { + const rectTop = rect.top; + const { top, left } = + editor.container.getBoundingClientRect(); + setReferenceMenuStyle({ + top: rectTop - top, + left: rect.left - left, + height: rect.height, + }); + setAnchorEl(ref.current); + } + } + } + if (isShow) { + const searchText = + editor.blockHelper.getDoubleLinkSearchSlashText(blockId); + if (searchText && searchText.startsWith('[[')) { + setSearchText(searchText.slice(2).trim()); + } + } + }, + [editor, isShow, blockId, hideMenu] + ); + + const handleKeyup = useCallback( + (event: React.KeyboardEvent) => handleSearch(event), + [handleSearch] + ); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.code === 'Escape') { + hideMenu(); + } + }, + [hideMenu] + ); + + useEffect(() => { + const sub = hooks + .get(HookType.ON_ROOT_NODE_KEYUP) + .subscribe(handleKeyup); + sub.add( + hooks + .get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE) + .subscribe(handleKeyDown) + ); + + return () => { + sub.unsubscribe(); + }; + }, [handleKeyup, handleKeyDown, hooks]); + + const handleSelected = async (linkBlockId: string) => { + if (blockId) { + if (linkBlockId === ADD_NEW_SUB_PAGE) { + handleAddSubPage(); + return; + } + if (linkBlockId === ADD_NEW_PAGE) { + setIsNewPage(true); + return; + } + if (isNewPage) { + const newPage = await services.api.editorBlock.create({ + workspace: editor.workspace, + type: 'page' as const, + }); + await services.api.pageTree.addChildPageToWorkspace( + editor.workspace, + linkBlockId, + newPage.id + ); + linkBlockId = newPage.id; + } + editor.blockHelper.setSelectDoubleLinkSearchSlash(blockId); + await editor.blockHelper.insertDoubleLink( + editor.workspace, + linkBlockId, + blockId + ); + hideMenu(); + } + }; + + const handleClose = () => { + blockId && editor.blockHelper.removeDoubleLinkSearchSlash(blockId); + }; + + const handleAddSubPage = async () => { + const newPage = await services.api.editorBlock.create({ + workspace: editor.workspace, + type: 'page' as const, + }); + setIsNewPage(false); + services.api.editorBlock.update({ + id: newPage.id, + workspace: editor.workspace, + properties: { + text: { value: [{ text: searchText }] }, + }, + }); + await services.api.pageTree.addChildPageToWorkspace( + editor.workspace, + getPageId(), + newPage.id + ); + handleSelected(newPage.id); + }; + + return ( +
+ hideMenu()}> + + {({ TransitionProps }) => ( + + + + + {}} + /> + + + + + + )} + + +
+ ); +}; + +const DoubleLinkMenuWrapper = styled('div')({ + zIndex: 1, +}); + +const SearchContainer = styled('div')({ + padding: '8px 8px', + input: { + height: '28px', + padding: '5px 10px', + with: '300px', + }, +}); diff --git a/libs/components/editor-plugins/src/menu/reference-menu/Plugin.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/Plugin.tsx similarity index 79% rename from libs/components/editor-plugins/src/menu/reference-menu/Plugin.tsx rename to libs/components/editor-plugins/src/menu/double-link-menu/Plugin.tsx index 688adfd147..3933bf5f1c 100644 --- a/libs/components/editor-plugins/src/menu/reference-menu/Plugin.tsx +++ b/libs/components/editor-plugins/src/menu/double-link-menu/Plugin.tsx @@ -1,12 +1,11 @@ import { StrictMode } from 'react'; - import { BasePlugin } from '../../base-plugin'; import { PluginRenderRoot } from '../../utils'; -import { ReferenceMenu } from './ReferenceMenu'; +import { DoubleLinkMenu } from './DoubleLinkMenu'; const PLUGIN_NAME = 'reference-menu'; -export class ReferenceMenuPlugin extends BasePlugin { +export class DoubleLinkMenuPlugin extends BasePlugin { private _root?: PluginRenderRoot; public static override get pluginName(): string { @@ -22,7 +21,7 @@ export class ReferenceMenuPlugin extends BasePlugin { this._root?.render( - + ); } diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/index.ts b/libs/components/editor-plugins/src/menu/double-link-menu/index.ts new file mode 100644 index 0000000000..e287e74e03 --- /dev/null +++ b/libs/components/editor-plugins/src/menu/double-link-menu/index.ts @@ -0,0 +1 @@ +export { DoubleLinkMenuPlugin } from './Plugin'; diff --git a/libs/components/editor-plugins/src/menu/index.ts b/libs/components/editor-plugins/src/menu/index.ts index 50d4133651..190e3a5109 100644 --- a/libs/components/editor-plugins/src/menu/index.ts +++ b/libs/components/editor-plugins/src/menu/index.ts @@ -1,9 +1,7 @@ +export { CommandMenuPlugin } from './command-menu'; +export { DoubleLinkMenuPlugin } from './double-link-menu'; +export { GroupMenuPlugin } from './group-menu'; export { InlineMenuPlugin } from './inline-menu'; export { LeftMenuPlugin } from './left-menu/LeftMenuPlugin'; -export { CommandMenuPlugin } from './command-menu'; -export { ReferenceMenuPlugin } from './reference-menu'; -export { SelectionGroupPlugin } from './selection-group-menu'; - export { MENU_WIDTH as menuWidth } from './left-menu/menu-config'; - -export { GroupMenuPlugin } from './group-menu'; +export { SelectionGroupPlugin } from './selection-group-menu'; diff --git a/libs/components/editor-plugins/src/menu/reference-menu/Container.tsx b/libs/components/editor-plugins/src/menu/reference-menu/Container.tsx deleted file mode 100644 index 42ca2d679d..0000000000 --- a/libs/components/editor-plugins/src/menu/reference-menu/Container.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import React, { useEffect, useState, useCallback, useRef } from 'react'; - -import { Virgo, PluginHooks, HookType } from '@toeverything/framework/virgo'; -import { - CommonList, - CommonListItem, - commonListContainer, -} from '@toeverything/components/common'; -import { domToRect } from '@toeverything/utils'; -import { styled } from '@toeverything/components/ui'; - -import { QueryResult } from '../../search'; - -export type ReferenceMenuContainerProps = { - editor: Virgo; - hooks: PluginHooks; - style?: React.CSSProperties; - isShow?: boolean; - blockId: string; - onSelected?: (item: string) => void; - onClose?: () => void; - searchBlocks?: QueryResult; - types?: Array; -}; - -export const ReferenceMenuContainer = ({ - hooks, - isShow = false, - onSelected, - onClose, - types, - searchBlocks, - style, -}: ReferenceMenuContainerProps) => { - const menu_ref = useRef(null); - const [current_item, set_current_item] = useState(); - const [need_check_into_view, set_need_check_into_view] = - useState(false); - - useEffect(() => { - if (need_check_into_view) { - if (current_item && menu_ref.current) { - const item_ele = - menu_ref.current.querySelector( - `.item-${current_item}` - ); - const scroll_ele = - menu_ref.current.querySelector( - `.${commonListContainer}` - ); - if (item_ele) { - const itemRect = domToRect(item_ele); - const scrollRect = domToRect(scroll_ele); - if ( - itemRect.top < scrollRect.top || - itemRect.bottom > scrollRect.bottom - ) { - // IMP: may be do it with self function - item_ele.scrollIntoView({ - block: 'nearest', - }); - } - } - } - set_need_check_into_view(false); - } - }, [need_check_into_view, current_item]); - - useEffect(() => { - if (isShow && types && !current_item) set_current_item(types[0]); - if (!isShow) onClose?.(); - }, [current_item, isShow, onClose, types]); - - useEffect(() => { - if (isShow && types) { - if (!types.includes(current_item)) { - set_need_check_into_view(true); - if (types.length) { - set_current_item(types[0]); - } else { - set_current_item(undefined); - } - } - } - }, [isShow, types, current_item]); - - const handle_click_up = useCallback( - (event: React.KeyboardEvent) => { - if (isShow && types && event.code === 'ArrowUp') { - event.preventDefault(); - if (!current_item && types.length) { - set_current_item(types[types.length - 1]); - } - if (current_item) { - const idx = types.indexOf(current_item); - if (idx > 0) { - set_need_check_into_view(true); - set_current_item(types[idx - 1]); - } - } - } - }, - [isShow, types, current_item] - ); - - const handle_click_down = useCallback( - (event: React.KeyboardEvent) => { - if (isShow && types && event.code === 'ArrowDown') { - event.preventDefault(); - if (!current_item && types.length) { - set_current_item(types[0]); - } - if (current_item) { - const idx = types.indexOf(current_item); - if (idx < types.length - 1) { - set_need_check_into_view(true); - set_current_item(types[idx + 1]); - } - } - } - }, - [isShow, types, current_item] - ); - - const handle_click_enter = useCallback( - async (event: React.KeyboardEvent) => { - if (isShow && event.code === 'Enter' && current_item) { - event.preventDefault(); - onSelected && onSelected(current_item); - } - }, - [isShow, current_item, onSelected] - ); - - const handle_key_down = useCallback( - (event: React.KeyboardEvent) => { - handle_click_up(event); - handle_click_down(event); - handle_click_enter(event); - }, - [handle_click_up, handle_click_down, handle_click_enter] - ); - - useEffect(() => { - const sub = hooks - .get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE) - .subscribe(handle_key_down); - - return () => { - sub.unsubscribe(); - }; - }, [hooks, handle_key_down]); - - return isShow ? ( - - - ({ block } as CommonListItem) - ) || [] - } - onSelected={type => onSelected?.(type)} - currentItem={current_item} - setCurrentItem={set_current_item} - /> - - - ) : null; -}; - -const RootContainer = styled('div')(({ theme }) => ({ - position: 'fixed', - zIndex: 1, - maxHeight: '525px', - borderRadius: '10px', - boxShadow: theme.affine.shadows.shadow1, - backgroundColor: '#fff', - padding: '8px 4px', -})); - -const ContentContainer = styled('div')(({ theme }) => ({ - display: 'flex', - overflow: 'hidden', - maxHeight: '493px', -})); diff --git a/libs/components/editor-plugins/src/menu/reference-menu/ReferenceMenu.tsx b/libs/components/editor-plugins/src/menu/reference-menu/ReferenceMenu.tsx deleted file mode 100644 index af1cd96cea..0000000000 --- a/libs/components/editor-plugins/src/menu/reference-menu/ReferenceMenu.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { MuiClickAwayListener, styled } from '@toeverything/components/ui'; -import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo'; -import { Point } from '@toeverything/utils'; - -import { ReferenceMenuContainer } from './Container'; -import { QueryBlocks, QueryResult } from '../../search'; - -export type ReferenceMenuProps = { - editor: Virgo; - hooks: PluginHooks; - style?: { left: number; top: number }; -}; - -export type RefLinkComponent = { - type: 'reflink'; - reference: string; -}; - -const BEFORE_REGEX = /\[\[(.*)$/; - -export const ReferenceMenu = ({ editor, hooks, style }: ReferenceMenuProps) => { - const [is_show, set_is_show] = useState(false); - const [block_id, set_block_id] = useState(); - const [position, set_position] = useState(new Point(0, 0)); - - const [search_text, set_search_text] = useState(''); - const [search_blocks, set_search_blocks] = useState([]); - - useEffect(() => { - QueryBlocks(editor, search_text, result => set_search_blocks(result)); - }, [editor, search_text]); - - const search_block_ids = useMemo( - () => Object.values(search_blocks).map(({ id }) => id), - [search_blocks] - ); - - const handle_search = useCallback( - async (event: React.KeyboardEvent) => { - const { type, anchorNode } = editor.selection.currentSelectInfo; - if ( - type === 'Range' && - anchorNode && - editor.blockHelper.isSelectionCollapsed(anchorNode.id) - ) { - const text = editor.blockHelper.getBlockTextBeforeSelection( - anchorNode.id - ); - const matched = BEFORE_REGEX.exec(text)?.[1]; - - if (typeof matched === 'string') { - if (event.key === '[') set_is_show(true); - - set_block_id(anchorNode.id); - set_search_text(matched); - - const rect = - editor.selection.currentSelectInfo?.browserSelection - ?.getRangeAt(0) - ?.getBoundingClientRect(); - if (rect) { - set_position(new Point(rect.left, rect.top + 24)); - } - } else if (is_show) { - set_is_show(false); - } - } - }, - [editor, is_show] - ); - - const handle_keyup = useCallback( - (event: React.KeyboardEvent) => handle_search(event), - [handle_search] - ); - - const handle_key_down = useCallback( - (event: React.KeyboardEvent) => { - if (event.code === 'Escape') { - set_is_show(false); - } - }, - [] - ); - - useEffect(() => { - const sub = hooks - .get(HookType.ON_ROOT_NODE_KEYUP) - .subscribe(handle_keyup); - sub.add( - hooks - .get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE) - .subscribe(handle_key_down) - ); - - return () => { - sub.unsubscribe(); - }; - }, [handle_keyup, handle_key_down, hooks]); - - const handle_selected = async (reference: string) => { - if (block_id) { - const { anchorNode } = editor.selection.currentSelectInfo; - editor.blockHelper.insertReference( - reference, - anchorNode.id, - editor.selection.currentSelectInfo?.browserSelection, - -search_text.length - 2 - ); - } - - set_is_show(false); - }; - - const handle_close = () => { - block_id && editor.blockHelper.removeSearchSlash(block_id); - }; - - return ( - - set_is_show(false)}> -
- -
-
-
- ); -}; - -const ReferenceMenuWrapper = styled('div')({ - position: 'absolute', - zIndex: 1, -}); diff --git a/libs/components/editor-plugins/src/menu/reference-menu/index.ts b/libs/components/editor-plugins/src/menu/reference-menu/index.ts deleted file mode 100644 index 0a3879e398..0000000000 --- a/libs/components/editor-plugins/src/menu/reference-menu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ReferenceMenuPlugin } from './Plugin'; diff --git a/libs/components/ui/src/button/ListButton.tsx b/libs/components/ui/src/button/ListButton.tsx index 1d6cead375..30e5ca9a31 100644 --- a/libs/components/ui/src/button/ListButton.tsx +++ b/libs/components/ui/src/button/ListButton.tsx @@ -1,6 +1,5 @@ import clsx from 'clsx'; import style9 from 'style9'; - import { SvgIconProps } from '../svg-icon'; import { BaseButton } from './base-button'; @@ -35,7 +34,7 @@ const styles = style9.create({ type ListButtonProps = { className?: string; - onClick: () => void; + onClick?: () => void; onMouseOver?: () => void; content?: string; children?: () => JSX.Element; diff --git a/libs/datasource/jwt/src/index.ts b/libs/datasource/jwt/src/index.ts index 50fec96d5e..664e0f21fa 100644 --- a/libs/datasource/jwt/src/index.ts +++ b/libs/datasource/jwt/src/index.ts @@ -1,7 +1,6 @@ /* eslint-disable max-lines */ import { DocumentSearchOptions } from 'flexsearch'; import LRUCache from 'lru-cache'; - import { AsyncDatabaseAdapter, BlockInstance, @@ -290,19 +289,22 @@ export class BlockClient< | string | Partial> ): Promise { - const promised_pages = await Promise.all( - this.search(part_of_title_or_content).flatMap(({ result }) => - result.map(async id => { - const page = this._pageMapping.get(id as string); - if (page) return page; - const block = await this.get(id as BlockTypeKeys); - return this.set_page(block); - }) - ) - ); - const pages = [ - ...new Set(promised_pages.filter((v): v is string => !!v)), - ]; + let pages = []; + if (part_of_title_or_content) { + const promisedPages = await Promise.all( + this.search(part_of_title_or_content).flatMap(({ result }) => + result.map(async id => { + const page = this._pageMapping.get(id as string); + if (page) return page; + const block = await this.get(id as BlockTypeKeys); + return this.set_page(block); + }) + ) + ); + pages = [...new Set(promisedPages.filter((v): v is string => !!v))]; + } else { + pages = await this.getBlockByFlavor('page'); + } return Promise.all( this._blockIndexer.getMetadata(pages).map(async page => ({ content: this.get_decoded_content( From a58825dd2fbb5e336dbea30fd45887dc07ad8d13 Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 05:23:25 +0800 Subject: [PATCH 02/12] feat: Intra-line double link interaction --- libs/components/common/src/lib/list/index.tsx | 4 +- .../src/menu/double-link-menu/Container.tsx | 41 +-- .../menu/double-link-menu/DoubleLinkMenu.tsx | 347 +++++++++--------- 3 files changed, 195 insertions(+), 197 deletions(-) diff --git a/libs/components/common/src/lib/list/index.tsx b/libs/components/common/src/lib/list/index.tsx index 7de7e35b8e..c00a4e1415 100644 --- a/libs/components/common/src/lib/list/index.tsx +++ b/libs/components/common/src/lib/list/index.tsx @@ -57,7 +57,9 @@ export const CommonList = (props: MenuItemsProps) => { > {usedItems?.length ? ( usedItems.map((item, idx) => { - if (item.block) { + if (item.renderCustom) { + return item.renderCustom(item); + } else if (item.block) { return ( void; onClose?: () => void; @@ -25,7 +21,6 @@ export type DoubleLinkMenuContainerProps = { export const DoubleLinkMenuContainer = ({ hooks, - isShow = false, onSelected, onClose, types, @@ -66,12 +61,13 @@ export const DoubleLinkMenuContainer = ({ }, [needCheckIntoView, currentItem]); useEffect(() => { - if (isShow && types && !currentItem) setCurrentItem(types[0]); - if (!isShow) onClose?.(); - }, [currentItem, isShow, onClose, types]); + if (types && !currentItem) { + setCurrentItem(types[0]); + } + }, [currentItem, onClose, types]); useEffect(() => { - if (isShow && types) { + if (types) { if (!types.includes(currentItem)) { setNeedCheckIntoView(true); if (types.length) { @@ -81,11 +77,11 @@ export const DoubleLinkMenuContainer = ({ } } } - }, [isShow, types, currentItem]); + }, [types, currentItem]); const handleClickUp = useCallback( (event: React.KeyboardEvent) => { - if (isShow && types && event.code === 'ArrowUp') { + if (types && event.code === 'ArrowUp') { event.preventDefault(); if (!currentItem && types.length) { setCurrentItem(types[types.length - 1]); @@ -99,12 +95,12 @@ export const DoubleLinkMenuContainer = ({ } } }, - [isShow, types, currentItem] + [types, currentItem] ); const handleClickDown = useCallback( (event: React.KeyboardEvent) => { - if (isShow && types && event.code === 'ArrowDown') { + if (types && event.code === 'ArrowDown') { event.preventDefault(); if (!currentItem && types.length) { setCurrentItem(types[0]); @@ -118,17 +114,17 @@ export const DoubleLinkMenuContainer = ({ } } }, - [isShow, types, currentItem] + [types, currentItem] ); const handleClickEnter = useCallback( async (event: React.KeyboardEvent) => { - if (isShow && event.code === 'Enter' && currentItem) { + if (event.code === 'Enter' && currentItem) { event.preventDefault(); onSelected && onSelected(currentItem); } }, - [isShow, currentItem, onSelected] + [currentItem, onSelected] ); const handleKeyDown = useCallback( @@ -150,7 +146,7 @@ export const DoubleLinkMenuContainer = ({ }; }, [hooks, handleKeyDown]); - return isShow ? ( + return ( - ) : null; + ); }; const RootContainer = styled('div')(({ theme }) => ({ - // position: 'fixed', zIndex: 1, maxHeight: '525px', borderRadius: '10px', 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 1f545809a3..e79ab7b90b 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 @@ -1,10 +1,10 @@ import { CommonListItem } from '@toeverything/components/common'; import { AddIcon } from '@toeverything/components/icons'; import { + Input, ListButton, MuiClickAwayListener, MuiGrow as Grow, - MuiOutlinedInput as OutlinedInput, MuiPaper as Paper, MuiPopper as Popper, styled, @@ -13,6 +13,7 @@ import { services } from '@toeverything/datasource/db-service'; import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo'; import { getPageId } from '@toeverything/utils'; import React, { + ChangeEvent, useCallback, useEffect, useMemo, @@ -24,6 +25,7 @@ import { DoubleLinkMenuContainer } from './Container'; const ADD_NEW_SUB_PAGE = 'AddNewSubPage'; const ADD_NEW_PAGE = 'AddNewPage'; +const ARRAY_KEYS = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown']; export type DoubleLinkMenuProps = { editor: Virgo; @@ -31,7 +33,7 @@ export type DoubleLinkMenuProps = { style?: { left: number; top: number }; }; -type DoubleMenuStyle = { +type DoubleLinkMenuStyle = { left: number; top: number; height: number; @@ -42,58 +44,63 @@ export const DoubleLinkMenu = ({ hooks, style, }: DoubleLinkMenuProps) => { - const [isShow, setIsShow] = useState(false); - const [blockId, setBlockId] = useState(); - const [searchText, setSearchText] = useState(''); - const [searchBlocks, setSearchBlocks] = useState([]); - const [items, setItems] = useState([]); - const [isNewPage, setIsNewPage] = useState(false); + const [isOpen, setIsOpen] = useState(false); const [anchorEl, setAnchorEl] = useState(null); - const ref = useRef(); - const ref1 = useRef(); - const [referenceMenuStyle, setReferenceMenuStyle] = - useState({ + const dialogRef = useRef(); + const newPageSearchRef = useRef(); + const [doubleLinkMenuStyle, setDoubleLinkMenuStyle] = + useState({ left: 0, top: 0, height: 0, }); - useEffect(() => { - QueryBlocks(editor, searchText, result => { - setSearchBlocks(result); - ref1?.current?.focus(); - const items: CommonListItem[] = []; - if (searchBlocks?.length > 0) { - if (isNewPage) { - items.push({ - renderCustom: () => { - return ; - }, - }); - } else { - items.push({ - renderCustom: () => { - return ; - }, - }); - } - items.push( - ...(searchBlocks?.map( - block => ({ block } as CommonListItem) - ) || []) - ); - } + const [curBlockId, setCurBlockId] = useState(); + const [searchText, setSearchText] = useState(''); + const [inAddNewPage, setInAddNewPage] = useState(false); + const [filterText, setFilterText] = useState(''); + const [searchResultBlocks, setSearchResultBlocks] = useState( + [] + ); - if (items.length > 0) { - items.push({ divider: 'newPage' }); - } + 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({ - content: { - id: ADD_NEW_SUB_PAGE, - content: 'Add new sub-page', - icon: AddIcon, + renderCustom: () => { + return ( + + ); }, }); + items.push( + ...(searchResultBlocks?.map( + block => ({ block } as CommonListItem) + ) || []) + ); + } + + if (items.length > 0) { + items.push({ divider: 'newPage' }); + } + items.push({ + content: { + id: ADD_NEW_SUB_PAGE, + content: 'Add new sub-page', + icon: AddIcon, + }, + }); + !inAddNewPage && items.push({ content: { id: ADD_NEW_PAGE, @@ -101,26 +108,28 @@ export const DoubleLinkMenu = ({ icon: AddIcon, }, }); + return items; + }, [searchResultBlocks, inAddNewPage]); - setItems(items); + useEffect(() => { + const text = inAddNewPage ? filterText : searchText; + QueryBlocks(editor, text, result => { + setSearchResultBlocks(result); }); - }, [editor, searchText, isNewPage]); - - const types = useMemo(() => { - return Object.values(searchBlocks) - .map(({ id }) => id) - .concat([ADD_NEW_SUB_PAGE, ADD_NEW_PAGE]); - }, [searchBlocks]); + }, [editor, searchText, filterText, inAddNewPage]); const hideMenu = useCallback(() => { - setIsShow(false); - setIsNewPage(false); - editor.blockHelper.removeDoubleLinkSearchSlash(blockId); + setIsOpen(false); + setInAddNewPage(false); + editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId); editor.scrollManager.unLock(); - }, [blockId, editor.blockHelper, editor.scrollManager]); + }, [curBlockId, editor]); - const handleSearch = useCallback( + const searchChange = useCallback( async (event: React.KeyboardEvent) => { + if (ARRAY_KEYS.includes(event.key)) { + return; + } const { type, anchorNode } = editor.selection.currentSelectInfo; if ( type === 'Range' && @@ -130,24 +139,31 @@ export const DoubleLinkMenu = ({ const text = editor.blockHelper.getBlockTextBeforeSelection( anchorNode.id ); - if (text.endsWith('[[')) { - if ( - [ - 'ArrowRight', - 'ArrowLeft', - 'ArrowUp', - 'ArrowDown', - ].includes(event.key) - ) { - return; - } if (event.key === 'Backspace') { hideMenu(); return; } - setBlockId(anchorNode.id); - editor.blockHelper.removeDoubleLinkSearchSlash(blockId); + editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId); + setCurBlockId(anchorNode.id); + 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(); + setDoubleLinkMenuStyle({ + top: rectTop - top, + left: clientRect.left - left, + height: clientRect.height, + }); + setAnchorEl(dialogRef.current); + } setTimeout(() => { const textSelection = editor.blockHelper.selectionToSlateRange( @@ -163,40 +179,22 @@ export const DoubleLinkMenu = ({ ); } }); - setSearchText(''); - setIsShow(true); - editor.scrollManager.lock(); - const rect = - editor.selection.currentSelectInfo?.browserSelection - ?.getRangeAt(0) - ?.getBoundingClientRect(); - if (rect) { - const rectTop = rect.top; - const { top, left } = - editor.container.getBoundingClientRect(); - setReferenceMenuStyle({ - top: rectTop - top, - left: rect.left - left, - height: rect.height, - }); - setAnchorEl(ref.current); - } } } - if (isShow) { + if (isOpen) { const searchText = - editor.blockHelper.getDoubleLinkSearchSlashText(blockId); + editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId); if (searchText && searchText.startsWith('[[')) { setSearchText(searchText.slice(2).trim()); } } }, - [editor, isShow, blockId, hideMenu] + [editor, isOpen, curBlockId, hideMenu] ); const handleKeyup = useCallback( - (event: React.KeyboardEvent) => handleSearch(event), - [handleSearch] + (event: React.KeyboardEvent) => searchChange(event), + [searchChange] ); const handleKeyDown = useCallback( @@ -223,75 +221,86 @@ export const DoubleLinkMenu = ({ }; }, [handleKeyup, handleKeyDown, hooks]); - const handleSelected = async (linkBlockId: string) => { - if (blockId) { - if (linkBlockId === ADD_NEW_SUB_PAGE) { - handleAddSubPage(); - return; - } - if (linkBlockId === ADD_NEW_PAGE) { - setIsNewPage(true); - return; - } - if (isNewPage) { - const newPage = await services.api.editorBlock.create({ - workspace: editor.workspace, - type: 'page' as const, - }); - await services.api.pageTree.addChildPageToWorkspace( - editor.workspace, - linkBlockId, - newPage.id - ); - linkBlockId = newPage.id; - } - editor.blockHelper.setSelectDoubleLinkSearchSlash(blockId); + const insertDoubleLink = useCallback( + async (pageId: string) => { + editor.blockHelper.setSelectDoubleLinkSearchSlash(curBlockId); await editor.blockHelper.insertDoubleLink( editor.workspace, - linkBlockId, - blockId + pageId, + curBlockId ); hideMenu(); + }, + [editor, curBlockId, hideMenu] + ); + + const addSubPage = useCallback( + async (parentPageId: string) => { + const newPage = await services.api.editorBlock.create({ + workspace: editor.workspace, + type: 'page' as const, + }); + services.api.editorBlock.update({ + id: newPage.id, + workspace: editor.workspace, + properties: { + text: { value: [{ text: searchText }] }, + }, + }); + await services.api.pageTree.addChildPageToWorkspace( + editor.workspace, + parentPageId, + newPage.id + ); + return newPage.id; + }, + [searchText, editor] + ); + + const handleSelected = async (id: string) => { + if (curBlockId) { + if (id === ADD_NEW_PAGE) { + setInAddNewPage(true); + setTimeout(() => { + newPageSearchRef.current?.focus(); + }); + return; + } + if (id === ADD_NEW_SUB_PAGE) { + const pageId = await addSubPage(getPageId()); + insertDoubleLink(pageId); + return; + } + if (inAddNewPage) { + const pageId = await addSubPage(id); + insertDoubleLink(pageId); + } else { + insertDoubleLink(id); + } } }; - const handleClose = () => { - blockId && editor.blockHelper.removeDoubleLinkSearchSlash(blockId); - }; + const handleFilterChange = useCallback( + async (e: ChangeEvent) => { + const text = e.target.value; - const handleAddSubPage = async () => { - const newPage = await services.api.editorBlock.create({ - workspace: editor.workspace, - type: 'page' as const, - }); - setIsNewPage(false); - services.api.editorBlock.update({ - id: newPage.id, - workspace: editor.workspace, - properties: { - text: { value: [{ text: searchText }] }, - }, - }); - await services.api.pageTree.addChildPageToWorkspace( - editor.workspace, - getPageId(), - newPage.id - ); - handleSelected(newPage.id); - }; + await setFilterText(text); + }, + [] + ); return (
hideMenu()}> - - - {}} + {inAddNewPage && ( + + - - - + + )} + )} @@ -332,15 +342,6 @@ export const DoubleLinkMenu = ({ ); }; -const DoubleLinkMenuWrapper = styled('div')({ - zIndex: 1, -}); - -const SearchContainer = styled('div')({ - padding: '8px 8px', - input: { - height: '28px', - padding: '5px 10px', - with: '300px', - }, +const NewPageSearchContainer = styled('div')({ + padding: '8px 8px 0px 8px', }); From 438d814d49ff3421a0941db2c14ac6f7a99c2318 Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 06:17:30 +0800 Subject: [PATCH 03/12] feat: Intra-line double link interaction --- .../common/src/lib/text/EditableText.tsx | 4 +-- .../src/menu/double-link-menu/Container.tsx | 3 +- .../menu/double-link-menu/DoubleLinkMenu.tsx | 33 +++++++++++++++++-- .../editor-plugins/src/search/Search.tsx | 15 ++++----- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/libs/components/common/src/lib/text/EditableText.tsx b/libs/components/common/src/lib/text/EditableText.tsx index 7bc6c3904a..8637246d7d 100644 --- a/libs/components/common/src/lib/text/EditableText.tsx +++ b/libs/components/common/src/lib/text/EditableText.tsx @@ -1,5 +1,4 @@ /* eslint-disable max-lines */ -import { SearchIcon } from '@toeverything/components/icons'; import { ErrorBoundary, isEqual } from '@toeverything/utils'; import isHotkey from 'is-hotkey'; import isUrl from 'is-url'; @@ -844,8 +843,7 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => { if (leaf.doubleLinkSearch) { customChildren = ( - - + {customChildren} ); diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx index 2fa8c71ba2..dd62fd1def 100644 --- a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx +++ b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx @@ -166,7 +166,6 @@ export const DoubleLinkMenuContainer = ({ const RootContainer = styled('div')(({ theme }) => ({ zIndex: 1, - maxHeight: '525px', borderRadius: '10px', boxShadow: theme.affine.shadows.shadow1, backgroundColor: '#fff', @@ -176,5 +175,5 @@ const RootContainer = styled('div')(({ theme }) => ({ const ContentContainer = styled('div')(({ theme }) => ({ display: 'flex', overflow: 'hidden', - maxHeight: '493px', + maxHeight: '300px', })); 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 e79ab7b90b..21fb7c1afc 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 @@ -56,7 +56,7 @@ export const DoubleLinkMenu = ({ }); const [curBlockId, setCurBlockId] = useState(); - const [searchText, setSearchText] = useState(''); + const [searchText, setSearchText] = useState(); const [inAddNewPage, setInAddNewPage] = useState(false); const [filterText, setFilterText] = useState(''); const [searchResultBlocks, setSearchResultBlocks] = useState( @@ -85,7 +85,13 @@ export const DoubleLinkMenu = ({ }); items.push( ...(searchResultBlocks?.map( - block => ({ block } as CommonListItem) + block => + ({ + block: { + ...block, + content: block.content || 'Untitled', + }, + } as CommonListItem) ) || []) ); } @@ -199,11 +205,32 @@ export const DoubleLinkMenu = ({ const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { + if (!isOpen) { + return; + } if (event.code === 'Escape') { hideMenu(); } + const { type, anchorNode } = editor.selection.currentSelectInfo; + if ( + type === 'Range' && + anchorNode && + editor.blockHelper.isSelectionCollapsed(anchorNode.id) + ) { + const text = editor.blockHelper.getBlockTextBeforeSelection( + anchorNode.id + ); + if (text.endsWith('[[')) { + if (event.key === 'Backspace') { + event.preventDefault(); + event.stopPropagation(); + hideMenu(); + return; + } + } + } }, - [hideMenu] + [hideMenu, editor, isOpen] ); useEffect(() => { diff --git a/libs/components/editor-plugins/src/search/Search.tsx b/libs/components/editor-plugins/src/search/Search.tsx index 6b02b6334e..b689345f64 100644 --- a/libs/components/editor-plugins/src/search/Search.tsx +++ b/libs/components/editor-plugins/src/search/Search.tsx @@ -1,16 +1,15 @@ -import { useCallback, useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router'; -import style9 from 'style9'; - import { BlockPreview } from '@toeverything/components/common'; import { - TransitionsModal, MuiBox as Box, MuiBox, styled, + TransitionsModal, } from '@toeverything/components/ui'; -import { Virgo, BlockEditor } from '@toeverything/framework/virgo'; +import { BlockEditor, Virgo } from '@toeverything/framework/virgo'; import { throttle } from '@toeverything/utils'; +import { useCallback, useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router'; +import style9 from 'style9'; const styles = style9.create({ wrapper: { @@ -37,9 +36,7 @@ const query_blocks = ( search: string, callback: (result: QueryResult) => void ) => { - (editor as BlockEditor) - .search(search) - .then(pages => callback(pages.filter(b => !!b.content))); + (editor as BlockEditor).search(search).then(pages => callback(pages)); }; export const QueryBlocks = throttle(query_blocks, 500); From f0f60654e839dc1b730cc123a59917a4dcad4f7a Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 09:57:04 +0800 Subject: [PATCH 04/12] feat: Intra-line double link interaction --- .../src/lib/text/plugins/DoubleLink.tsx | 1 + .../common/src/lib/text/slate-utils.ts | 4 ++ .../menu/double-link-menu/DoubleLinkMenu.tsx | 62 ++++++++++--------- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/libs/components/common/src/lib/text/plugins/DoubleLink.tsx b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx index 8160731bf2..822b684323 100644 --- a/libs/components/common/src/lib/text/plugins/DoubleLink.tsx +++ b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx @@ -20,6 +20,7 @@ export const DoubleLinkComponent = ({ attributes, children, element }: any) => { {children} diff --git a/libs/components/common/src/lib/text/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts index 9b1fff664c..a1cf6f2c95 100644 --- a/libs/components/common/src/lib/text/slate-utils.ts +++ b/libs/components/common/src/lib/text/slate-utils.ts @@ -599,6 +599,10 @@ class SlateUtils { const textChildren: Text[] = []; for (const child of fragmentChildren) { + if ((child as any).type === 'link') { + textChildren.push(child as Text); + continue; + } if (!('text' in child)) { console.error('Debug information:', point1, point2, fragment); throw new Error('Fragment exists nested!'); 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 21fb7c1afc..c012010eb1 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 @@ -11,7 +11,6 @@ import { } from '@toeverything/components/ui'; import { services } from '@toeverything/datasource/db-service'; import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo'; -import { getPageId } from '@toeverything/utils'; import React, { ChangeEvent, useCallback, @@ -20,12 +19,13 @@ import React, { useRef, useState, } from 'react'; +import { useParams } from 'react-router-dom'; import { QueryBlocks, QueryResult } from '../../search'; import { DoubleLinkMenuContainer } from './Container'; const ADD_NEW_SUB_PAGE = 'AddNewSubPage'; const ADD_NEW_PAGE = 'AddNewPage'; -const ARRAY_KEYS = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown']; +const ARRAY_CODES = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown']; export type DoubleLinkMenuProps = { editor: Virgo; @@ -44,6 +44,7 @@ export const DoubleLinkMenu = ({ hooks, style, }: DoubleLinkMenuProps) => { + const { page_id: curPageId } = useParams(); const [isOpen, setIsOpen] = useState(false); const [anchorEl, setAnchorEl] = useState(null); const dialogRef = useRef(); @@ -120,9 +121,12 @@ export const DoubleLinkMenu = ({ useEffect(() => { const text = inAddNewPage ? filterText : searchText; QueryBlocks(editor, text, result => { + if (!inAddNewPage) { + result = result.filter(item => item.id !== curPageId); + } setSearchResultBlocks(result); }); - }, [editor, searchText, filterText, inAddNewPage]); + }, [editor, searchText, filterText, inAddNewPage, curPageId]); const hideMenu = useCallback(() => { setIsOpen(false); @@ -133,23 +137,31 @@ export const DoubleLinkMenu = ({ const searchChange = useCallback( async (event: React.KeyboardEvent) => { - if (ARRAY_KEYS.includes(event.key)) { + if (ARRAY_CODES.includes(event.code)) { return; } + if (event.code === 'Backspace') { + const searchText = + editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId); + if (!searchText || searchText === '[[') { + hideMenu(); + event.preventDefault(); + event.stopPropagation(); + return; + } + } const { type, anchorNode } = editor.selection.currentSelectInfo; if ( - type === 'Range' && - anchorNode && - editor.blockHelper.isSelectionCollapsed(anchorNode.id) + !isOpen || + (type === 'Range' && + anchorNode && + anchorNode.id !== curBlockId && + editor.blockHelper.isSelectionCollapsed(anchorNode.id)) ) { const text = editor.blockHelper.getBlockTextBeforeSelection( anchorNode.id ); if (text.endsWith('[[')) { - if (event.key === 'Backspace') { - hideMenu(); - return; - } editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId); setCurBlockId(anchorNode.id); setSearchText(''); @@ -211,26 +223,18 @@ export const DoubleLinkMenu = ({ if (event.code === 'Escape') { hideMenu(); } - const { type, anchorNode } = editor.selection.currentSelectInfo; - if ( - type === 'Range' && - anchorNode && - editor.blockHelper.isSelectionCollapsed(anchorNode.id) - ) { - const text = editor.blockHelper.getBlockTextBeforeSelection( - anchorNode.id - ); - if (text.endsWith('[[')) { - if (event.key === 'Backspace') { - event.preventDefault(); - event.stopPropagation(); - hideMenu(); - return; - } + + if (event.code === 'Backspace') { + const searchText = + editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId); + if (!searchText || searchText === '[[') { + event.preventDefault(); + event.stopPropagation(); + return; } } }, - [hideMenu, editor, isOpen] + [hideMenu, editor, isOpen, curBlockId] ); useEffect(() => { @@ -294,7 +298,7 @@ export const DoubleLinkMenu = ({ return; } if (id === ADD_NEW_SUB_PAGE) { - const pageId = await addSubPage(getPageId()); + const pageId = await addSubPage(curPageId); insertDoubleLink(pageId); return; } From 1820135f3a4603ec11b4d89919b1de46b6595453 Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 10:27:26 +0800 Subject: [PATCH 05/12] feat: Intra-line double link interaction --- libs/components/common/src/lib/index.ts | 47 +++++++++---------- .../src/lib/text/plugins/DoubleLink.tsx | 9 ++++ 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/libs/components/common/src/lib/index.ts b/libs/components/common/src/lib/index.ts index 9a1eea5191..f42927223e 100644 --- a/libs/components/common/src/lib/index.ts +++ b/libs/components/common/src/lib/index.ts @@ -1,5 +1,6 @@ import { BaseEditor } from 'slate'; import { ReactEditor } from 'slate-react'; +import { DoubleLinkElement } from './text/plugins/DoubleLink'; import { LinkElement } from './text/plugins/link'; import { RefLinkElement } from './text/plugins/reflink'; @@ -8,7 +9,8 @@ export type CustomElement = | { type: string; children: CustomElement[] } | CustomText | LinkElement - | RefLinkElement; + | RefLinkElement + | DoubleLinkElement; declare module 'slate' { interface CustomTypes { Editor: BaseEditor & ReactEditor; @@ -19,35 +21,32 @@ declare module 'slate' { export { BlockPreview, StyledBlockPreview } from './block-preview'; export { default as Button } from './button'; -export type { CommonListItem } from './list'; -export { CommonList, BackLink, commonListContainer } from './list'; -export * from './Logo'; -export { default as Toolbar } from './toolbar'; export { CollapsibleTitle } from './collapsible-title'; - -export * from './text'; - +export * from './comming-soon/CommingSoon'; export { - NewpageIcon, + AddIcon, ClockIcon, - ViewSidebarIcon, + CloseIcon, + CodeBlockInlineIcon, + DocumentIcon, + FilterIcon, + FullScreenIcon, + HighlighterDuotoneIcon, + KanbanIcon, ListIcon, - SpaceIcon, + NewpageIcon, + PagesIcon, PencilDotDuotoneIcon, PencilDuotoneIcon, - HighlighterDuotoneIcon, - CodeBlockInlineIcon, - PagesIcon, - CloseIcon, - DocumentIcon, - TodoListIcon, - KanbanIcon, - TableIcon, - AddIcon, - FilterIcon, SorterIcon, - FullScreenIcon, + SpaceIcon, + TableIcon, + TodoListIcon, UnGroupIcon, + ViewSidebarIcon, } from './icon'; - -export * from './comming-soon/CommingSoon'; +export { BackLink, CommonList, commonListContainer } from './list'; +export type { CommonListItem } from './list'; +export * from './Logo'; +export * from './text'; +export { default as Toolbar } from './toolbar'; diff --git a/libs/components/common/src/lib/text/plugins/DoubleLink.tsx b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx index 822b684323..3bacd92f5f 100644 --- a/libs/components/common/src/lib/text/plugins/DoubleLink.tsx +++ b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx @@ -1,6 +1,15 @@ import { PagesIcon } from '@toeverything/components/icons'; import React, { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; +import { Descendant } from 'slate'; + +export type DoubleLinkElement = { + type: 'link'; + workspaceId: string; + blockId: string; + children: Descendant[]; + id: string; +}; export const DoubleLinkComponent = ({ attributes, children, element }: any) => { const navigate = useNavigate(); From 2406f9556fdc5e72127b64d07a5ecf46a4874d78 Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 11:13:15 +0800 Subject: [PATCH 06/12] feat: Intra-line double link interaction --- .../common/src/lib/text/slate-utils.ts | 9 ++- .../src/editor/block/async-block.ts | 28 ++++--- .../menu/double-link-menu/DoubleLinkMenu.tsx | 75 ++++++++++--------- 3 files changed, 57 insertions(+), 55 deletions(-) diff --git a/libs/components/common/src/lib/text/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts index a1cf6f2c95..476883a2c9 100644 --- a/libs/components/common/src/lib/text/slate-utils.ts +++ b/libs/components/common/src/lib/text/slate-utils.ts @@ -597,10 +597,11 @@ class SlateUtils { const fragmentChildren = firstFragment.children; - const textChildren: Text[] = []; - for (const child of fragmentChildren) { - if ((child as any).type === 'link') { - textChildren.push(child as Text); + const textChildren = []; + 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); continue; } if (!('text' in child)) { diff --git a/libs/components/editor-core/src/editor/block/async-block.ts b/libs/components/editor-core/src/editor/block/async-block.ts index 9eb156852e..73db5de087 100644 --- a/libs/components/editor-core/src/editor/block/async-block.ts +++ b/libs/components/editor-core/src/editor/block/async-block.ts @@ -508,21 +508,19 @@ export class AsyncBlock { id: item.blockId, }); - if (linkBlock) { - let children = linkBlock.getProperties().text?.value || []; - if (children.length === 1 && !children[0].text) { - children = [{ text: 'Untitled' }]; - } - if ( - children.map(v => v.text).join('') !== - (item.children || []).map((v: any) => v.text).join('') - ) { - const newItem = { - ...item, - children: children, - }; - values.splice(i, 1, newItem); - } + let children = linkBlock?.getProperties().text?.value || []; + if (children.length === 1 && !children[0].text) { + children = [{ text: 'Untitled' }]; + } + if ( + children.map(v => v.text).join('') !== + (item.children || []).map((v: any) => v.text).join('') + ) { + const newItem = { + ...item, + children: children, + }; + values.splice(i, 1, newItem); } } } 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 c012010eb1..eae70b40f4 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 @@ -135,6 +135,44 @@ export const DoubleLinkMenu = ({ editor.scrollManager.unLock(); }, [curBlockId, editor]); + const resetState = useCallback( + (preNodeId: string, nextNodeId: string) => { + editor.blockHelper.removeDoubleLinkSearchSlash(preNodeId); + 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(); + setDoubleLinkMenuStyle({ + top: rectTop - top, + left: clientRect.left - left, + height: clientRect.height, + }); + setAnchorEl(dialogRef.current); + } + setTimeout(() => { + const textSelection = editor.blockHelper.selectionToSlateRange( + nextNodeId, + editor.selection.currentSelectInfo.browserSelection + ); + if (textSelection) { + const { anchor } = textSelection; + editor.blockHelper.setDoubleLinkSearchSlash( + nextNodeId, + anchor + ); + } + }); + }, + [editor] + ); + const searchChange = useCallback( async (event: React.KeyboardEvent) => { if (ARRAY_CODES.includes(event.code)) { @@ -162,41 +200,7 @@ export const DoubleLinkMenu = ({ anchorNode.id ); if (text.endsWith('[[')) { - editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId); - setCurBlockId(anchorNode.id); - 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(); - setDoubleLinkMenuStyle({ - top: rectTop - top, - left: clientRect.left - left, - height: clientRect.height, - }); - setAnchorEl(dialogRef.current); - } - setTimeout(() => { - const textSelection = - editor.blockHelper.selectionToSlateRange( - anchorNode.id, - editor.selection.currentSelectInfo - .browserSelection - ); - if (textSelection) { - const { anchor } = textSelection; - editor.blockHelper.setDoubleLinkSearchSlash( - anchorNode.id, - anchor - ); - } - }); + resetState(curBlockId, anchorNode.id); } } if (isOpen) { @@ -372,7 +376,6 @@ export const DoubleLinkMenu = ({
); }; - const NewPageSearchContainer = styled('div')({ padding: '8px 8px 0px 8px', }); From 9411422faf5596579600c1daa9f4df5c7e20f412 Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 11:41:53 +0800 Subject: [PATCH 07/12] feat: Intra-line double link interaction --- .../src/menu/double-link-menu/Container.tsx | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx index dd62fd1def..d7bcfa9e19 100644 --- a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx +++ b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx @@ -19,14 +19,10 @@ export type DoubleLinkMenuContainerProps = { items?: CommonListItem[]; }; -export const DoubleLinkMenuContainer = ({ - hooks, - onSelected, - onClose, - types, - style, - items, -}: DoubleLinkMenuContainerProps) => { +export const DoubleLinkMenuContainer = ( + props: DoubleLinkMenuContainerProps +) => { + const { hooks, onSelected, onClose, types, style, items } = props; const menuRef = useRef(null); const [currentItem, setCurrentItem] = useState(); const [needCheckIntoView, setNeedCheckIntoView] = useState(false); @@ -79,42 +75,37 @@ export const DoubleLinkMenuContainer = ({ } }, [types, currentItem]); - const handleClickUp = useCallback( + const handleUpDownKey = useCallback( (event: React.KeyboardEvent) => { - if (types && event.code === 'ArrowUp') { + if (types && ['ArrowUp', 'ArrowDown'].includes(event.code)) { event.preventDefault(); + const isUpkey = event.code === 'ArrowUp'; if (!currentItem && types.length) { - setCurrentItem(types[types.length - 1]); + setCurrentItem(types[isUpkey ? types.length - 1 : 0]); } if (currentItem) { const idx = types.indexOf(currentItem); - if (idx > 0) { + if (isUpkey ? idx > 0 : idx < types.length - 1) { setNeedCheckIntoView(true); - setCurrentItem(types[idx - 1]); + setCurrentItem(types[isUpkey ? idx - 1 : idx + 1]); } } } }, - [types, currentItem] + [currentItem, types] + ); + const handleClickUp = useCallback( + (event: React.KeyboardEvent) => { + handleUpDownKey(event); + }, + [handleUpDownKey] ); const handleClickDown = useCallback( (event: React.KeyboardEvent) => { - if (types && event.code === 'ArrowDown') { - event.preventDefault(); - if (!currentItem && types.length) { - setCurrentItem(types[0]); - } - if (currentItem) { - const idx = types.indexOf(currentItem); - if (idx < types.length - 1) { - setNeedCheckIntoView(true); - setCurrentItem(types[idx + 1]); - } - } - } + handleUpDownKey(event); }, - [types, currentItem] + [handleUpDownKey] ); const handleClickEnter = useCallback( From c67d961d662cf99e9bf798dbc62cf02a69f04b8b Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 11:56:57 +0800 Subject: [PATCH 08/12] feat: Intra-line double link interaction --- .../src/menu/double-link-menu/Container.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx index d7bcfa9e19..7294a909b7 100644 --- a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx +++ b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx @@ -30,23 +30,22 @@ export const DoubleLinkMenuContainer = ( useEffect(() => { if (needCheckIntoView) { if (currentItem && menuRef.current) { - const itemEle = + const itemElement = menuRef.current.querySelector( `.item-${currentItem}` ); - const scrollEle = + const scrollElement = menuRef.current.querySelector( `.${commonListContainer}` ); - if (itemEle) { - const itemRect = domToRect(itemEle); - const scrollRect = domToRect(scrollEle); + if (itemElement) { + const itemRect = domToRect(itemElement); + const scrollRect = domToRect(scrollElement); if ( itemRect.top < scrollRect.top || itemRect.bottom > scrollRect.bottom ) { - // IMP: may be do it with self function - itemEle.scrollIntoView({ + itemElement.scrollIntoView({ block: 'nearest', }); } @@ -80,12 +79,16 @@ export const DoubleLinkMenuContainer = ( if (types && ['ArrowUp', 'ArrowDown'].includes(event.code)) { event.preventDefault(); const isUpkey = event.code === 'ArrowUp'; + const indexBound = isUpkey ? types.length - 1 : 0; if (!currentItem && types.length) { - setCurrentItem(types[isUpkey ? types.length - 1 : 0]); + setCurrentItem(types[indexBound]); } if (currentItem) { const idx = types.indexOf(currentItem); - if (isUpkey ? idx > 0 : idx < types.length - 1) { + const needChange = isUpkey + ? idx > indexBound + : idx < indexBound; + if (needChange) { setNeedCheckIntoView(true); setCurrentItem(types[isUpkey ? idx - 1 : idx + 1]); } From 33d8b551d4cb1ed8bb1c843c23ac9b4aae846c6d Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 12:06:50 +0800 Subject: [PATCH 09/12] feat: Intra-line double link interaction --- .../src/menu/double-link-menu/Container.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx index 7294a909b7..147f154129 100644 --- a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx +++ b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx @@ -79,16 +79,12 @@ export const DoubleLinkMenuContainer = ( if (types && ['ArrowUp', 'ArrowDown'].includes(event.code)) { event.preventDefault(); const isUpkey = event.code === 'ArrowUp'; - const indexBound = isUpkey ? types.length - 1 : 0; if (!currentItem && types.length) { - setCurrentItem(types[indexBound]); + setCurrentItem(types[isUpkey ? types.length - 1 : 0]); } if (currentItem) { const idx = types.indexOf(currentItem); - const needChange = isUpkey - ? idx > indexBound - : idx < indexBound; - if (needChange) { + if (isUpkey ? idx > 0 : idx < types.length - 1) { setNeedCheckIntoView(true); setCurrentItem(types[isUpkey ? idx - 1 : idx + 1]); } From 3570aab4e807e3ff895ed19c96026bc2e080c8bb Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 18:22:57 +0800 Subject: [PATCH 10/12] feat: Intra-line double link interaction --- .../src/lib/text/plugins/DoubleLink.tsx | 13 ++--- .../src/editor/block/async-block.ts | 1 - .../src/menu/double-link-menu/Container.tsx | 50 +------------------ .../menu/double-link-menu/DoubleLinkMenu.tsx | 21 +++----- 4 files changed, 16 insertions(+), 69 deletions(-) diff --git a/libs/components/common/src/lib/text/plugins/DoubleLink.tsx b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx index 3bacd92f5f..e4bcb9bec2 100644 --- a/libs/components/common/src/lib/text/plugins/DoubleLink.tsx +++ b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx @@ -2,6 +2,7 @@ import { PagesIcon } from '@toeverything/components/icons'; import React, { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Descendant } from 'slate'; +import { RenderElementProps } from 'slate-react'; export type DoubleLinkElement = { type: 'link'; @@ -11,17 +12,17 @@ export type DoubleLinkElement = { id: string; }; -export const DoubleLinkComponent = ({ attributes, children, element }: any) => { +export const DoubleLinkComponent = (props: RenderElementProps) => { + const { attributes, children, element } = props; + const doubleLinkElement = element as DoubleLinkElement; const navigate = useNavigate(); const handleClickLinkText = useCallback( (event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - const { workspaceId, blockId } = element; + const { workspaceId, blockId } = doubleLinkElement; navigate(`/${workspaceId}/${blockId}`); }, - [element, navigate] + [doubleLinkElement, navigate] ); return ( @@ -30,7 +31,7 @@ export const DoubleLinkComponent = ({ attributes, children, element }: any) => { {children} diff --git a/libs/components/editor-core/src/editor/block/async-block.ts b/libs/components/editor-core/src/editor/block/async-block.ts index 73db5de087..52dbae6ec8 100644 --- a/libs/components/editor-core/src/editor/block/async-block.ts +++ b/libs/components/editor-core/src/editor/block/async-block.ts @@ -162,7 +162,6 @@ export class AsyncBlock { const oldData = this.raw_data; this.raw_data = blockData; this.raw_data = await this.filterPageInvalidChildren(blockData); - this.raw_data = await this.updateDoubleLinkBlock(this.raw_data); this.emit('update', { block: this, oldData }); } ); diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx index 147f154129..ec5f52be08 100644 --- a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx +++ b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx @@ -1,11 +1,6 @@ -import { - CommonList, - commonListContainer, - CommonListItem, -} from '@toeverything/components/common'; +import { CommonList, CommonListItem } from '@toeverything/components/common'; import { styled } from '@toeverything/components/ui'; import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo'; -import { domToRect } from '@toeverything/utils'; import React, { useCallback, useEffect, useRef, useState } from 'react'; export type DoubleLinkMenuContainerProps = { @@ -25,35 +20,6 @@ export const DoubleLinkMenuContainer = ( const { hooks, onSelected, onClose, types, style, items } = props; const menuRef = useRef(null); const [currentItem, setCurrentItem] = useState(); - const [needCheckIntoView, setNeedCheckIntoView] = useState(false); - - useEffect(() => { - if (needCheckIntoView) { - if (currentItem && menuRef.current) { - const itemElement = - menuRef.current.querySelector( - `.item-${currentItem}` - ); - const scrollElement = - menuRef.current.querySelector( - `.${commonListContainer}` - ); - if (itemElement) { - const itemRect = domToRect(itemElement); - const scrollRect = domToRect(scrollElement); - if ( - itemRect.top < scrollRect.top || - itemRect.bottom > scrollRect.bottom - ) { - itemElement.scrollIntoView({ - block: 'nearest', - }); - } - } - } - setNeedCheckIntoView(false); - } - }, [needCheckIntoView, currentItem]); useEffect(() => { if (types && !currentItem) { @@ -61,19 +27,6 @@ export const DoubleLinkMenuContainer = ( } }, [currentItem, onClose, types]); - useEffect(() => { - if (types) { - if (!types.includes(currentItem)) { - setNeedCheckIntoView(true); - if (types.length) { - setCurrentItem(types[0]); - } else { - setCurrentItem(undefined); - } - } - } - }, [types, currentItem]); - const handleUpDownKey = useCallback( (event: React.KeyboardEvent) => { if (types && ['ArrowUp', 'ArrowDown'].includes(event.code)) { @@ -85,7 +38,6 @@ export const DoubleLinkMenuContainer = ( if (currentItem) { const idx = types.indexOf(currentItem); if (isUpkey ? idx > 0 : idx < types.length - 1) { - setNeedCheckIntoView(true); setCurrentItem(types[isUpkey ? idx - 1 : idx + 1]); } } 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 eae70b40f4..a81e8b3122 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 @@ -156,19 +156,14 @@ export const DoubleLinkMenu = ({ }); setAnchorEl(dialogRef.current); } - setTimeout(() => { - const textSelection = editor.blockHelper.selectionToSlateRange( - nextNodeId, - editor.selection.currentSelectInfo.browserSelection - ); - if (textSelection) { - const { anchor } = textSelection; - editor.blockHelper.setDoubleLinkSearchSlash( - nextNodeId, - anchor - ); - } - }); + const textSelection = editor.blockHelper.selectionToSlateRange( + nextNodeId, + editor.selection.currentSelectInfo?.browserSelection + ); + if (textSelection) { + const { anchor } = textSelection; + editor.blockHelper.setDoubleLinkSearchSlash(nextNodeId, anchor); + } }, [editor] ); From 6bc78b35ec2a8e6a9729c71ea64e9e86f8091880 Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 18:30:52 +0800 Subject: [PATCH 11/12] feat: Intra-line double link interaction --- libs/components/editor-core/src/editor/block/async-block.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/components/editor-core/src/editor/block/async-block.ts b/libs/components/editor-core/src/editor/block/async-block.ts index 52dbae6ec8..73db5de087 100644 --- a/libs/components/editor-core/src/editor/block/async-block.ts +++ b/libs/components/editor-core/src/editor/block/async-block.ts @@ -162,6 +162,7 @@ export class AsyncBlock { const oldData = this.raw_data; this.raw_data = blockData; this.raw_data = await this.filterPageInvalidChildren(blockData); + this.raw_data = await this.updateDoubleLinkBlock(this.raw_data); this.emit('update', { block: this, oldData }); } ); From b53581aae5dae96f371e53d7b330f0dea4db5d4b Mon Sep 17 00:00:00 2001 From: xiaodong zuo Date: Fri, 19 Aug 2022 18:42:46 +0800 Subject: [PATCH 12/12] feat: Intra-line double link interaction --- libs/components/common/src/lib/text/slate-utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/components/common/src/lib/text/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts index 476883a2c9..fd1de5f674 100644 --- a/libs/components/common/src/lib/text/slate-utils.ts +++ b/libs/components/common/src/lib/text/slate-utils.ts @@ -11,6 +11,7 @@ import { Transforms, } from 'slate'; import { ReactEditor } from 'slate-react'; +import type { CustomElement } from '..'; import { fontBgColorPalette, fontColorPalette, @@ -597,7 +598,7 @@ class SlateUtils { const fragmentChildren = firstFragment.children; - const textChildren = []; + const textChildren: CustomElement[] = []; for (let i = 0; i < fragmentChildren.length; i++) { const child = fragmentChildren[i]; if ('type' in child && child.type === 'link') {