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(