Merge pull request #294 from toeverything/feat/doublelink

Feat/doublelink
This commit is contained in:
DarkSky
2022-08-19 18:43:56 +08:00
committed by GitHub
20 changed files with 872 additions and 496 deletions
+23 -24
View File
@@ -1,5 +1,6 @@
import { BaseEditor } from 'slate'; import { BaseEditor } from 'slate';
import { ReactEditor } from 'slate-react'; import { ReactEditor } from 'slate-react';
import { DoubleLinkElement } from './text/plugins/DoubleLink';
import { LinkElement } from './text/plugins/link'; import { LinkElement } from './text/plugins/link';
import { RefLinkElement } from './text/plugins/reflink'; import { RefLinkElement } from './text/plugins/reflink';
@@ -8,7 +9,8 @@ export type CustomElement =
| { type: string; children: CustomElement[] } | { type: string; children: CustomElement[] }
| CustomText | CustomText
| LinkElement | LinkElement
| RefLinkElement; | RefLinkElement
| DoubleLinkElement;
declare module 'slate' { declare module 'slate' {
interface CustomTypes { interface CustomTypes {
Editor: BaseEditor & ReactEditor; Editor: BaseEditor & ReactEditor;
@@ -19,35 +21,32 @@ declare module 'slate' {
export { BlockPreview, StyledBlockPreview } from './block-preview'; export { BlockPreview, StyledBlockPreview } from './block-preview';
export { default as Button } from './button'; 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 { CollapsibleTitle } from './collapsible-title';
export * from './comming-soon/CommingSoon';
export * from './text';
export { export {
NewpageIcon, AddIcon,
ClockIcon, ClockIcon,
ViewSidebarIcon, CloseIcon,
CodeBlockInlineIcon,
DocumentIcon,
FilterIcon,
FullScreenIcon,
HighlighterDuotoneIcon,
KanbanIcon,
ListIcon, ListIcon,
SpaceIcon, NewpageIcon,
PagesIcon,
PencilDotDuotoneIcon, PencilDotDuotoneIcon,
PencilDuotoneIcon, PencilDuotoneIcon,
HighlighterDuotoneIcon,
CodeBlockInlineIcon,
PagesIcon,
CloseIcon,
DocumentIcon,
TodoListIcon,
KanbanIcon,
TableIcon,
AddIcon,
FilterIcon,
SorterIcon, SorterIcon,
FullScreenIcon, SpaceIcon,
TableIcon,
TodoListIcon,
UnGroupIcon, UnGroupIcon,
ViewSidebarIcon,
} from './icon'; } from './icon';
export { BackLink, CommonList, commonListContainer } from './list';
export * from './comming-soon/CommingSoon'; export type { CommonListItem } from './list';
export * from './Logo';
export * from './text';
export { default as Toolbar } from './toolbar';
+10 -9
View File
@@ -1,8 +1,4 @@
import { useState } from 'react'; import { BackwardUndoIcon } from '@toeverything/components/icons';
import { useNavigate } from 'react-router';
import clsx from 'clsx';
import style9 from 'style9';
import { import {
BaseButton, BaseButton,
ListButton, ListButton,
@@ -12,9 +8,11 @@ import {
} from '@toeverything/components/ui'; } from '@toeverything/components/ui';
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries // eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
import { BlockSearchItem } from '@toeverything/datasource/jwt'; 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 { BlockPreview } from '../block-preview';
import { BackwardUndoIcon } from '@toeverything/components/icons';
export const commonListContainer = 'commonListContainer'; export const commonListContainer = 'commonListContainer';
@@ -28,6 +26,7 @@ export type CommonListItem = {
divider?: string; divider?: string;
content?: Content; content?: Content;
block?: BlockSearchItem; block?: BlockSearchItem;
renderCustom?: (props: CommonListItem) => JSX.Element;
}; };
type MenuItemsProps = { type MenuItemsProps = {
@@ -45,7 +44,7 @@ export const CommonList = (props: MenuItemsProps) => {
// ]); // ]);
// TODO Insert bidirectional link to be developed // TODO Insert bidirectional link to be developed
const JSONUnsupportedBlockTypes = ['page']; const JSONUnsupportedBlockTypes = ['page'];
let usedItems = items.filter(item => { const usedItems = items.filter(item => {
return !JSONUnsupportedBlockTypes.includes(item?.content?.id); return !JSONUnsupportedBlockTypes.includes(item?.content?.id);
}); });
return ( return (
@@ -58,7 +57,9 @@ export const CommonList = (props: MenuItemsProps) => {
> >
{usedItems?.length ? ( {usedItems?.length ? (
usedItems.map((item, idx) => { usedItems.map((item, idx) => {
if (item.block) { if (item.renderCustom) {
return item.renderCustom(item);
} else if (item.block) {
return ( return (
<BlockPreview <BlockPreview
className={clsx( className={clsx(
@@ -1,54 +1,53 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import { ErrorBoundary, isEqual } from '@toeverything/utils';
import isHotkey from 'is-hotkey';
import isUrl from 'is-url';
import React, { import React, {
CSSProperties,
DragEvent,
forwardRef,
KeyboardEvent, KeyboardEvent,
KeyboardEventHandler, KeyboardEventHandler,
MouseEvent,
MouseEventHandler,
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
forwardRef,
MouseEventHandler,
useLayoutEffect,
CSSProperties,
MouseEvent,
DragEvent,
} from 'react'; } from 'react';
import isHotkey from 'is-hotkey';
import { import {
createEditor, createEditor,
Descendant, Descendant,
Range,
Element as SlateElement,
Editor, Editor,
Transforms, Element as SlateElement,
Node, Node,
Path, Path,
Range,
Transforms,
} from 'slate'; } from 'slate';
import { import {
Editable, Editable,
withReact,
Slate,
ReactEditor, ReactEditor,
Slate,
useSlateStatic, useSlateStatic,
withReact,
} from 'slate-react'; } from 'slate-react';
import { CustomElement } from '..';
import { ErrorBoundary, isEqual, uaHelper } from '@toeverything/utils'; import { HOTKEYS, INLINE_STYLES } from './constants';
import { TextWithComments } from './element-leaf/TextWithComments';
import { Contents, SlateUtils, isSelectAll } from './slate-utils'; 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 { import {
getCommentsIdsOnTextNode, getCommentsIdsOnTextNode,
getExtraPropertiesFromEditorOutmostNode, getExtraPropertiesFromEditorOutmostNode,
isInterceptCharacter, isInterceptCharacter,
matchMarkdown, matchMarkdown,
} from './utils'; } 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 { export interface TextProps {
/** read only */ /** read only */
@@ -739,6 +738,9 @@ const EditorElement = (props: any) => {
switch (element.type) { switch (element.type) {
case 'link': { case 'link': {
if (element.linkType === 'doubleLink') {
return <DoubleLinkComponent {...props} editor={editor} />;
}
return ( return (
<LinkComponent <LinkComponent
{...props} {...props}
@@ -839,6 +841,14 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => {
} }
} }
if (leaf.doubleLinkSearch) {
customChildren = (
<span style={{ backgroundColor: '#eee', borderRadius: '4px' }}>
{customChildren}
</span>
);
}
customChildren = ( customChildren = (
<TextWithComments commentsIds={commentsIds}> <TextWithComments commentsIds={commentsIds}>
{customChildren} {customChildren}
@@ -0,0 +1,40 @@
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';
workspaceId: string;
blockId: string;
children: Descendant[];
id: string;
};
export const DoubleLinkComponent = (props: RenderElementProps) => {
const { attributes, children, element } = props;
const doubleLinkElement = element as DoubleLinkElement;
const navigate = useNavigate();
const handleClickLinkText = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
const { workspaceId, blockId } = doubleLinkElement;
navigate(`/${workspaceId}/${blockId}`);
},
[doubleLinkElement, navigate]
);
return (
<span>
<PagesIcon style={{ verticalAlign: 'middle', height: '20px' }} />
<a
{...attributes}
style={{ cursor: 'pointer' }}
href={`/${doubleLinkElement.workspaceId}/${doubleLinkElement.blockId}`}
>
<span onClick={handleClickLinkText}>{children}</span>
</a>
</span>
);
};
@@ -11,7 +11,7 @@ import {
Transforms, Transforms,
} from 'slate'; } from 'slate';
import { ReactEditor } from 'slate-react'; import { ReactEditor } from 'slate-react';
import type { CustomElement } from '..';
import { import {
fontBgColorPalette, fontBgColorPalette,
fontColorPalette, fontColorPalette,
@@ -21,6 +21,7 @@ import {
import { import {
getCommentsIdsOnTextNode, getCommentsIdsOnTextNode,
getEditorMarkForCommentId, getEditorMarkForCommentId,
getRandomString,
MARKDOWN_STYLE_MAP, MARKDOWN_STYLE_MAP,
MatchRes, MatchRes,
} from './utils'; } from './utils';
@@ -246,7 +247,11 @@ Editor.insertText = function (
const { path, offset } = at; const { path, offset } = at;
if (text.length > 0) { if (text.length > 0) {
const marks = Editor.marks(editor); 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 // If the input is a space and the mark has content, remove the mark
const newPath = [path[0], path[1] + 1]; const newPath = [path[0], path[1] + 1];
const newPoint = { const newPoint = {
@@ -593,8 +598,13 @@ class SlateUtils {
const fragmentChildren = firstFragment.children; const fragmentChildren = firstFragment.children;
const textChildren: Text[] = []; const textChildren: CustomElement[] = [];
for (const child of fragmentChildren) { 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)) { if (!('text' in child)) {
console.error('Debug information:', point1, point2, fragment); console.error('Debug information:', point1, point2, fragment);
throw new Error('Fragment exists nested!'); throw new Error('Fragment exists nested!');
@@ -1084,34 +1094,130 @@ class SlateUtils {
}); });
} }
public insertReference(reference: string) { public setDoubleLinkSearchSlash(point: Point) {
try { const str = Editor.string(this.editor, {
// Transforms.setSelection(this.editor, this.getEndSelection()); anchor: this.getStart(),
const { anchor, focus } = this.getEndSelection(); focus: point,
Transforms.insertNodes( });
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, this.editor,
{ {
type: 'reflink', path,
reference, offset: 1,
children: [],
}, },
{ at: focus || anchor } {
unit: 'offset',
}
); );
const focus = Editor.after(
// requestAnimationFrame(() => { this.editor,
// console.log(this.editor.selection, this.editor.insertNode); {
// this.editor.insertNode({ path,
// type: 'reflink', offset: text.length - 1,
// reference, },
// children: [{ text: '' }] {
// }); unit: 'offset',
// // Transforms.select(); }
// }); );
} catch (e) { Transforms.select(this.editor, { anchor, focus });
console.log(e);
} }
} }
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<SlateNode>,
{
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 */ /** todo improve if selection is collapsed */
public getCommentsIdsBySelection() { public getCommentsIdsBySelection() {
const commentedTextNodes = Editor.nodes(this.editor, { const commentedTextNodes = Editor.nodes(this.editor, {
@@ -1,19 +1,19 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import EventEmitter from 'eventemitter3';
import { import {
ReturnEditorBlock,
UpdateEditorBlock,
DefaultColumnsValue, DefaultColumnsValue,
Protocol, Protocol,
ReturnEditorBlock,
UpdateEditorBlock,
} from '@toeverything/datasource/db-service'; } from '@toeverything/datasource/db-service';
import { import {
isDev,
createNoopWithMessage, createNoopWithMessage,
lowerFirst, isDev,
last, last,
lowerFirst,
} from '@toeverything/utils'; } from '@toeverything/utils';
import { BlockProvider } from './block-provider'; import EventEmitter from 'eventemitter3';
import { BaseView, BaseView as BlockView } from './../views/base-view'; import { BaseView, BaseView as BlockView } from './../views/base-view';
import { BlockProvider } from './block-provider';
type EventType = 'update'; type EventType = 'update';
export interface EventData { export interface EventData {
@@ -154,6 +154,7 @@ export class AsyncBlock {
} }
this.initialized = true; this.initialized = true;
this.raw_data = await this.filterPageInvalidChildren(this.raw_data); this.raw_data = await this.filterPageInvalidChildren(this.raw_data);
this.raw_data = await this.updateDoubleLinkBlock(this.raw_data);
const { workspace, id } = this.raw_data; const { workspace, id } = this.raw_data;
this.unobserve = await this.services.observe( this.unobserve = await this.services.observe(
{ workspace, id }, { workspace, id },
@@ -161,6 +162,7 @@ export class AsyncBlock {
const oldData = this.raw_data; const oldData = this.raw_data;
this.raw_data = blockData; this.raw_data = blockData;
this.raw_data = await this.filterPageInvalidChildren(blockData); this.raw_data = await this.filterPageInvalidChildren(blockData);
this.raw_data = await this.updateDoubleLinkBlock(this.raw_data);
this.emit('update', { block: this, oldData }); this.emit('update', { block: this, oldData });
} }
); );
@@ -495,4 +497,33 @@ export class AsyncBlock {
getBoundingClientRect() { getBoundingClientRect() {
return this.dom?.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,
});
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;
}
} }
@@ -19,6 +19,11 @@ type TextUtilsFunctions =
| 'setSearchSlash' | 'setSearchSlash'
| 'removeSearchSlash' | 'removeSearchSlash'
| 'getSearchSlashText' | 'getSearchSlashText'
| 'setDoubleLinkSearchSlash'
| 'getDoubleLinkSearchSlashText'
| 'setSelectDoubleLinkSearchSlash'
| 'removeDoubleLinkSearchSlash'
| 'insertDoubleLink'
| 'selectionToSlateRange' | 'selectionToSlateRange'
| 'transformPoint' | 'transformPoint'
| 'toggleTextFormatBySelection' | 'toggleTextFormatBySelection'
@@ -32,7 +37,6 @@ type TextUtilsFunctions =
| 'getCommentsIdsBySelection' | 'getCommentsIdsBySelection'
| 'getCurrentSelection' | 'getCurrentSelection'
| 'removeSelection' | 'removeSelection'
| 'insertReference'
| 'isCollapsed' | 'isCollapsed'
| 'blur' | 'blur'
| 'setSelection' | 'setSelection'
@@ -149,27 +153,60 @@ export class BlockHelper {
} }
} }
public insertReference( public setDoubleLinkSearchSlash(blockId: string, point: Point) {
reference: string, 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, blockId: string,
selection: Selection, isRemoveSlash?: boolean
offset: number
) { ) {
const text_utils = this._blockTextUtilsMap[blockId]; const textUtils = this._blockTextUtilsMap[blockId];
if (text_utils) { if (textUtils) {
const offsetSelection = window.getSelection(); textUtils.removeDoubleLinkSearchSlash(isRemoveSlash);
offsetSelection.setBaseAndExtent( } else {
selection.anchorNode, console.warn('Could find the block text utils');
selection.anchorOffset, }
selection.focusNode, }
selection.focusOffset + offset
);
text_utils.removeSelection(offsetSelection); public async insertDoubleLink(
text_utils.insertReference(reference); workspaceId: string,
linkBlockId: string,
// range. blockId: string
// text_utils.toggleTextFormatBySelection(format, range); ) {
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'); console.warn('Could find the block text utils');
} }
@@ -1,17 +1,15 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import HotKeys from 'hotkeys-js';
import type { PatchNode } from '@toeverything/components/ui'; import type { PatchNode } from '@toeverything/components/ui';
import { Commands } from '@toeverything/datasource/commands';
import type { import type {
BlockFlavors, BlockFlavors,
ReturnEditorBlock, ReturnEditorBlock,
UpdateEditorBlock, UpdateEditorBlock,
} from '@toeverything/datasource/db-service'; } from '@toeverything/datasource/db-service';
import { services } 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 { domToRect, last, Point, sleep } from '@toeverything/utils';
import assert from 'assert'; import assert from 'assert';
import HotKeys from 'hotkeys-js';
import type { WorkspaceAndBlockId } from './block'; import type { WorkspaceAndBlockId } from './block';
import { AsyncBlock } from './block'; import { AsyncBlock } from './block';
import { BlockHelper } from './block/block-helper'; import { BlockHelper } from './block/block-helper';
@@ -282,7 +280,7 @@ export class Editor implements Virgo {
return await blockView.onCreate(block); return await blockView.onCreate(block);
} }
private async getBlock({ public async getBlock({
workspace, workspace,
id, id,
}: WorkspaceAndBlockId): Promise<AsyncBlock | null> { }: WorkspaceAndBlockId): Promise<AsyncBlock | null> {
+11 -11
View File
@@ -1,15 +1,15 @@
import type { PluginCreator } from '@toeverything/framework/virgo'; 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 { 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 { PlaceholderPlugin } from './placeholder';
// import { BlockPropertyPlugin } from './block-property'; // import { BlockPropertyPlugin } from './block-property';
@@ -19,7 +19,7 @@ export const plugins: PluginCreator[] = [
LeftMenuPlugin, LeftMenuPlugin,
InlineMenuPlugin, InlineMenuPlugin,
CommandMenuPlugin, CommandMenuPlugin,
ReferenceMenuPlugin, DoubleLinkMenuPlugin,
TemplatePlugin, TemplatePlugin,
// SelectionGroupPlugin, // SelectionGroupPlugin,
AddCommentPlugin, AddCommentPlugin,
@@ -0,0 +1,121 @@
import { CommonList, CommonListItem } from '@toeverything/components/common';
import { styled } from '@toeverything/components/ui';
import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo';
import React, { useCallback, useEffect, useRef, useState } from 'react';
export type DoubleLinkMenuContainerProps = {
editor: Virgo;
hooks: PluginHooks;
style?: React.CSSProperties;
blockId: string;
onSelected?: (item: string) => void;
onClose?: () => void;
types?: Array<string>;
items?: CommonListItem[];
};
export const DoubleLinkMenuContainer = (
props: DoubleLinkMenuContainerProps
) => {
const { hooks, onSelected, onClose, types, style, items } = props;
const menuRef = useRef<HTMLDivElement>(null);
const [currentItem, setCurrentItem] = useState<string | undefined>();
useEffect(() => {
if (types && !currentItem) {
setCurrentItem(types[0]);
}
}, [currentItem, onClose, types]);
const handleUpDownKey = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (types && ['ArrowUp', 'ArrowDown'].includes(event.code)) {
event.preventDefault();
const isUpkey = event.code === 'ArrowUp';
if (!currentItem && types.length) {
setCurrentItem(types[isUpkey ? types.length - 1 : 0]);
}
if (currentItem) {
const idx = types.indexOf(currentItem);
if (isUpkey ? idx > 0 : idx < types.length - 1) {
setCurrentItem(types[isUpkey ? idx - 1 : idx + 1]);
}
}
}
},
[currentItem, types]
);
const handleClickUp = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
handleUpDownKey(event);
},
[handleUpDownKey]
);
const handleClickDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
handleUpDownKey(event);
},
[handleUpDownKey]
);
const handleClickEnter = useCallback(
async (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.code === 'Enter' && currentItem) {
event.preventDefault();
onSelected && onSelected(currentItem);
}
},
[currentItem, onSelected]
);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
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 (
<RootContainer
ref={menuRef}
onKeyDownCapture={handleKeyDown}
style={style}
>
<ContentContainer>
<CommonList
items={items}
onSelected={type => onSelected?.(type)}
currentItem={currentItem}
setCurrentItem={setCurrentItem}
/>
</ContentContainer>
</RootContainer>
);
};
const RootContainer = styled('div')(({ theme }) => ({
zIndex: 1,
borderRadius: '10px',
boxShadow: theme.affine.shadows.shadow1,
backgroundColor: '#fff',
padding: '8px 4px',
}));
const ContentContainer = styled('div')(({ theme }) => ({
display: 'flex',
overflow: 'hidden',
maxHeight: '300px',
}));
@@ -0,0 +1,376 @@
import { CommonListItem } from '@toeverything/components/common';
import { AddIcon } from '@toeverything/components/icons';
import {
Input,
ListButton,
MuiClickAwayListener,
MuiGrow as Grow,
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 React, {
ChangeEvent,
useCallback,
useEffect,
useMemo,
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_CODES = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'];
export type DoubleLinkMenuProps = {
editor: Virgo;
hooks: PluginHooks;
style?: { left: number; top: number };
};
type DoubleLinkMenuStyle = {
left: number;
top: number;
height: number;
};
export const DoubleLinkMenu = ({
editor,
hooks,
style,
}: DoubleLinkMenuProps) => {
const { page_id: curPageId } = useParams();
const [isOpen, setIsOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const dialogRef = useRef<HTMLDivElement>();
const newPageSearchRef = useRef<HTMLInputElement>();
const [doubleLinkMenuStyle, setDoubleLinkMenuStyle] =
useState<DoubleLinkMenuStyle>({
left: 0,
top: 0,
height: 0,
});
const [curBlockId, setCurBlockId] = useState<string>();
const [searchText, setSearchText] = useState<string>();
const [inAddNewPage, setInAddNewPage] = useState(false);
const [filterText, setFilterText] = useState<string>('');
const [searchResultBlocks, setSearchResultBlocks] = useState<QueryResult>(
[]
);
const menuTypes = useMemo(() => {
return Object.values(searchResultBlocks)
.map(({ id }) => id)
.concat([ADD_NEW_SUB_PAGE, ADD_NEW_PAGE]);
}, [searchResultBlocks]);
const menuItems: CommonListItem[] = useMemo(() => {
const items: CommonListItem[] = [];
if (searchResultBlocks?.length > 0) {
items.push({
renderCustom: () => {
return (
<ListButton
content={
inAddNewPage ? 'SUGGESTED' : 'LINK TO PAGE'
}
/>
);
},
});
items.push(
...(searchResultBlocks?.map(
block =>
({
block: {
...block,
content: block.content || 'Untitled',
},
} 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,
content: 'Add new page in...',
icon: AddIcon,
},
});
return items;
}, [searchResultBlocks, inAddNewPage]);
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, curPageId]);
const hideMenu = useCallback(() => {
setIsOpen(false);
setInAddNewPage(false);
editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId);
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);
}
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<HTMLDivElement>) => {
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 (
!isOpen ||
(type === 'Range' &&
anchorNode &&
anchorNode.id !== curBlockId &&
editor.blockHelper.isSelectionCollapsed(anchorNode.id))
) {
const text = editor.blockHelper.getBlockTextBeforeSelection(
anchorNode.id
);
if (text.endsWith('[[')) {
resetState(curBlockId, anchorNode.id);
}
}
if (isOpen) {
const searchText =
editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId);
if (searchText && searchText.startsWith('[[')) {
setSearchText(searchText.slice(2).trim());
}
}
},
[editor, isOpen, curBlockId, hideMenu]
);
const handleKeyup = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => searchChange(event),
[searchChange]
);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!isOpen) {
return;
}
if (event.code === 'Escape') {
hideMenu();
}
if (event.code === 'Backspace') {
const searchText =
editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId);
if (!searchText || searchText === '[[') {
event.preventDefault();
event.stopPropagation();
return;
}
}
},
[hideMenu, editor, isOpen, curBlockId]
);
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 insertDoubleLink = useCallback(
async (pageId: string) => {
editor.blockHelper.setSelectDoubleLinkSearchSlash(curBlockId);
await editor.blockHelper.insertDoubleLink(
editor.workspace,
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(curPageId);
insertDoubleLink(pageId);
return;
}
if (inAddNewPage) {
const pageId = await addSubPage(id);
insertDoubleLink(pageId);
} else {
insertDoubleLink(id);
}
}
};
const handleFilterChange = useCallback(
async (e: ChangeEvent<HTMLInputElement>) => {
const text = e.target.value;
await setFilterText(text);
},
[]
);
return (
<div
ref={dialogRef}
style={{
position: 'absolute',
width: '10px',
...doubleLinkMenuStyle,
}}
>
<MuiClickAwayListener onClickAway={() => hideMenu()}>
<Popper
open={isOpen}
anchorEl={anchorEl}
transition
placement="bottom-start"
>
{({ TransitionProps }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: 'left bottom',
}}
>
<Paper>
{inAddNewPage && (
<NewPageSearchContainer>
<Input
ref={newPageSearchRef}
value={filterText}
onChange={handleFilterChange}
placeholder="Search page to add to..."
/>
</NewPageSearchContainer>
)}
<DoubleLinkMenuContainer
editor={editor}
hooks={hooks}
style={style}
blockId={curBlockId}
onSelected={handleSelected}
onClose={hideMenu}
items={menuItems}
types={menuTypes}
/>
</Paper>
</Grow>
)}
</Popper>
</MuiClickAwayListener>
</div>
);
};
const NewPageSearchContainer = styled('div')({
padding: '8px 8px 0px 8px',
});
@@ -1,12 +1,11 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { BasePlugin } from '../../base-plugin'; import { BasePlugin } from '../../base-plugin';
import { PluginRenderRoot } from '../../utils'; import { PluginRenderRoot } from '../../utils';
import { ReferenceMenu } from './ReferenceMenu'; import { DoubleLinkMenu } from './DoubleLinkMenu';
const PLUGIN_NAME = 'reference-menu'; const PLUGIN_NAME = 'reference-menu';
export class ReferenceMenuPlugin extends BasePlugin { export class DoubleLinkMenuPlugin extends BasePlugin {
private _root?: PluginRenderRoot; private _root?: PluginRenderRoot;
public static override get pluginName(): string { public static override get pluginName(): string {
@@ -22,7 +21,7 @@ export class ReferenceMenuPlugin extends BasePlugin {
this._root?.render( this._root?.render(
<StrictMode> <StrictMode>
<ReferenceMenu editor={this.editor} hooks={this.hooks} /> <DoubleLinkMenu editor={this.editor} hooks={this.hooks} />
</StrictMode> </StrictMode>
); );
} }
@@ -0,0 +1 @@
export { DoubleLinkMenuPlugin } from './Plugin';
@@ -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 { InlineMenuPlugin } from './inline-menu';
export { LeftMenuPlugin } from './left-menu/LeftMenuPlugin'; 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 { MENU_WIDTH as menuWidth } from './left-menu/menu-config';
export { SelectionGroupPlugin } from './selection-group-menu';
export { GroupMenuPlugin } from './group-menu';
@@ -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<string>;
};
export const ReferenceMenuContainer = ({
hooks,
isShow = false,
onSelected,
onClose,
types,
searchBlocks,
style,
}: ReferenceMenuContainerProps) => {
const menu_ref = useRef<HTMLDivElement>(null);
const [current_item, set_current_item] = useState<string | undefined>();
const [need_check_into_view, set_need_check_into_view] =
useState<boolean>(false);
useEffect(() => {
if (need_check_into_view) {
if (current_item && menu_ref.current) {
const item_ele =
menu_ref.current.querySelector<HTMLButtonElement>(
`.item-${current_item}`
);
const scroll_ele =
menu_ref.current.querySelector<HTMLButtonElement>(
`.${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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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 ? (
<RootContainer
ref={menu_ref}
onKeyDownCapture={handle_key_down}
style={style}
>
<ContentContainer>
<CommonList
items={
searchBlocks?.map(
block => ({ block } as CommonListItem)
) || []
}
onSelected={type => onSelected?.(type)}
currentItem={current_item}
setCurrentItem={set_current_item}
/>
</ContentContainer>
</RootContainer>
) : 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',
}));
@@ -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<string>();
const [position, set_position] = useState<Point>(new Point(0, 0));
const [search_text, set_search_text] = useState<string>('');
const [search_blocks, set_search_blocks] = useState<QueryResult>([]);
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<HTMLDivElement>) => {
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<HTMLDivElement>) => handle_search(event),
[handle_search]
);
const handle_key_down = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
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 (
<ReferenceMenuWrapper
style={{ top: position.y, left: position.x }}
onKeyUp={handle_keyup}
>
<MuiClickAwayListener onClickAway={() => set_is_show(false)}>
<div>
<ReferenceMenuContainer
editor={editor}
hooks={hooks}
style={style}
isShow={is_show && !!search_text}
blockId={block_id}
onSelected={handle_selected}
onClose={handle_close}
searchBlocks={search_blocks}
types={search_block_ids}
/>
</div>
</MuiClickAwayListener>
</ReferenceMenuWrapper>
);
};
const ReferenceMenuWrapper = styled('div')({
position: 'absolute',
zIndex: 1,
});
@@ -1 +0,0 @@
export { ReferenceMenuPlugin } from './Plugin';
@@ -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 { BlockPreview } from '@toeverything/components/common';
import { import {
TransitionsModal,
MuiBox as Box, MuiBox as Box,
MuiBox, MuiBox,
styled, styled,
TransitionsModal,
} from '@toeverything/components/ui'; } from '@toeverything/components/ui';
import { Virgo, BlockEditor } from '@toeverything/framework/virgo'; import { BlockEditor, Virgo } from '@toeverything/framework/virgo';
import { throttle } from '@toeverything/utils'; 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({ const styles = style9.create({
wrapper: { wrapper: {
@@ -37,9 +36,7 @@ const query_blocks = (
search: string, search: string,
callback: (result: QueryResult) => void callback: (result: QueryResult) => void
) => { ) => {
(editor as BlockEditor) (editor as BlockEditor).search(search).then(pages => callback(pages));
.search(search)
.then(pages => callback(pages.filter(b => !!b.content)));
}; };
export const QueryBlocks = throttle(query_blocks, 500); export const QueryBlocks = throttle(query_blocks, 500);
+1 -2
View File
@@ -1,6 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import style9 from 'style9'; import style9 from 'style9';
import { SvgIconProps } from '../svg-icon'; import { SvgIconProps } from '../svg-icon';
import { BaseButton } from './base-button'; import { BaseButton } from './base-button';
@@ -35,7 +34,7 @@ const styles = style9.create({
type ListButtonProps = { type ListButtonProps = {
className?: string; className?: string;
onClick: () => void; onClick?: () => void;
onMouseOver?: () => void; onMouseOver?: () => void;
content?: string; content?: string;
children?: () => JSX.Element; children?: () => JSX.Element;
+16 -14
View File
@@ -1,7 +1,6 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import { DocumentSearchOptions } from 'flexsearch'; import { DocumentSearchOptions } from 'flexsearch';
import LRUCache from 'lru-cache'; import LRUCache from 'lru-cache';
import { import {
AsyncDatabaseAdapter, AsyncDatabaseAdapter,
BlockInstance, BlockInstance,
@@ -290,19 +289,22 @@ export class BlockClient<
| string | string
| Partial<DocumentSearchOptions<boolean>> | Partial<DocumentSearchOptions<boolean>>
): Promise<BlockSearchItem[]> { ): Promise<BlockSearchItem[]> {
const promised_pages = await Promise.all( let pages = [];
this.search(part_of_title_or_content).flatMap(({ result }) => if (part_of_title_or_content) {
result.map(async id => { const promisedPages = await Promise.all(
const page = this._pageMapping.get(id as string); this.search(part_of_title_or_content).flatMap(({ result }) =>
if (page) return page; result.map(async id => {
const block = await this.get(id as BlockTypeKeys); const page = this._pageMapping.get(id as string);
return this.set_page(block); 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)), );
]; pages = [...new Set(promisedPages.filter((v): v is string => !!v))];
} else {
pages = await this.getBlockByFlavor('page');
}
return Promise.all( return Promise.all(
this._blockIndexer.getMetadata(pages).map(async page => ({ this._blockIndexer.getMetadata(pages).map(async page => ({
content: this.get_decoded_content( content: this.get_decoded_content(