mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 07:36:42 +08:00
init: the first public commit for AFFiNE
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { useMemo } from 'react';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { CommandMenuCategories } from './config';
|
||||
|
||||
type MenuCategoriesProps = {
|
||||
currentCategories: CommandMenuCategories;
|
||||
onSetCategories?: (categories: CommandMenuCategories) => void;
|
||||
categories: Array<CommandMenuCategories>;
|
||||
};
|
||||
|
||||
export const MenuCategories = ({
|
||||
currentCategories,
|
||||
onSetCategories,
|
||||
categories,
|
||||
}: MenuCategoriesProps) => {
|
||||
const categories_data = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
type: CommandMenuCategories.pages,
|
||||
text: 'PAGES',
|
||||
},
|
||||
{
|
||||
type: CommandMenuCategories.typesetting,
|
||||
text: 'TYPESETTING',
|
||||
},
|
||||
{
|
||||
type: CommandMenuCategories.lists,
|
||||
text: 'LISTS',
|
||||
},
|
||||
{
|
||||
type: CommandMenuCategories.media,
|
||||
text: 'MEDIA',
|
||||
},
|
||||
{
|
||||
type: CommandMenuCategories.blocks,
|
||||
text: 'BLOCKS',
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const handle_click = (type: CommandMenuCategories) => {
|
||||
onSetCategories && onSetCategories(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles('rootContainer')}>
|
||||
{categories_data.map((menu_category, index) => {
|
||||
const { type, text } = menu_category;
|
||||
return categories.includes(type) ? (
|
||||
<button
|
||||
className={styles({
|
||||
categoryItem: true,
|
||||
activeItem: currentCategories === type,
|
||||
})}
|
||||
key={type}
|
||||
onClick={() => {
|
||||
handle_click(type);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = style9.create({
|
||||
rootContainer: {
|
||||
minWidth: '120px',
|
||||
marginTop: '6px',
|
||||
},
|
||||
categoryItem: {
|
||||
display: 'flex',
|
||||
width: '120px',
|
||||
paddingLeft: '12px',
|
||||
paddingTop: '6px',
|
||||
paddingBottom: '6px',
|
||||
borderRadius: '5px',
|
||||
color: '#98ACBD',
|
||||
fontSize: '12px',
|
||||
lineHeight: '14px',
|
||||
fontFamily: 'Helvetica,Arial,"Microsoft Yahei",SimHei,sans-serif',
|
||||
textAlign: 'justify',
|
||||
letterSpacing: '1.5px',
|
||||
},
|
||||
activeItem: {
|
||||
backgroundColor: 'rgba(152, 172, 189, 0.1)',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
|
||||
import { BasePlugin } from '../../base-plugin';
|
||||
import { CommandMenu } from './Menu';
|
||||
|
||||
const PLUGIN_NAME = 'command-menu';
|
||||
|
||||
export class CommandMenuPlugin extends BasePlugin {
|
||||
private root?: Root;
|
||||
|
||||
public static override get pluginName(): string {
|
||||
return PLUGIN_NAME;
|
||||
}
|
||||
|
||||
protected override on_render(): void {
|
||||
const container = document.createElement('div');
|
||||
// TODO remove
|
||||
container.classList.add(`id-${PLUGIN_NAME}`);
|
||||
// this.editor.attachElement(this.menu_container);
|
||||
window.document.body.appendChild(container);
|
||||
this.root = createRoot(container);
|
||||
this.render_command_menu();
|
||||
}
|
||||
|
||||
private renderCommandMenu(): void {
|
||||
//TODO If you change to PluginRenderRoot here, you need to support PluginRenderRoot under body
|
||||
this.root?.render(
|
||||
<StrictMode>
|
||||
<CommandMenu editor={this.editor} hooks={this.hooks} />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { BlockFlavorKeys } from '@toeverything/datasource/db-service';
|
||||
import { Virgo, PluginHooks, HookType } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
CommonList,
|
||||
CommonListItem,
|
||||
commonListContainer,
|
||||
} from '@toeverything/components/common';
|
||||
import { domToRect } from '@toeverything/utils';
|
||||
|
||||
import { MenuCategories } from './Categories';
|
||||
import { menuItemsMap, CommandMenuCategories } from './config';
|
||||
import { QueryResult } from '../../search';
|
||||
|
||||
export type CommandMenuContainerProps = {
|
||||
editor: Virgo;
|
||||
hooks: PluginHooks;
|
||||
style?: React.CSSProperties;
|
||||
isShow?: boolean;
|
||||
blockId: string;
|
||||
onSelected?: (item: BlockFlavorKeys | string) => void;
|
||||
onclose?: () => void;
|
||||
searchBlocks?: QueryResult;
|
||||
types?: Array<BlockFlavorKeys | string>;
|
||||
categories: Array<CommandMenuCategories>;
|
||||
};
|
||||
|
||||
export const CommandMenuContainer = ({
|
||||
hooks,
|
||||
isShow = false,
|
||||
onSelected,
|
||||
onclose,
|
||||
types,
|
||||
categories,
|
||||
searchBlocks,
|
||||
style,
|
||||
}: CommandMenuContainerProps) => {
|
||||
const menu_ref = useRef<HTMLDivElement>(null);
|
||||
const [current_item, set_current_item] = useState<
|
||||
BlockFlavorKeys | string | undefined
|
||||
>();
|
||||
const [need_check_into_view, set_need_check_into_view] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const current_category = useMemo(
|
||||
() =>
|
||||
(Object.entries(menuItemsMap).find(
|
||||
([, infos]) =>
|
||||
infos.findIndex(info => current_item === info.type) !== -1
|
||||
)?.[0] || CommandMenuCategories.pages) as CommandMenuCategories,
|
||||
[current_item]
|
||||
);
|
||||
|
||||
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) {
|
||||
set_current_item(types[0]);
|
||||
}
|
||||
if (!isShow) {
|
||||
onclose && onclose();
|
||||
}
|
||||
}, [isShow]);
|
||||
|
||||
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(() => {
|
||||
hooks.addHook(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE, handle_key_down);
|
||||
|
||||
return () => {
|
||||
hooks.removeHook(
|
||||
HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE,
|
||||
handle_key_down
|
||||
);
|
||||
};
|
||||
}, [hooks, handle_key_down]);
|
||||
|
||||
const handleSetCategories = (type: CommandMenuCategories) => {
|
||||
const newItem = menuItemsMap[type][0].type;
|
||||
set_need_check_into_view(true);
|
||||
set_current_item(newItem);
|
||||
};
|
||||
|
||||
const items = useMemo(() => {
|
||||
const blocks = searchBlocks?.map(
|
||||
block => ({ block } as CommonListItem)
|
||||
);
|
||||
if (blocks?.length) {
|
||||
blocks.push({ divider: CommandMenuCategories.pages });
|
||||
}
|
||||
return [
|
||||
...(blocks || []),
|
||||
...Object.entries(menuItemsMap).flatMap(
|
||||
([category, items], idx, all) => {
|
||||
let render_separator = false;
|
||||
const lines: CommonListItem[] = items
|
||||
.filter(item => types.includes(item.type))
|
||||
.map(item => {
|
||||
const { text, type, icon } = item;
|
||||
render_separator = true;
|
||||
return {
|
||||
content: { id: type, content: text, icon },
|
||||
};
|
||||
});
|
||||
if (render_separator && idx !== all.length - 1) {
|
||||
lines.push({ divider: category });
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
),
|
||||
];
|
||||
}, [searchBlocks, types]);
|
||||
|
||||
return isShow ? (
|
||||
<div
|
||||
ref={menu_ref}
|
||||
className={styles('rootContainer')}
|
||||
onKeyDownCapture={handle_key_down}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles('contentContainer')}>
|
||||
<MenuCategories
|
||||
currentCategories={current_category}
|
||||
onSetCategories={handleSetCategories}
|
||||
categories={categories}
|
||||
/>
|
||||
<CommonList
|
||||
items={items}
|
||||
onSelected={type => onSelected?.(type)}
|
||||
currentItem={current_item}
|
||||
setCurrentItem={set_current_item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const styles = style9.create({
|
||||
rootContainer: {
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
width: 352,
|
||||
maxHeight: 525,
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
backgroundColor: '#fff',
|
||||
padding: '8px 4px',
|
||||
},
|
||||
contentContainer: {
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
maxHeight: 493,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,265 @@
|
||||
import { BlockFlavorKeys, Protocol } from '@toeverything/datasource/db-service';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { MuiClickAwayListener } from '@toeverything/components/ui';
|
||||
import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo';
|
||||
import { Point } from '@toeverything/utils';
|
||||
|
||||
import { CommandMenuContainer } from './Container';
|
||||
import {
|
||||
CommandMenuCategories,
|
||||
commandMenuHandlerMap,
|
||||
commonCommandMenuHandler,
|
||||
menuItemsMap,
|
||||
} from './config';
|
||||
import { QueryBlocks, QueryResult } from '../../search';
|
||||
|
||||
export type CommandMenuProps = {
|
||||
editor: Virgo;
|
||||
hooks: PluginHooks;
|
||||
style?: { left: number; top: number };
|
||||
};
|
||||
type CommandMenuPosition = {
|
||||
left: number;
|
||||
top: number | 'initial';
|
||||
bottom: number | 'initial';
|
||||
};
|
||||
|
||||
export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
|
||||
const [is_show, set_is_show] = useState(false);
|
||||
const [block_id, set_block_id] = useState<string>();
|
||||
const [commandMenuPosition, setCommandMenuPosition] =
|
||||
useState<CommandMenuPosition>({
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
});
|
||||
|
||||
const [search_text, set_search_text] = useState<string>('');
|
||||
const [search_blocks, set_search_blocks] = useState<QueryResult>([]);
|
||||
const commandMenuContentRef = useRef();
|
||||
// TODO: Two-way link to be developed
|
||||
// useEffect(() => {
|
||||
// QueryBlocks(editor, search_text, result => set_search_blocks(result));
|
||||
// }, [editor, search_text]);
|
||||
|
||||
const [types, categories] = useMemo(() => {
|
||||
const types: Array<BlockFlavorKeys | string> = [];
|
||||
const categories: Array<CommandMenuCategories> = [];
|
||||
if (search_blocks.length) {
|
||||
Object.values(search_blocks).forEach(({ id }) => types.push(id));
|
||||
categories.push(CommandMenuCategories.pages);
|
||||
}
|
||||
Object.entries(menuItemsMap).forEach(([category, itemInfoList]) => {
|
||||
itemInfoList.forEach(info => {
|
||||
if (
|
||||
!search_text ||
|
||||
info.text.toLowerCase().includes(search_text.toLowerCase())
|
||||
) {
|
||||
types.push(info.type);
|
||||
}
|
||||
|
||||
if (
|
||||
!categories.includes(category as CommandMenuCategories) ||
|
||||
types.includes(info.type)
|
||||
) {
|
||||
categories.push(category as CommandMenuCategories);
|
||||
}
|
||||
});
|
||||
});
|
||||
return [types, categories];
|
||||
}, [search_blocks, search_text]);
|
||||
|
||||
const check_if_show_command_menu = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const { type, anchorNode } = editor.selection.currentSelectInfo;
|
||||
if (event.key === '/' && type === 'Range') {
|
||||
if (anchorNode) {
|
||||
const text = editor.blockHelper.getBlockTextBeforeSelection(
|
||||
anchorNode.id
|
||||
);
|
||||
if (text.endsWith('/')) {
|
||||
set_block_id(anchorNode.id);
|
||||
editor.blockHelper.removeSearchSlash(block_id);
|
||||
setTimeout(() => {
|
||||
const textSelection =
|
||||
editor.blockHelper.selectionToSlateRange(
|
||||
anchorNode.id,
|
||||
editor.selection.currentSelectInfo
|
||||
.browserSelection
|
||||
);
|
||||
if (textSelection) {
|
||||
const { anchor } = textSelection;
|
||||
editor.blockHelper.setSearchSlash(
|
||||
anchorNode.id,
|
||||
anchor
|
||||
);
|
||||
}
|
||||
});
|
||||
set_search_text('');
|
||||
set_is_show(true);
|
||||
const rect =
|
||||
editor.selection.currentSelectInfo?.browserSelection
|
||||
?.getRangeAt(0)
|
||||
?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
let top = rect.top;
|
||||
let clientHeight =
|
||||
document.documentElement.clientHeight;
|
||||
|
||||
const COMMAND_MENU_HEIGHT = 509;
|
||||
if (clientHeight - top <= COMMAND_MENU_HEIGHT) {
|
||||
top = clientHeight - top + 10;
|
||||
setCommandMenuPosition({
|
||||
left: rect.left,
|
||||
bottom: top,
|
||||
top: 'initial',
|
||||
});
|
||||
} else {
|
||||
top += 24;
|
||||
setCommandMenuPosition({
|
||||
left: rect.left,
|
||||
top: top,
|
||||
bottom: 'initial',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[editor, block_id]
|
||||
);
|
||||
|
||||
const handle_click_others = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (is_show) {
|
||||
const { anchorNode } = editor.selection.currentSelectInfo;
|
||||
if (anchorNode.id !== block_id) {
|
||||
set_is_show(false);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
const searchText =
|
||||
editor.blockHelper.getSearchSlashText(block_id);
|
||||
// check if has search text
|
||||
if (searchText && searchText.startsWith('/')) {
|
||||
set_search_text(searchText.slice(1));
|
||||
} else {
|
||||
set_is_show(false);
|
||||
}
|
||||
if (searchText.length > 6 && !types.length) {
|
||||
set_is_show(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[editor, is_show, block_id, types]
|
||||
);
|
||||
|
||||
const handle_keyup = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
check_if_show_command_menu(event);
|
||||
handle_click_others(event);
|
||||
},
|
||||
[check_if_show_command_menu, handle_click_others]
|
||||
);
|
||||
|
||||
const handle_key_down = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.code === 'Escape') {
|
||||
set_is_show(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
hooks.addHook(HookType.ON_ROOT_NODE_KEYUP, handle_keyup);
|
||||
hooks.addHook(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE, handle_key_down);
|
||||
|
||||
return () => {
|
||||
hooks.removeHook(HookType.ON_ROOT_NODE_KEYUP, handle_keyup);
|
||||
hooks.removeHook(
|
||||
HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE,
|
||||
handle_key_down
|
||||
);
|
||||
};
|
||||
}, [handle_keyup, handle_key_down, hooks]);
|
||||
|
||||
const handle_click_away = () => {
|
||||
set_is_show(false);
|
||||
};
|
||||
|
||||
const handle_selected = async (type: BlockFlavorKeys | string) => {
|
||||
const text = await editor.commands.textCommands.getBlockText(block_id);
|
||||
editor.blockHelper.removeSearchSlash(block_id, true);
|
||||
if (type.startsWith('Virgo')) {
|
||||
const handler =
|
||||
commandMenuHandlerMap[Protocol.Block.Type.reference];
|
||||
handler(block_id, type, editor);
|
||||
} else if (text.length > 1) {
|
||||
const handler = commandMenuHandlerMap[type];
|
||||
if (handler) {
|
||||
await handler(block_id, type, editor);
|
||||
} else {
|
||||
await commonCommandMenuHandler(block_id, type, editor);
|
||||
}
|
||||
const block = await editor.getBlockById(block_id);
|
||||
block.remove();
|
||||
} else {
|
||||
if (Protocol.Block.Type[type as BlockFlavorKeys]) {
|
||||
const block = await editor.commands.blockCommands.convertBlock(
|
||||
block_id,
|
||||
type as BlockFlavorKeys
|
||||
);
|
||||
block.firstCreateFlag = true;
|
||||
}
|
||||
}
|
||||
set_is_show(false);
|
||||
};
|
||||
|
||||
const handle_close = () => {
|
||||
editor.blockHelper.removeSearchSlash(block_id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles('commandMenu')}
|
||||
onKeyUpCapture={handle_keyup}
|
||||
ref={commandMenuContentRef}
|
||||
>
|
||||
<MuiClickAwayListener onClickAway={handle_click_away}>
|
||||
<CommandMenuContainer
|
||||
editor={editor}
|
||||
hooks={hooks}
|
||||
style={{
|
||||
...commandMenuPosition,
|
||||
...style,
|
||||
}}
|
||||
isShow={is_show}
|
||||
blockId={block_id}
|
||||
onSelected={handle_selected}
|
||||
onclose={handle_close}
|
||||
searchBlocks={search_blocks}
|
||||
types={types}
|
||||
categories={categories}
|
||||
/>
|
||||
</MuiClickAwayListener>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = style9.create({
|
||||
commandMenu: {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
import type { FC } from 'react';
|
||||
import {
|
||||
HeadingOneIcon,
|
||||
HeadingTwoIcon,
|
||||
HeadingThreeIcon,
|
||||
ToDoIcon,
|
||||
NumberIcon,
|
||||
BulletIcon,
|
||||
CodeIcon,
|
||||
TextIcon,
|
||||
PagesIcon,
|
||||
ImageIcon,
|
||||
FileIcon,
|
||||
QuoteIcon,
|
||||
CalloutIcon,
|
||||
DividerIcon,
|
||||
FigmaIcon,
|
||||
YoutubeIcon,
|
||||
EmbedIcon,
|
||||
} from '@toeverything/components/icons';
|
||||
import { SvgIconProps } from '@toeverything/components/ui';
|
||||
import { Virgo } from '@toeverything/framework/virgo';
|
||||
import { BlockFlavorKeys, Protocol } from '@toeverything/datasource/db-service';
|
||||
import { without } from '@toeverything/utils';
|
||||
|
||||
export enum CommandMenuCategories {
|
||||
pages = 'pages',
|
||||
typesetting = 'typesetting',
|
||||
lists = 'lists',
|
||||
media = 'media',
|
||||
blocks = 'blocks',
|
||||
}
|
||||
|
||||
type ClickItemHandler = (
|
||||
anchorNodeId: string,
|
||||
type: BlockFlavorKeys | string,
|
||||
editor: Virgo
|
||||
) => void;
|
||||
|
||||
export type CommandMenuDataType = {
|
||||
type: BlockFlavorKeys;
|
||||
text: string;
|
||||
icon: FC<SvgIconProps>;
|
||||
};
|
||||
|
||||
export const commonCommandMenuHandler: ClickItemHandler = async (
|
||||
anchorNodeId,
|
||||
type,
|
||||
editor
|
||||
) => {
|
||||
if (anchorNodeId) {
|
||||
if (Protocol.Block.Type[type as BlockFlavorKeys]) {
|
||||
const block = await editor.commands.blockCommands.createNextBlock(
|
||||
anchorNodeId,
|
||||
type as BlockFlavorKeys
|
||||
);
|
||||
editor.selectionManager.activeNodeByNodeId(block.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const menuItemsMap: {
|
||||
[k in CommandMenuCategories]: Array<CommandMenuDataType>;
|
||||
} = {
|
||||
[CommandMenuCategories.pages]: [],
|
||||
[CommandMenuCategories.typesetting]: [
|
||||
{
|
||||
text: 'Text',
|
||||
type: Protocol.Block.Type.text,
|
||||
icon: TextIcon,
|
||||
},
|
||||
{
|
||||
text: 'Page',
|
||||
type: 'page',
|
||||
icon: PagesIcon,
|
||||
},
|
||||
{
|
||||
text: 'Heading 1',
|
||||
type: Protocol.Block.Type.heading1,
|
||||
icon: HeadingOneIcon,
|
||||
},
|
||||
{
|
||||
text: 'Heading 2',
|
||||
type: Protocol.Block.Type.heading2,
|
||||
icon: HeadingTwoIcon,
|
||||
},
|
||||
{
|
||||
text: 'Heading 3',
|
||||
type: Protocol.Block.Type.heading3,
|
||||
icon: HeadingThreeIcon,
|
||||
},
|
||||
],
|
||||
[CommandMenuCategories.lists]: [
|
||||
{
|
||||
text: 'To do',
|
||||
type: Protocol.Block.Type.todo,
|
||||
icon: ToDoIcon,
|
||||
},
|
||||
{
|
||||
text: 'Number',
|
||||
type: Protocol.Block.Type.numbered,
|
||||
icon: NumberIcon,
|
||||
},
|
||||
{
|
||||
text: 'Bullet',
|
||||
type: Protocol.Block.Type.bullet,
|
||||
icon: BulletIcon,
|
||||
},
|
||||
],
|
||||
[CommandMenuCategories.media]: [
|
||||
{
|
||||
text: 'Image',
|
||||
type: Protocol.Block.Type.image,
|
||||
icon: ImageIcon,
|
||||
},
|
||||
{
|
||||
text: 'File',
|
||||
type: Protocol.Block.Type.file,
|
||||
icon: FileIcon,
|
||||
},
|
||||
{
|
||||
text: 'Embed Link',
|
||||
type: 'embedLink',
|
||||
icon: EmbedIcon,
|
||||
},
|
||||
{
|
||||
text: 'Figma',
|
||||
type: Protocol.Block.Type.figma,
|
||||
icon: FigmaIcon,
|
||||
},
|
||||
{
|
||||
text: 'Youtube',
|
||||
type: Protocol.Block.Type.youtube,
|
||||
icon: YoutubeIcon,
|
||||
},
|
||||
],
|
||||
[CommandMenuCategories.blocks]: [
|
||||
{
|
||||
text: 'Code',
|
||||
type: Protocol.Block.Type.code,
|
||||
icon: CodeIcon,
|
||||
},
|
||||
{
|
||||
text: 'Quote',
|
||||
icon: QuoteIcon,
|
||||
type: Protocol.Block.Type.quote,
|
||||
},
|
||||
{
|
||||
text: 'Callout',
|
||||
type: Protocol.Block.Type.callout,
|
||||
icon: CalloutIcon,
|
||||
},
|
||||
{
|
||||
text: 'Divider',
|
||||
icon: DividerIcon,
|
||||
type: Protocol.Block.Type.divider,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const defaultCategoriesList = without(
|
||||
Object.keys(menuItemsMap),
|
||||
'pages'
|
||||
) as Array<CommandMenuCategories>;
|
||||
|
||||
export const defaultTypeList = Object.values(menuItemsMap).reduce(
|
||||
(pre, cur) => {
|
||||
cur.forEach(info => {
|
||||
pre.push(info.type);
|
||||
});
|
||||
return pre;
|
||||
},
|
||||
[] as Array<BlockFlavorKeys>
|
||||
);
|
||||
|
||||
export const commandMenuHandlerMap: Partial<{
|
||||
[k in BlockFlavorKeys | string]: ClickItemHandler;
|
||||
}> = {
|
||||
[Protocol.Block.Type.page]: () => {},
|
||||
[Protocol.Block.Type.reference]: async (anchorNodeId, type, editor) => {
|
||||
if (anchorNodeId) {
|
||||
console.log(anchorNodeId, type, editor);
|
||||
const reflink_block =
|
||||
await editor.commands.blockCommands.createNextBlock(
|
||||
anchorNodeId,
|
||||
Protocol.Block.Type.reference
|
||||
);
|
||||
await reflink_block.setProperty('reference', type);
|
||||
}
|
||||
},
|
||||
[Protocol.Block.Type.image]: async (anchorNodeId, type, editor) => {
|
||||
if (anchorNodeId) {
|
||||
const image_block =
|
||||
await editor.commands.blockCommands.createNextBlock(
|
||||
anchorNodeId,
|
||||
Protocol.Block.Type.image
|
||||
);
|
||||
// set img block active open
|
||||
image_block.firstCreateFlag = true;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './CommandMenu';
|
||||
Reference in New Issue
Block a user