feat(plugin): migrate plugin runner to rxjs

This commit is contained in:
austaras
2022-07-27 16:22:05 +08:00
parent 8280cf47b9
commit 21fbadb399
19 changed files with 323 additions and 431 deletions

View File

@@ -9,30 +9,35 @@ import {
import { Clip } from './clip'; import { Clip } from './clip';
import assert from 'assert'; import assert from 'assert';
import ClipboardParse from './clipboard-parse'; import ClipboardParse from './clipboard-parse';
import { Subscription } from 'rxjs';
class ClipboardPopulator { class ClipboardPopulator {
private editor: Editor; private _editor: Editor;
private hooks: PluginHooks; private _hooks: PluginHooks;
private selection_manager: SelectionManager; private _selectionManager: SelectionManager;
private clipboard: any; private _clipboardParse: ClipboardParse;
private clipboard_parse: ClipboardParse; private _sub = new Subscription();
constructor( constructor(
editor: Editor, editor: Editor,
hooks: PluginHooks, hooks: PluginHooks,
selectionManager: SelectionManager, selectionManager: SelectionManager
clipboard: any
) { ) {
this.editor = editor; this._editor = editor;
this.hooks = hooks; this._hooks = hooks;
this.selection_manager = selectionManager; this._selectionManager = selectionManager;
this.clipboard = clipboard; this._clipboardParse = new ClipboardParse(editor);
this.clipboard_parse = new ClipboardParse(editor); this._sub.add(
hooks.addHook(HookType.BEFORE_COPY, this.populate_app_clipboard, this); hooks
hooks.addHook(HookType.BEFORE_CUT, this.populate_app_clipboard, this); .get(HookType.BEFORE_COPY)
.subscribe(this._populateAppClipboard)
);
this._sub.add(
hooks.get(HookType.BEFORE_CUT).subscribe(this._populateAppClipboard)
);
} }
private async populate_app_clipboard(e: ClipboardEvent) { private _populateAppClipboard = async (e: ClipboardEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const clips = await this.getClips(); const clips = await this.getClips();
@@ -58,7 +63,8 @@ class ClipboardPopulator {
} }
} }
} }
} };
private copy_to_cliboard_from_pc(clips: any[]) { private copy_to_cliboard_from_pc(clips: any[]) {
let success = false; let success = false;
const tempElem = document.createElement('textarea'); const tempElem = document.createElement('textarea');
@@ -91,8 +97,8 @@ class ClipboardPopulator {
} }
private async get_clip_block_info(selBlock: SelectBlock) { private async get_clip_block_info(selBlock: SelectBlock) {
const block = await this.editor.getBlockById(selBlock.blockId); const block = await this._editor.getBlockById(selBlock.blockId);
const block_view = this.editor.getView(block.type); const block_view = this._editor.getView(block.type);
assert(block_view); assert(block_view);
const block_info: ClipBlockInfo = { const block_info: ClipBlockInfo = {
type: block.type, type: block.type,
@@ -112,7 +118,8 @@ class ClipboardPopulator {
private async get_inner_clip(): Promise<InnerClipInfo> { private async get_inner_clip(): Promise<InnerClipInfo> {
const clips: ClipBlockInfo[] = []; const clips: ClipBlockInfo[] = [];
const select_info: SelectInfo = await this.selection_manager.getSelectInfo(); const select_info: SelectInfo =
await this._selectionManager.getSelectInfo();
for (let i = 0; i < select_info.blocks.length; i++) { for (let i = 0; i < select_info.blocks.length; i++) {
const sel_block = select_info.blocks[i]; const sel_block = select_info.blocks[i];
const clip_block_info = await this.get_clip_block_info(sel_block); const clip_block_info = await this.get_clip_block_info(sel_block);
@@ -136,7 +143,7 @@ class ClipboardPopulator {
) )
); );
const html_clip = await this.clipboard_parse.generateHtml(); const html_clip = await this._clipboardParse.generateHtml();
html_clip && html_clip &&
clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, html_clip)); clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, html_clip));
@@ -144,12 +151,8 @@ class ClipboardPopulator {
} }
disposeInternal() { disposeInternal() {
this.hooks.removeHook( this._sub.unsubscribe();
HookType.BEFORE_COPY, this._hooks = null;
this.populate_app_clipboard
);
this.hooks.removeHook(HookType.BEFORE_CUT, this.populate_app_clipboard);
this.hooks = null;
} }
} }

View File

@@ -186,8 +186,7 @@ export class Editor implements Virgo {
this.clipboard_populator = new ClipboardPopulator( this.clipboard_populator = new ClipboardPopulator(
this, this,
this.hooks, this.hooks,
this.selectionManager, this.selectionManager
this.clipboard
); );
} }
} }

View File

@@ -1,91 +1,37 @@
import { import { Observable, Subject } from 'rxjs';
HooksRunner, import { HooksRunner, HookType, BlockDomInfo, PluginHooks } from '../types';
PluginHooks,
HookType,
HookBaseArgs,
BlockDomInfo,
AnyFunction,
AnyThisType,
} from '../types';
interface PluginHookInfo {
thisObj?: AnyThisType;
callback: AnyFunction;
once: boolean;
}
export class Hooks implements HooksRunner, PluginHooks { export class Hooks implements HooksRunner, PluginHooks {
private _hooksMap: Map<string, PluginHookInfo[]> = new Map(); private _subject: Record<string, Subject<unknown>> = {};
private _observable: Record<string, Observable<unknown>> = {};
dispose() { dispose() {
this._hooksMap.clear(); this._subject = {};
} }
private _runHook(key: HookType, ...params: unknown[]): void { private _runHook(key: HookType, ...params: unknown[]): void {
const hookInfos: PluginHookInfo[] = this._hooksMap.get(key) || []; if (this._subject[key] == null) {
hookInfos.forEach(hookInfo => { this._subject[key] = new Subject();
if (hookInfo.once) { this._observable[key] = this._subject[key].asObservable();
this.removeHook(key, hookInfo.callback); }
} let payload: unknown = params;
let isStoppedPropagation = false;
const hookOption: HookBaseArgs = { if (params.length === 0) {
stopImmediatePropagation: () => { payload = undefined;
isStoppedPropagation = true; }
}, if (params.length === 1) {
}; payload = params[0];
hookInfo.callback.call( }
hookInfo.thisObj || this, this._subject[key].next(payload);
...params,
hookOption
);
return isStoppedPropagation;
});
} }
private _hasHook(key: HookType, callback: AnyFunction): boolean { public get<K extends keyof HooksRunner>(key: K) {
const hookInfos: PluginHookInfo[] = this._hooksMap.get(key) || []; if (this._subject[key] == null) {
for (let i = hookInfos.length - 1; i >= 0; i--) { this._subject[key] = new Subject();
if (hookInfos[i].callback === callback) { this._observable[key] = this._subject[key].asObservable();
return true;
}
} }
return false;
}
// 执行多次 return this._observable[key] as any;
public addHook(
key: HookType,
callback: AnyFunction,
thisObj?: AnyThisType,
once?: boolean
): void {
if (this._hasHook(key, callback)) {
throw new Error('Duplicate registration of the same class');
}
if (!this._hooksMap.has(key)) {
this._hooksMap.set(key, []);
}
const hookInfos: PluginHookInfo[] = this._hooksMap.get(key);
hookInfos.push({ callback, thisObj, once });
}
// 执行一次
public addOnceHook(
key: HookType,
callback: AnyFunction,
thisObj?: AnyThisType
): void {
this.addHook(key, callback, thisObj, true);
}
// 移除
public removeHook(key: HookType, callback: AnyFunction): void {
const hookInfos: PluginHookInfo[] = this._hooksMap.get(key) || [];
for (let i = hookInfos.length - 1; i >= 0; i--) {
if (hookInfos[i].callback === callback) {
hookInfos.splice(i, 1);
}
}
} }
public init(): void { public init(): void {

View File

@@ -1,7 +1,7 @@
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';
import { domToRect, Rect } from '@toeverything/utils'; import { domToRect, Rect } from '@toeverything/utils';
import type { Editor as Block_editor } from '../editor'; import type { Editor as BlockEditor } from '../editor';
import { AsyncBlock } from '../block'; import { AsyncBlock } from '../block';
@@ -9,7 +9,7 @@ type VerticalTypes = 'up' | 'down' | null;
type HorizontalTypes = 'left' | 'right' | null; type HorizontalTypes = 'left' | 'right' | null;
export class ScrollManager { export class ScrollManager {
private _editor: Block_editor; private _editor: BlockEditor;
private _animationFrame: null | number = null; private _animationFrame: null | number = null;
private _eventName = 'scrolling'; private _eventName = 'scrolling';
private _currentMoveDirection: [HorizontalTypes, VerticalTypes] = [ private _currentMoveDirection: [HorizontalTypes, VerticalTypes] = [
@@ -20,9 +20,8 @@ export class ScrollManager {
private _scrollMoveOffset = 8; private _scrollMoveOffset = 8;
private _scrollingEvent = new EventEmitter(); private _scrollingEvent = new EventEmitter();
constructor(editor: Block_editor) { constructor(editor: BlockEditor) {
this._editor = editor; this._editor = editor;
console.log('scrollmanager constructor', this._editor.ui_container);
} }
private _updateScrollInfo(left: number, top: number) { private _updateScrollInfo(left: number, top: number) {

View File

@@ -21,6 +21,7 @@ import type { BlockHelper } from './block/block-helper';
import type { BlockCommands } from './commands/block-commands'; import type { BlockCommands } from './commands/block-commands';
import type { DragDropManager } from './drag-drop'; import type { DragDropManager } from './drag-drop';
import { MouseManager } from './mouse'; import { MouseManager } from './mouse';
import { Observable } from 'rxjs';
// import { BrowserClipboard } from './clipboard/browser-clipboard'; // import { BrowserClipboard } from './clipboard/browser-clipboard';
@@ -176,10 +177,6 @@ export enum HookType {
BEFORE_CUT = 'beforeCut', BEFORE_CUT = 'beforeCut',
} }
export interface HookBaseArgs {
stopImmediatePropagation: () => void;
}
export interface BlockDomInfo { export interface BlockDomInfo {
blockId: string; blockId: string;
dom: HTMLElement; dom: HTMLElement;
@@ -227,25 +224,16 @@ export interface HooksRunner {
beforeCut: (e: ClipboardEvent) => void; beforeCut: (e: ClipboardEvent) => void;
} }
export type AnyFunction = (...args: any[]) => any; export type PayloadType<T extends Array<any>> = T extends []
export type AnyThisType = ThisParameterType<any>; ? void
: T extends [infer U]
? U
: T;
// hook管理,在editor、plugin中使用
export interface PluginHooks { export interface PluginHooks {
// 执行多次 get<K extends keyof HooksRunner>(
addHook: ( key: K
key: HookType, ): Observable<PayloadType<Parameters<HooksRunner[K]>>>;
callback: AnyFunction,
thisObj?: AnyThisType,
once?: boolean
) => void;
// 执行一次
addOnceHook: (
key: HookType,
callback: AnyFunction,
thisObj?: AnyThisType
) => void;
// 移除
removeHook: (key: HookType, callback: AnyFunction) => void;
} }
export * from './drag-drop/types'; export * from './drag-drop/types';

View File

@@ -1,17 +1,17 @@
import { import {
HookType,
Virgo, Virgo,
Plugin, Plugin,
PluginHooks, PluginHooks,
HookType,
} from '@toeverything/framework/virgo'; } from '@toeverything/framework/virgo';
import { genErrorObj } from '@toeverything/utils'; import { genErrorObj } from '@toeverything/utils';
import { Subscription } from 'rxjs';
export abstract class BasePlugin implements Plugin { export abstract class BasePlugin implements Plugin {
protected editor: Virgo; protected editor: Virgo;
protected hooks: PluginHooks; protected hooks: PluginHooks;
private hook_queue: [type: HookType, fn: (...args: unknown[]) => void][] = protected sub: Subscription;
[]; private _disposed = false;
private is_disposed = false;
// Unique identifier to distinguish between different Plugins // Unique identifier to distinguish between different Plugins
public static get pluginName(): string { public static get pluginName(): string {
@@ -27,22 +27,8 @@ export abstract class BasePlugin implements Plugin {
constructor(editor: Virgo, hooks: PluginHooks) { constructor(editor: Virgo, hooks: PluginHooks) {
this.editor = editor; this.editor = editor;
// TODO perfect it this.hooks = hooks;
this.hooks = { this.sub = hooks.get(HookType.RENDER).subscribe(() => this._onRender());
addHook: (...args) => {
this.hook_queue.push([args[0], args[1]]);
return hooks.addHook(...args);
},
addOnceHook(...args) {
return hooks.addHook(...args);
},
// TODO fix remove
removeHook(...args) {
return hooks.removeHook(...args);
},
};
this._onRender = this._onRender.bind(this);
hooks.addHook(HookType.RENDER, this._onRender, this);
} }
/** /**
@@ -62,18 +48,12 @@ export abstract class BasePlugin implements Plugin {
public dispose(): void { public dispose(): void {
// See https://stackoverflow.com/questions/33387318/access-to-static-properties-via-this-constructor-in-typescript // See https://stackoverflow.com/questions/33387318/access-to-static-properties-via-this-constructor-in-typescript
const pluginName = (this.constructor as typeof BasePlugin).pluginName; const pluginName = (this.constructor as typeof BasePlugin).pluginName;
if (this.is_disposed) { if (this._disposed) {
console.warn(`Plugin '${pluginName}' already disposed`); console.warn(`Plugin '${pluginName}' already disposed`);
return; return;
} }
this.is_disposed = true; this.sub.unsubscribe();
// FIX will remove hook multiple times this._disposed = true;
// if the hook has been removed manually
// or set once flag when add hook
this.hook_queue.forEach(([type, fn]) => {
this.hooks.removeHook(type, fn);
});
this.hook_queue = [];
const errorMsg = `You are trying to access an invalid editor or hooks. const errorMsg = `You are trying to access an invalid editor or hooks.
The plugin '${pluginName}' has been disposed. The plugin '${pluginName}' has been disposed.

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { BasePlugin } from '../base-plugin';
import { BlockDomInfo, HookType } from '@toeverything/framework/virgo';
import View from './View';
import { Subscription } from 'rxjs';
const PLUGIN_NAME = 'block-property';
export class BlockPropertyPlugin extends BasePlugin {
public static override get pluginName(): string {
return PLUGIN_NAME;
}
private _root: Root | undefined;
private _rootDom: HTMLElement;
// record mouse moving block id
private _currentSlidingBlockInfo: BlockDomInfo;
private _hover = false;
private _setHover = (isHover: boolean) => {
this._hover = isHover;
};
private _insertRootToBlock = async () => {
this._rootDom = document.createElement('div');
this._rootDom.style.position = 'relative';
this._rootDom.style.zIndex = '1000';
this._rootDom.classList.add(`id-${PLUGIN_NAME}`);
this._currentSlidingBlockInfo.dom.appendChild(this._rootDom);
this._root = createRoot(this._rootDom);
};
private _onSlidingBlockChange = async (blockDomInfo: BlockDomInfo) => {
this._currentSlidingBlockInfo = blockDomInfo;
await this._insertRootToBlock();
this._renderView();
};
private _onMouseMove = async ([event, blockDomInfo]: [
React.MouseEvent,
BlockDomInfo
]) => {
if (
blockDomInfo.blockId !== this._currentSlidingBlockInfo?.blockId &&
!this._hover
) {
await this.dispose();
await this._onSlidingBlockChange(blockDomInfo);
}
};
private _renderView = () => {
this._root.render(
<View
blockDomInfo={this._currentSlidingBlockInfo}
setIsHover={this._setHover}
/>
);
};
protected override _onRender(): void {
const sub = this.hooks
.get(HookType.AFTER_ON_NODE_MOUSE_MOVE)
.subscribe(this._onMouseMove);
this.sub.add(sub);
}
override async dispose() {
if (this._currentSlidingBlockInfo) {
this._currentSlidingBlockInfo.dom.removeChild(this._rootDom);
this._currentSlidingBlockInfo = undefined;
}
this._rootDom = undefined;
if (this._root) {
this._root.unmount();
}
super.dispose();
}
}

View File

@@ -1,83 +0,0 @@
import React from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { BasePlugin } from '../base-plugin';
import { BlockDomInfo, HookType } from '@toeverything/framework/virgo';
import View from './view';
const PLUGIN_NAME = 'block-property';
export class BlockPropertyPlugin extends BasePlugin {
public static override get pluginName(): string {
return PLUGIN_NAME;
}
private root: Root | undefined;
private root_dom: HTMLElement;
// record mouse moving block id
private current_sliding_block_info: BlockDomInfo;
private is_render = false;
private is_hover = false;
private set_is_hover = (isHover: boolean) => {
this.is_hover = isHover;
};
private insert_root_to_block = async () => {
this.root_dom = document.createElement('div');
this.root_dom.style.position = 'relative';
this.root_dom.style.zIndex = '1000';
this.root_dom.classList.add(`id-${PLUGIN_NAME}`);
this.current_sliding_block_info.dom.appendChild(this.root_dom);
this.root = createRoot(this.root_dom);
};
private on_sliding_block_change = async (blockDomInfo: BlockDomInfo) => {
this.current_sliding_block_info = blockDomInfo;
await this.insert_root_to_block();
this.render_view();
this.is_render = true;
};
private on_mouse_move = async (
event: React.MouseEvent,
blockDomInfo: BlockDomInfo
) => {
if (
blockDomInfo.blockId !== this.current_sliding_block_info?.blockId &&
!this.is_hover
) {
await this.dispose();
await this.on_sliding_block_change(blockDomInfo);
}
};
private render_view = () => {
this.root.render(
<View
blockDomInfo={this.current_sliding_block_info}
setIsHover={this.set_is_hover}
/>
);
};
protected override _onRender(): void {
this.hooks.addHook(
HookType.AFTER_ON_NODE_MOUSE_MOVE,
this.on_mouse_move,
this
);
}
override async dispose() {
if (this.current_sliding_block_info) {
this.current_sliding_block_info.dom.removeChild(this.root_dom);
this.current_sliding_block_info = undefined;
}
this.root_dom = undefined;
this.is_render = false;
if (this.root) {
this.root.unmount();
}
}
}

View File

@@ -190,13 +190,12 @@ export const CommandMenuContainer = ({
); );
useEffect(() => { useEffect(() => {
hooks.addHook(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE, handleKeyDown); const sub = hooks
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
.subscribe(handle_key_down);
return () => { return () => {
hooks.removeHook( sub.unsubscribe();
HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE,
handleKeyDown
);
}; };
}, [hooks, handleKeyDown]); }, [hooks, handleKeyDown]);

View File

@@ -18,7 +18,7 @@ import {
commonCommandMenuHandler, commonCommandMenuHandler,
menuItemsMap, menuItemsMap,
} from './config'; } from './config';
import { QueryBlocks, QueryResult } from '../../search'; import { QueryResult } from '../../search';
export type CommandMenuProps = { export type CommandMenuProps = {
editor: Virgo; editor: Virgo;
@@ -31,9 +31,11 @@ type CommandMenuPosition = {
bottom: number | 'initial'; bottom: number | 'initial';
}; };
const COMMAND_MENU_HEIGHT = 509;
export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => { export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
const [is_show, set_is_show] = useState(false); const [show, setShow] = useState(false);
const [block_id, set_block_id] = useState<string>(); const [blockId, setBlockId] = useState<string>();
const [commandMenuPosition, setCommandMenuPosition] = const [commandMenuPosition, setCommandMenuPosition] =
useState<CommandMenuPosition>({ useState<CommandMenuPosition>({
left: 0, left: 0,
@@ -85,8 +87,8 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
anchorNode.id anchorNode.id
); );
if (text.endsWith('/')) { if (text.endsWith('/')) {
set_block_id(anchorNode.id); setBlockId(anchorNode.id);
editor.blockHelper.removeSearchSlash(block_id); editor.blockHelper.removeSearchSlash(blockId);
setTimeout(() => { setTimeout(() => {
const textSelection = const textSelection =
editor.blockHelper.selectionToSlateRange( editor.blockHelper.selectionToSlateRange(
@@ -103,18 +105,16 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
} }
}); });
set_search_text(''); set_search_text('');
set_is_show(true); setShow(true);
const rect = const rect =
editor.selection.currentSelectInfo?.browserSelection editor.selection.currentSelectInfo?.browserSelection
?.getRangeAt(0) ?.getRangeAt(0)
?.getBoundingClientRect(); ?.getBoundingClientRect();
if (rect) { if (rect) {
let top = rect.top; let top = rect.top;
let clientHeight = const clientHeight =
document.documentElement.clientHeight; document.documentElement.clientHeight;
const COMMAND_MENU_HEIGHT =
window.innerHeight * 0.4;
if (clientHeight - top <= COMMAND_MENU_HEIGHT) { if (clientHeight - top <= COMMAND_MENU_HEIGHT) {
top = clientHeight - top + 10; top = clientHeight - top + 10;
setCommandMenuPosition({ setCommandMenuPosition({
@@ -135,33 +135,33 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
} }
} }
}, },
[editor, block_id] [editor, blockId]
); );
const handle_click_others = useCallback( const handle_click_others = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (is_show) { if (show) {
const { anchorNode } = editor.selection.currentSelectInfo; const { anchorNode } = editor.selection.currentSelectInfo;
if (anchorNode.id !== block_id) { if (anchorNode.id !== blockId) {
set_is_show(false); setShow(false);
return; return;
} }
setTimeout(() => { setTimeout(() => {
const searchText = const searchText =
editor.blockHelper.getSearchSlashText(block_id); editor.blockHelper.getSearchSlashText(blockId);
// check if has search text // check if has search text
if (searchText && searchText.startsWith('/')) { if (searchText && searchText.startsWith('/')) {
set_search_text(searchText.slice(1)); set_search_text(searchText.slice(1));
} else { } else {
set_is_show(false); setShow(false);
} }
if (searchText.length > 6 && !types.length) { if (searchText.length > 6 && !types.length) {
set_is_show(false); setShow(false);
} }
}); });
} }
}, },
[editor, is_show, block_id, types] [editor, show, blockId, types]
); );
const handle_keyup = useCallback( const handle_keyup = useCallback(
@@ -175,59 +175,61 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
const handle_key_down = useCallback( const handle_key_down = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.code === 'Escape') { if (event.code === 'Escape') {
set_is_show(false); setShow(false);
} }
}, },
[] []
); );
useEffect(() => { useEffect(() => {
hooks.addHook(HookType.ON_ROOT_NODE_KEYUP, handle_keyup); const sub = hooks
hooks.addHook(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE, handle_key_down); .get(HookType.ON_ROOT_NODE_KEYUP)
.subscribe(handle_keyup);
sub.add(
hooks
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
.subscribe(handle_key_down)
);
return () => { return () => {
hooks.removeHook(HookType.ON_ROOT_NODE_KEYUP, handle_keyup); sub.unsubscribe();
hooks.removeHook(
HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE,
handle_key_down
);
}; };
}, [handle_keyup, handle_key_down, hooks]); }, [handle_keyup, handle_key_down, hooks]);
const handle_click_away = () => { const handle_click_away = () => {
set_is_show(false); setShow(false);
}; };
const handle_selected = async (type: BlockFlavorKeys | string) => { const handle_selected = async (type: BlockFlavorKeys | string) => {
const text = await editor.commands.textCommands.getBlockText(block_id); const text = await editor.commands.textCommands.getBlockText(blockId);
editor.blockHelper.removeSearchSlash(block_id, true); editor.blockHelper.removeSearchSlash(blockId, true);
if (type.startsWith('Virgo')) { if (type.startsWith('Virgo')) {
const handler = const handler =
commandMenuHandlerMap[Protocol.Block.Type.reference]; commandMenuHandlerMap[Protocol.Block.Type.reference];
handler(block_id, type, editor); handler(blockId, type, editor);
} else if (text.length > 1) { } else if (text.length > 1) {
const handler = commandMenuHandlerMap[type]; const handler = commandMenuHandlerMap[type];
if (handler) { if (handler) {
await handler(block_id, type, editor); await handler(blockId, type, editor);
} else { } else {
await commonCommandMenuHandler(block_id, type, editor); await commonCommandMenuHandler(blockId, type, editor);
} }
const block = await editor.getBlockById(block_id); const block = await editor.getBlockById(blockId);
block.remove(); block.remove();
} else { } else {
if (Protocol.Block.Type[type as BlockFlavorKeys]) { if (Protocol.Block.Type[type as BlockFlavorKeys]) {
const block = await editor.commands.blockCommands.convertBlock( const block = await editor.commands.blockCommands.convertBlock(
block_id, blockId,
type as BlockFlavorKeys type as BlockFlavorKeys
); );
block.firstCreateFlag = true; block.firstCreateFlag = true;
} }
} }
set_is_show(false); setShow(false);
}; };
const handle_close = () => { const handle_close = () => {
editor.blockHelper.removeSearchSlash(block_id); editor.blockHelper.removeSearchSlash(blockId);
}; };
return ( return (
@@ -237,24 +239,21 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
ref={commandMenuContentRef} ref={commandMenuContentRef}
> >
<MuiClickAwayListener onClickAway={handle_click_away}> <MuiClickAwayListener onClickAway={handle_click_away}>
{/* MuiClickAwayListener 渲染子节点问题*/} <CommandMenuContainer
<div> editor={editor}
<CommandMenuContainer hooks={hooks}
editor={editor} style={{
hooks={hooks} ...commandMenuPosition,
style={{ ...style,
...commandMenuPosition, }}
...style, isShow={show}
}} blockId={blockId}
isShow={is_show} onSelected={handle_selected}
blockId={block_id} onclose={handle_close}
onSelected={handle_selected} searchBlocks={search_blocks}
onclose={handle_close} types={types}
searchBlocks={search_blocks} categories={categories}
types={types} />
categories={categories}
/>
</div>
</MuiClickAwayListener> </MuiClickAwayListener>
</div> </div>
); );

View File

@@ -91,29 +91,31 @@ export const GroupMenu = function ({ editor, hooks }: GroupMenuProps) {
[editor, groupBlock] [editor, groupBlock]
); );
const handleRootDragEnd = (e: DragEvent) => { const handleRootDragEnd = () => {
setDragOverGroup(null); setDragOverGroup(null);
}; };
useEffect(() => { useEffect(() => {
hooks.addHook(HookType.ON_ROOTNODE_MOUSE_MOVE, handleRootMouseMove); const sub = hooks
hooks.addHook(HookType.ON_ROOTNODE_MOUSE_DOWN, handleRootMouseDown); .get(HookType.ON_ROOTNODE_MOUSE_MOVE)
hooks.addHook(HookType.ON_ROOTNODE_DRAG_OVER, handleRootDragOver); .subscribe(handleRootMouseMove);
hooks.addHook(HookType.ON_ROOTNODE_DRAG_END, handleRootDragEnd); sub.add(
hooks
.get(HookType.ON_ROOTNODE_MOUSE_DOWN)
.subscribe(handleRootMouseDown)
);
sub.add(
hooks
.get(HookType.ON_ROOTNODE_DRAG_OVER)
.subscribe(handleRootDragOver)
);
sub.add(
hooks
.get(HookType.ON_ROOTNODE_DRAG_END)
.subscribe(handleRootDragEnd)
);
return () => { return () => {
hooks.removeHook( sub.unsubscribe();
HookType.ON_ROOTNODE_MOUSE_MOVE,
handleRootMouseMove
);
hooks.removeHook(
HookType.ON_ROOTNODE_MOUSE_DOWN,
handleRootMouseDown
);
hooks.removeHook(
HookType.ON_ROOTNODE_DRAG_OVER,
handleRootDragOver
);
hooks.removeHook(HookType.ON_ROOTNODE_DRAG_END, handleRootDragEnd);
}; };
}, [ }, [
hooks, hooks,

View File

@@ -3,7 +3,6 @@ import { useState, useEffect, FC } from 'react';
import { import {
Virgo, Virgo,
BlockDomInfo, BlockDomInfo,
HookType,
PluginHooks, PluginHooks,
BlockDropPlacement, BlockDropPlacement,
} from '@toeverything/framework/virgo'; } from '@toeverything/framework/virgo';
@@ -116,7 +115,7 @@ function DragComponent(props: {
} }
export const LeftMenuDraggable: FC<LeftMenuProps> = props => { export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
const { editor, blockInfo, defaultVisible, hooks, lineInfo } = props; const { editor, blockInfo, defaultVisible, lineInfo } = props;
const [visible, setVisible] = useState(defaultVisible); const [visible, setVisible] = useState(defaultVisible);
const [anchorEl, setAnchorEl] = useState<Element>(); const [anchorEl, setAnchorEl] = useState<Element>();
@@ -128,10 +127,6 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
window.addEventListener('dragover', handleDragOverCapture, { window.addEventListener('dragover', handleDragOverCapture, {
capture: true, capture: true,
}); });
hooks.addHook(
HookType.ON_ROOTNODE_DRAG_OVER_CAPTURE,
handleDragOverCapture
);
const onDragStart = async (event: React.DragEvent<Element>) => { const onDragStart = async (event: React.DragEvent<Element>) => {
editor.dragDropManager.isOnDrag = true; editor.dragDropManager.isOnDrag = true;

View File

@@ -8,10 +8,10 @@ import { Subject } from 'rxjs';
import { domToRect, last, Point } from '@toeverything/utils'; import { domToRect, last, Point } from '@toeverything/utils';
export class LeftMenuPlugin extends BasePlugin { export class LeftMenuPlugin extends BasePlugin {
private mousedown?: boolean; private _mousedown?: boolean;
private root?: PluginRenderRoot; private _root?: PluginRenderRoot;
private preBlockId: string; private _preBlockId: string;
private hideTimer: number; private _hideTimer: number;
private _blockInfo: Subject<BlockDomInfo | undefined> = new Subject(); private _blockInfo: Subject<BlockDomInfo | undefined> = new Subject();
private _lineInfo: LineInfoSubject = new Subject(); private _lineInfo: LineInfoSubject = new Subject();
@@ -21,39 +21,49 @@ export class LeftMenuPlugin extends BasePlugin {
} }
public override init(): void { public override init(): void {
this.hooks.addHook( this.sub.add(
HookType.AFTER_ON_NODE_MOUSE_MOVE, this.hooks
this._handleMouseMove .get(HookType.AFTER_ON_NODE_MOUSE_MOVE)
.subscribe(this._handleMouseMove)
); );
this.hooks.addHook( this.sub.add(
HookType.ON_ROOTNODE_MOUSE_DOWN, this.hooks
this._handleMouseDown .get(HookType.ON_ROOTNODE_MOUSE_DOWN)
.subscribe(this._handleMouseDown)
); );
this.hooks.addHook( this.sub.add(
HookType.ON_ROOTNODE_MOUSE_LEAVE, this.hooks
this._handleRootMouseLeave, .get(HookType.ON_ROOTNODE_MOUSE_LEAVE)
this .subscribe(this._hideLeftMenu)
); );
this.hooks.addHook(HookType.ON_ROOTNODE_MOUSE_UP, this._handleMouseUp); this.sub.add(
this.hooks.addHook( this.hooks
HookType.AFTER_ON_NODE_DRAG_OVER, .get(HookType.ON_ROOTNODE_MOUSE_UP)
this._handleDragOverBlockNode .subscribe(this._handleMouseUp)
);
this.sub.add(
this.hooks
.get(HookType.AFTER_ON_NODE_DRAG_OVER)
.subscribe(this._handleDragOverBlockNode)
);
this.sub.add(
this.hooks
.get(HookType.ON_ROOT_NODE_KEYDOWN)
.subscribe(this._handleKeyDown)
);
this.sub.add(
this.hooks.get(HookType.ON_ROOTNODE_DROP).subscribe(this._onDrop)
); );
this.hooks.addHook(HookType.ON_ROOT_NODE_KEYDOWN, this._handleKeyDown);
this.hooks.addHook(HookType.ON_ROOTNODE_DROP, this._onDrop);
} }
private _handleRootMouseLeave() {
this._hideLeftMenu();
}
private _onDrop = () => { private _onDrop = () => {
this.preBlockId = ''; this._preBlockId = '';
this._lineInfo.next(undefined); this._lineInfo.next(undefined);
}; };
private _handleDragOverBlockNode = async ( private _handleDragOverBlockNode = async ([event, blockInfo]: [
event: React.DragEvent<Element>, React.DragEvent<Element>,
blockInfo: BlockDomInfo BlockDomInfo
) => { ]) => {
const { type, dom, blockId } = blockInfo; const { type, dom, blockId } = blockInfo;
event.preventDefault(); event.preventDefault();
if (this.editor.dragDropManager.isDragBlock(event)) { if (this.editor.dragDropManager.isDragBlock(event)) {
@@ -70,24 +80,24 @@ export class LeftMenuPlugin extends BasePlugin {
} }
}; };
private _handleMouseMove = async ( private _handleMouseMove = async ([e, node]: [
e: React.MouseEvent<HTMLDivElement, MouseEvent>, React.MouseEvent<HTMLDivElement, MouseEvent>,
node: BlockDomInfo BlockDomInfo
) => { ]) => {
if (!this.hideTimer) { if (!this._hideTimer) {
this.hideTimer = window.setTimeout(() => { this._hideTimer = window.setTimeout(() => {
if (this.mousedown) { if (this._mousedown) {
this._hideLeftMenu(); this._hideLeftMenu();
return; return;
} }
this.hideTimer = 0; this._hideTimer = 0;
}, 300); }, 300);
} }
if (this.editor.readonly) { if (this.editor.readonly) {
this._hideLeftMenu(); this._hideLeftMenu();
return; return;
} }
if (node.blockId !== this.preBlockId) { if (node.blockId !== this._preBlockId) {
if (node.dom) { if (node.dom) {
const mousePoint = new Point(e.clientX, e.clientY); const mousePoint = new Point(e.clientX, e.clientY);
const children = await ( const children = await (
@@ -109,27 +119,21 @@ export class LeftMenuPlugin extends BasePlugin {
} }
} }
} }
this.preBlockId = node.blockId; this._preBlockId = node.blockId;
this._showLeftMenu(node); this._showLeftMenu(node);
} }
}; };
private _handleMouseUp( private _handleMouseUp() {
e: React.MouseEvent<HTMLDivElement, MouseEvent>, if (this._hideTimer) {
node: BlockDomInfo window.clearTimeout(this._hideTimer);
) { this._hideTimer = 0;
if (this.hideTimer) {
window.clearTimeout(this.hideTimer);
this.hideTimer = 0;
} }
this.mousedown = false; this._mousedown = false;
} }
private _handleMouseDown = ( private _handleMouseDown = () => {
e: React.MouseEvent<HTMLDivElement, MouseEvent>, this._mousedown = true;
node: BlockDomInfo
) => {
this.mousedown = true;
}; };
private _hideLeftMenu = (): void => { private _hideLeftMenu = (): void => {
@@ -148,14 +152,14 @@ export class LeftMenuPlugin extends BasePlugin {
}; };
protected override _onRender(): void { protected override _onRender(): void {
this.root = new PluginRenderRoot({ this._root = new PluginRenderRoot({
name: LeftMenuPlugin.pluginName, name: LeftMenuPlugin.pluginName,
render: (...args) => { render: (...args) => {
return this.editor.reactRenderRoot?.render(...args); return this.editor.reactRenderRoot?.render(...args);
}, },
}); });
this.root.mount(); this._root.mount();
this.root.render( this._root.render(
<StrictMode> <StrictMode>
<LeftMenuDraggable <LeftMenuDraggable
key={Math.random() + ''} key={Math.random() + ''}
@@ -170,8 +174,7 @@ export class LeftMenuPlugin extends BasePlugin {
} }
public override dispose(): void { public override dispose(): void {
// TODO: rxjs this._root?.unmount();
this.root?.unmount();
super.dispose(); super.dispose();
} }
} }

View File

@@ -142,13 +142,12 @@ export const ReferenceMenuContainer = ({
); );
useEffect(() => { useEffect(() => {
hooks.addHook(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE, handle_key_down); const sub = hooks
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
.subscribe(handle_key_down);
return () => { return () => {
hooks.removeHook( sub.unsubscribe();
HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE,
handle_key_down
);
}; };
}, [hooks, handle_key_down]); }, [hooks, handle_key_down]);

View File

@@ -5,7 +5,7 @@ import { MuiClickAwayListener } from '@toeverything/components/ui';
import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo'; import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo';
import { Point } from '@toeverything/utils'; import { Point } from '@toeverything/utils';
import { ReferenceMenuContainer } from './container'; import { ReferenceMenuContainer } from './Container';
import { QueryBlocks, QueryResult } from '../../search'; import { QueryBlocks, QueryResult } from '../../search';
export type ReferenceMenuProps = { export type ReferenceMenuProps = {
@@ -87,15 +87,17 @@ export const ReferenceMenu = ({ editor, hooks, style }: ReferenceMenuProps) => {
); );
useEffect(() => { useEffect(() => {
hooks.addHook(HookType.ON_ROOT_NODE_KEYUP, handle_keyup); const sub = hooks
hooks.addHook(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE, handle_key_down); .get(HookType.ON_ROOT_NODE_KEYUP)
.subscribe(handle_keyup);
sub.add(
hooks
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
.subscribe(handle_key_down)
);
return () => { return () => {
hooks.removeHook(HookType.ON_ROOT_NODE_KEYUP, handle_keyup); sub.unsubscribe();
hooks.removeHook(
HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE,
handle_key_down
);
}; };
}, [handle_keyup, handle_key_down, hooks]); }, [handle_keyup, handle_key_down, hooks]);

View File

@@ -3,30 +3,10 @@ import {
styled, styled,
} from '@toeverything/components/ui'; } from '@toeverything/components/ui';
import { Protocol } from '@toeverything/datasource/db-service'; import { Protocol } from '@toeverything/datasource/db-service';
import type { import type { AsyncBlock, Virgo } from '@toeverything/framework/virgo';
AsyncBlock, import { useEffect, useState, type CSSProperties } from 'react';
PluginHooks,
Virgo,
} from '@toeverything/framework/virgo';
import {
createContext,
useContext,
useEffect,
useState,
type CSSProperties,
} from 'react';
export type Store = export const MenuApp = ({ editor }: { editor: Virgo }) => {
| {
editor: Virgo;
hooks: PluginHooks;
}
| Record<string, never>;
export const StoreContext = createContext<Store>({});
export const MenuApp = () => {
const { editor } = useContext(StoreContext);
const [show, setShow] = useState<boolean>(false); const [show, setShow] = useState<boolean>(false);
const [style, setStyle] = useState<CSSProperties>(); const [style, setStyle] = useState<CSSProperties>();
const [selectedNodes, setSelectedNodes] = useState<AsyncBlock[]>([]); const [selectedNodes, setSelectedNodes] = useState<AsyncBlock[]>([]);

View File

@@ -1,7 +1,7 @@
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 { MenuApp, StoreContext } from './MenuApp'; import { MenuApp } from './MenuApp';
const PLUGIN_NAME = 'selection-group'; const PLUGIN_NAME = 'selection-group';
@@ -10,27 +10,23 @@ export class SelectionGroupPlugin extends BasePlugin {
return PLUGIN_NAME; return PLUGIN_NAME;
} }
private root: PluginRenderRoot | undefined; private _root: PluginRenderRoot | undefined;
protected override _onRender() { protected override _onRender() {
this.root = new PluginRenderRoot({ this._root = new PluginRenderRoot({
name: SelectionGroupPlugin.pluginName, name: SelectionGroupPlugin.pluginName,
render: this.editor.reactRenderRoot?.render, render: this.editor.reactRenderRoot?.render,
}); });
this.root.mount(); this._root.mount();
this.root.render( this._root.render(
<StrictMode> <StrictMode>
<StoreContext.Provider <MenuApp editor={this.editor} />
value={{ editor: this.editor, hooks: this.hooks }}
>
<MenuApp />
</StoreContext.Provider>
</StrictMode> </StrictMode>
); );
} }
public override dispose() { public override dispose() {
this.root?.unmount(); this._root?.unmount();
super.dispose(); super.dispose();
} }
} }

View File

@@ -15,7 +15,9 @@ export class FullTextSearchPlugin extends BasePlugin {
} }
public override init(): void { public override init(): void {
this.hooks.addHook(HookType.ON_SEARCH, this.handle_search, this); this.sub.add(
this.hooks.get(HookType.ON_SEARCH).subscribe(this._handleSearch)
);
} }
protected override _onRender(): void { protected override _onRender(): void {
@@ -31,12 +33,13 @@ export class FullTextSearchPlugin extends BasePlugin {
this.#root.unmount(); this.#root.unmount();
// this.#root = undefined; // this.#root = undefined;
} }
this.sub.unsubscribe();
} }
private handle_search() { private _handleSearch = () => {
this.editor.setHotKeysScope('search'); this.editor.setHotKeysScope('search');
this.render_search(); this.render_search();
} };
private render_search() { private render_search() {
if (this.#root) { if (this.#root) {
this.#root.mount(); this.#root.mount();