init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions
@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import { Virgo, PluginHooks } from '@toeverything/framework/virgo';
import { Cascader, CascaderItemProps } from '@toeverything/components/ui';
import { TurnIntoMenu } from './TurnIntoMenu';
import {
DeleteCashBinIcon,
TurnIntoIcon,
UngroupIcon,
} from '@toeverything/components/icons';
interface LeftMenuProps {
anchorEl?: Element;
children?: React.ReactElement;
onClose: () => void;
editor?: Virgo;
hooks: PluginHooks;
blockId: string;
}
export function LeftMenu(props: LeftMenuProps) {
const { editor, anchorEl, hooks, blockId } = props;
const menu: CascaderItemProps[] = [
{
title: 'Delete',
callback: () => {
editor.commands.blockCommands.removeBlock(blockId);
},
shortcut: 'Del',
icon: <DeleteCashBinIcon />,
},
{
title: 'Turn into',
subItems: [],
children: (
<TurnIntoMenu
editor={editor}
hooks={hooks}
blockId={blockId}
onClose={() => {
props.onClose();
editor.selection.setSelectedNodesIds([]);
}}
/>
),
icon: <TurnIntoIcon />,
},
{
title: 'Divide Here As A New Group',
icon: <UngroupIcon />,
callback: () => {
editor.commands.blockCommands.splitGroupFromBlock(blockId);
},
},
].filter(v => v);
const [menuList, setMenuList] = useState<CascaderItemProps[]>(menu);
const filter_items = (
value: string,
menuList: CascaderItemProps[],
filterList: CascaderItemProps[]
) => {
menuList.forEach(item => {
if (item?.subItems.length === 0) {
if (item.title.toLocaleLowerCase().indexOf(value) !== -1) {
filterList.push(item);
}
} else {
filter_items(value, item.subItems || [], filterList);
}
});
};
const on_filter = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
const value = e.currentTarget.value;
if (!value) {
setMenuList(menu);
} else {
const filterList: CascaderItemProps[] = [];
filter_items(value.toLocaleLowerCase(), menu, filterList);
setMenuList(
filterList.length > 0 ? filterList : [{ title: 'No Result' }]
);
}
};
return (
<>
{props.children}
<Cascader
items={menuList}
anchorEl={anchorEl}
placement="bottom-start"
open={Boolean(anchorEl)}
onClose={() => props.onClose()}
/>
</>
);
}
@@ -0,0 +1,271 @@
import { useState, useEffect, FC } from 'react';
import {
Virgo,
BlockDomInfo,
HookType,
PluginHooks,
BlockDropPlacement,
} from '@toeverything/framework/virgo';
import { Button } from '@toeverything/components/common';
import { styled } from '@toeverything/components/ui';
import { LeftMenu } from './LeftMenu';
import { debounce } from '@toeverything/utils';
import type { Subject } from 'rxjs';
import { HandleChildIcon } from '@toeverything/components/icons';
import { MENU_WIDTH } from './menu-config';
const MENU_BUTTON_OFFSET = 12;
export type LineInfoSubject = Subject<
| {
direction: BlockDropPlacement;
blockInfo: BlockDomInfo;
}
| undefined
>;
export type LeftMenuProps = {
editor: Virgo;
hooks: PluginHooks;
defaultVisible?: boolean;
blockInfo: Subject<BlockDomInfo | undefined>;
lineInfo: LineInfoSubject;
};
type LineInfo = {
direction: BlockDropPlacement;
blockInfo: BlockDomInfo;
};
function Line(props: { lineInfo: LineInfo }) {
const { lineInfo } = props;
if (!lineInfo || lineInfo.direction === BlockDropPlacement.none) {
return null;
}
const { direction, blockInfo } = lineInfo;
const finalDirection = direction;
const lineStyle = {
zIndex: 2,
position: 'absolute' as const,
background: '#502EC4',
};
const intersectionRect = blockInfo.rect;
const horizontalLineStyle = {
...lineStyle,
width: intersectionRect.width,
height: 2,
left: intersectionRect.x - blockInfo.rootRect.x,
};
const topLineStyle = {
...horizontalLineStyle,
top: intersectionRect.top,
};
const bottomLineStyle = {
...horizontalLineStyle,
top: intersectionRect.bottom + 1 - blockInfo.rootRect.y,
};
const verticalLineStyle = {
...lineStyle,
width: 2,
height: intersectionRect.height,
top: intersectionRect.y - blockInfo.rootRect.y,
};
const leftLineStyle = {
...verticalLineStyle,
left: intersectionRect.x - 10 - blockInfo.rootRect.x,
};
const rightLineStyle = {
...verticalLineStyle,
left: intersectionRect.right + 10 - blockInfo.rootRect.x,
};
const styleMap = {
left: leftLineStyle,
right: rightLineStyle,
top: topLineStyle,
bottom: bottomLineStyle,
};
return (
<div className="editor-menu-line" style={styleMap[finalDirection]} />
);
}
function DragComponent(props: {
children: React.ReactNode;
style: React.CSSProperties;
onDragStart: (event: React.DragEvent<Element>) => void;
onDragEnd: (event: React.DragEvent<Element>) => void;
}) {
const { style, children, onDragStart, onDragEnd } = props;
return (
<LigoLeftMenu
draggable
style={style}
onMouseMove={event => event.stopPropagation()}
onMouseDown={event => event.stopPropagation()}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
{children}
</LigoLeftMenu>
);
}
export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
const { editor, blockInfo, defaultVisible, hooks, lineInfo } = props;
const [visible, setVisible] = useState(defaultVisible);
const [anchorEl, setAnchorEl] = useState<Element>();
const [block, setBlock] = useState<BlockDomInfo | undefined>();
const [line, setLine] = useState<LineInfo | undefined>(undefined);
const handleDragStart = (event: React.DragEvent<Element>) => {
window.addEventListener('dragover', handleDragOverCapture, {
capture: true,
});
hooks.addHook(
HookType.ON_ROOTNODE_DRAG_OVER_CAPTURE,
handleDragOverCapture
);
const onDragStart = async (event: React.DragEvent<Element>) => {
if (block == null) return;
const dragImage = await editor.blockHelper.getBlockDragImg(
block.blockId
);
if (dragImage) {
event.dataTransfer.setDragImage(dragImage, -50, -10);
editor.dragDropManager.setDragBlockInfo(event, block.blockId);
}
setVisible(false);
};
onDragStart(event);
event.stopPropagation();
};
const handleDragEnd = (event: React.DragEvent<Element>) => {
event.preventDefault();
window.removeEventListener('dragover', handleDragOverCapture, {
capture: true,
});
setLine(undefined);
};
const onClick = (event: React.MouseEvent) => {
if (block == null) return;
const currentTarget = event.currentTarget;
editor.selection.setSelectedNodesIds([block.blockId]);
setVisible(true);
setAnchorEl(currentTarget);
};
/**
* clear line info
*/
const handleDragOverCapture = debounce((e: MouseEvent) => {
const { target } = e;
if (
target instanceof HTMLElement &&
(!target.closest('[data-block-id]') ||
!editor.container.contains(target))
) {
setLine(undefined);
}
}, 10);
useEffect(() => {
const sub = blockInfo.subscribe(block => {
setBlock(block);
if (block != null) {
setVisible(true);
}
});
return () => sub.unsubscribe();
}, [blockInfo, editor]);
useEffect(() => {
const sub = lineInfo.subscribe(data => {
if (data == null) {
setLine(undefined);
} else {
const { direction, blockInfo } = data;
setLine(prev => {
if (
prev?.blockInfo.blockId !== blockInfo.blockId ||
prev?.direction !== direction
) {
return {
direction,
blockInfo,
};
} else {
return prev;
}
});
}
});
return () => sub.unsubscribe();
}, [editor.dragDropManager, lineInfo]);
return (
<>
{block && (
<DragComponent
style={{
position: 'absolute',
left:
Math.min(
block.rect.left -
MENU_WIDTH -
MENU_BUTTON_OFFSET
) - block.rootRect.left,
top: block.rect.top - block.rootRect.top,
opacity: visible ? 1 : 0,
zIndex: 1,
}}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
{
<LeftMenu
anchorEl={anchorEl}
editor={props.editor}
hooks={props.hooks}
onClose={() => setAnchorEl(undefined)}
blockId={block.blockId}
>
<Draggable onClick={onClick}>
<HandleChildIcon />
</Draggable>
</LeftMenu>
}
</DragComponent>
)}
<Line lineInfo={line} />
</>
);
};
const Draggable = styled(Button)({
cursor: 'grab',
padding: '0',
display: 'inlineFlex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent',
width: '24px',
height: '24px',
':hover': {
backgroundColor: '#edeef0',
borderRadius: '4px',
},
});
const LigoLeftMenu = styled('div')({
backgroundColor: 'transparent',
marginRight: '4px',
});
@@ -0,0 +1,176 @@
import { BlockDomInfo, HookType } from '@toeverything/framework/virgo';
import React, { StrictMode } from 'react';
import { BasePlugin } from '../../base-plugin';
import { ignoreBlockTypes } from './menu-config';
import { LineInfoSubject, LeftMenuDraggable } from './LeftMenuDraggable';
import { PluginRenderRoot } from '../../utils';
import { Subject } from 'rxjs';
import { domToRect, last, Point } from '@toeverything/utils';
export class LeftMenuPlugin extends BasePlugin {
private mousedown?: boolean;
private root?: PluginRenderRoot;
private preBlockId: string;
private hideTimer: number;
private _blockInfo: Subject<BlockDomInfo | undefined> = new Subject();
private _lineInfo: LineInfoSubject = new Subject();
public static override get pluginName(): string {
return 'left-menu';
}
public override init(): void {
this.hooks.addHook(
HookType.AFTER_ON_NODE_MOUSE_MOVE,
this._handleMouseMove
);
this.hooks.addHook(
HookType.ON_ROOTNODE_MOUSE_DOWN,
this._handleMouseDown
);
this.hooks.addHook(
HookType.ON_ROOTNODE_MOUSE_LEAVE,
this._handleRootMouseLeave,
this
);
this.hooks.addHook(HookType.ON_ROOTNODE_MOUSE_UP, this._handleMouseUp);
this.hooks.addHook(
HookType.AFTER_ON_NODE_DRAG_OVER,
this._handleDragOverBlockNode
);
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 = () => {
this.preBlockId = '';
this._lineInfo.next(undefined);
};
private _handleDragOverBlockNode = async (
event: React.DragEvent<Element>,
blockInfo: BlockDomInfo
) => {
const { type, dom, blockId } = blockInfo;
if (this.editor.dragDropManager.isDragBlock(event)) {
event.preventDefault();
if (ignoreBlockTypes.includes(type)) {
return;
}
const direction =
await this.editor.dragDropManager.checkBlockDragTypes(
event,
dom,
blockId
);
this._lineInfo.next({ direction, blockInfo });
}
};
private _handleMouseMove = async (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
node: BlockDomInfo
) => {
if (!this.hideTimer) {
this.hideTimer = window.setTimeout(() => {
if (this.mousedown) {
this._hideLeftMenu();
return;
}
this.hideTimer = 0;
}, 300);
}
if (this.editor.readonly) {
this._hideLeftMenu();
return;
}
if (node.blockId !== this.preBlockId) {
if (node.dom) {
const mousePoint = new Point(e.clientX, e.clientY);
const children = await (
await this.editor.getBlockById(node.blockId)
).children();
// if mouse point is between the first and last child do not show left menu
if (children.length) {
const firstChildren = children[0];
const lastChildren = last(children);
if (firstChildren.dom && lastChildren.dom) {
const firstChildrenRect = domToRect(firstChildren.dom);
const lastChildrenRect = domToRect(lastChildren.dom);
if (
firstChildrenRect.top < mousePoint.y &&
lastChildrenRect.bottom > mousePoint.y
) {
return;
}
}
}
}
this.preBlockId = node.blockId;
this._showLeftMenu(node);
}
};
private _handleMouseUp(
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
node: BlockDomInfo
) {
if (this.hideTimer) {
window.clearTimeout(this.hideTimer);
this.hideTimer = 0;
}
this.mousedown = false;
}
private _handleMouseDown = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
node: BlockDomInfo
) => {
this.mousedown = true;
};
private _hideLeftMenu = (): void => {
this._blockInfo.next(undefined);
};
private _handleKeyDown = () => {
this._hideLeftMenu();
};
private _showLeftMenu = (blockInfo: BlockDomInfo): void => {
if (ignoreBlockTypes.includes(blockInfo.type)) {
return;
}
this._blockInfo.next(blockInfo);
};
protected override on_render(): void {
this.root = new PluginRenderRoot({
name: LeftMenuPlugin.pluginName,
render: (...args) => {
return this.editor.reactRenderRoot?.render(...args);
},
});
this.root.mount();
this.root.render(
<StrictMode>
<LeftMenuDraggable
key={Math.random() + ''}
defaultVisible={true}
editor={this.editor}
hooks={this.hooks}
blockInfo={this._blockInfo}
lineInfo={this._lineInfo}
/>
</StrictMode>
);
}
public override dispose(): void {
// TODO: rxjs
this.root?.unmount();
}
}
@@ -0,0 +1,40 @@
import React from 'react';
import { BlockFlavorKeys, Protocol } from '@toeverything/datasource/db-service';
import { Virgo, PluginHooks } from '@toeverything/framework/virgo';
import { CommandMenuContainer } from '../command-menu/Container';
import { defaultCategoriesList, defaultTypeList } from '../command-menu/config';
interface TurnIntoMenuProps {
editor: Virgo;
hooks: PluginHooks;
blockId: string;
onClose?: () => void;
}
export const TurnIntoMenu = ({
blockId,
editor,
hooks,
onClose,
}: TurnIntoMenuProps) => {
const handle_select = (type: string) => {
if (Protocol.Block.Type[type as BlockFlavorKeys]) {
editor.commands.blockCommands.convertBlock(
blockId,
type as BlockFlavorKeys
);
onClose && onClose();
}
};
return (
<CommandMenuContainer
editor={editor}
hooks={hooks}
isShow={true}
blockId={blockId}
onSelected={handle_select}
types={defaultTypeList}
categories={defaultCategoriesList}
style={{ position: 'relative', boxShadow: 'none', padding: '0' }}
/>
);
};
@@ -0,0 +1,104 @@
import { BlockFlavorKeys, Protocol } from '@toeverything/datasource/db-service';
import ShortTextIcon from '@mui/icons-material/ShortText';
import TitleIcon from '@mui/icons-material/Title';
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
import {
CodeBlockInlineIcon,
PagesIcon,
} from '@toeverything/components/common';
import ListIcon from '@mui/icons-material/List';
export const MENU_WIDTH = 14;
export const pageConvertIconSize = 24;
type MenuItem = {
name: string[];
title: string;
blockType: string;
render?({ renderNode }: { renderNode: any }): void;
icon: typeof ShortTextIcon;
params?: Record<string, any>;
disable?: boolean;
};
const textTypeBlocks: MenuItem[] = [
{
name: ['text'], // name represents the search keyword
title: 'Text',
blockType: Protocol.Block.Type.text,
icon: ShortTextIcon,
},
{
name: ['quote'],
title: 'Quote',
blockType: Protocol.Block.Type.quote,
icon: ShortTextIcon,
},
{
name: ['page'],
title: 'Page',
blockType: Protocol.Block.Type.page,
icon: PagesIcon as any,
},
{
name: ['todo'],
title: 'To-do list',
blockType: Protocol.Block.Type.todo,
icon: ListIcon,
},
{
name: ['heading1'],
title: 'Heading 1',
blockType: Protocol.Block.Type.heading1,
icon: TitleIcon,
},
{
name: ['heading2'],
title: 'Heading 2',
blockType: Protocol.Block.Type.heading2,
icon: TitleIcon,
},
{
name: ['heading3'],
title: 'Heading 3',
blockType: Protocol.Block.Type.heading3,
icon: TitleIcon,
},
{
name: ['code'],
title: 'Code',
blockType: Protocol.Block.Type.code,
icon: CodeBlockInlineIcon as any,
},
{
name: ['bullet'],
title: 'Bullet List',
blockType: Protocol.Block.Type.bullet,
icon: FormatListBulletedIcon,
},
{
name: ['call', 'callout'],
title: 'Callout',
blockType: Protocol.Block.Type.callout,
icon: NotificationsNoneIcon,
},
{
name: ['div', 'divider'],
title: 'Divider',
blockType: Protocol.Block.Type.divider,
icon: HorizontalRuleIcon,
},
];
export const addMenuList = [...textTypeBlocks].filter(v => v);
export const textConvertMenuList = textTypeBlocks;
export const ignoreBlockTypes: BlockFlavorKeys[] = [
Protocol.Block.Type.workspace,
Protocol.Block.Type.page,
Protocol.Block.Type.group,
Protocol.Block.Type.title,
Protocol.Block.Type.grid,
Protocol.Block.Type.gridItem,
];