mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 09:06:19 +08:00
init: the first public commit for AFFiNE
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { Button } from '@toeverything/components/common';
|
||||
import { AsyncBlock, Virgo } from '@toeverything/components/editor-core';
|
||||
import { HandleParentIcon } from '@toeverything/components/icons';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { Point } from '@toeverything/utils';
|
||||
|
||||
export const ICON_WIDTH = 24;
|
||||
|
||||
type DragItemProps = {
|
||||
isShow: boolean;
|
||||
groupBlock: AsyncBlock;
|
||||
editor: Virgo;
|
||||
onPositionChange?: (position: Point) => void;
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const DragItem = function ({
|
||||
isShow,
|
||||
editor,
|
||||
groupBlock,
|
||||
...divProps
|
||||
}: DragItemProps) {
|
||||
return (
|
||||
<StyledDiv {...divProps}>
|
||||
<StyledButton>
|
||||
<HandleParentIcon />
|
||||
</StyledButton>
|
||||
</StyledDiv>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledDiv = styled('div')({
|
||||
padding: '0',
|
||||
display: 'inlineFlex',
|
||||
width: `${ICON_WIDTH}px`,
|
||||
height: `${ICON_WIDTH}px`,
|
||||
':hover': {
|
||||
backgroundColor: '#edeef0',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
});
|
||||
|
||||
const StyledButton = styled(Button)({
|
||||
padding: '0',
|
||||
display: 'inlineFlex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
HookType,
|
||||
PluginHooks,
|
||||
Virgo,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { domToRect, Point } from '@toeverything/utils';
|
||||
import { GroupDirection } from '@toeverything/framework/virgo';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { DragItem } from './DragItem';
|
||||
import { Line } from './Line';
|
||||
import { Menu } from './Menu';
|
||||
|
||||
type GroupMenuProps = {
|
||||
editor?: Virgo;
|
||||
hooks: PluginHooks;
|
||||
};
|
||||
export const GroupMenu = function ({ editor, hooks }: GroupMenuProps) {
|
||||
const [groupBlock, setGroupBlock] = useState<AsyncBlock | null>(null);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [position, setPosition] = useState<Point>(new Point(0, 0));
|
||||
const [dragOverGroup, setDragOverGroup] = useState<AsyncBlock | null>(null);
|
||||
const [direction, setDirection] = useState<GroupDirection>(
|
||||
GroupDirection.down
|
||||
);
|
||||
const menuRef = useRef<HTMLUListElement>(null);
|
||||
|
||||
const handleRootMouseMove = useCallback(
|
||||
async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const groupBlockNew =
|
||||
await editor.dragDropManager.getGroupBlockByPoint(
|
||||
new Point(e.clientX, e.clientY)
|
||||
);
|
||||
groupBlockNew && setGroupBlock(groupBlockNew || null);
|
||||
},
|
||||
[editor, setGroupBlock]
|
||||
);
|
||||
|
||||
const handleRootMouseDown = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (
|
||||
menuRef.current &&
|
||||
!menuRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setShowMenu(false);
|
||||
}
|
||||
},
|
||||
[setShowMenu]
|
||||
);
|
||||
|
||||
const handleRootDragOver = useCallback(
|
||||
async (e: React.DragEvent<Element>) => {
|
||||
let groupBlockOnDragOver = null;
|
||||
const mousePoint = new Point(e.clientX, e.clientY);
|
||||
if (editor.dragDropManager.isDragGroup(e)) {
|
||||
groupBlockOnDragOver =
|
||||
await editor.dragDropManager.getGroupBlockByPoint(
|
||||
mousePoint
|
||||
);
|
||||
if (groupBlockOnDragOver?.id === groupBlock?.id) {
|
||||
groupBlockOnDragOver = null;
|
||||
}
|
||||
}
|
||||
setDragOverGroup(groupBlockOnDragOver || null);
|
||||
const direction =
|
||||
await editor.dragDropManager.checkDragGroupDirection(
|
||||
groupBlock,
|
||||
groupBlockOnDragOver,
|
||||
mousePoint
|
||||
);
|
||||
setDirection(direction);
|
||||
},
|
||||
[editor, groupBlock]
|
||||
);
|
||||
|
||||
const handleRootDrop = useCallback(
|
||||
async (e: React.DragEvent<Element>) => {
|
||||
let groupBlockOnDrop = null;
|
||||
if (editor.dragDropManager.isDragGroup(e)) {
|
||||
groupBlockOnDrop =
|
||||
await editor.dragDropManager.getGroupBlockByPoint(
|
||||
new Point(e.clientX, e.clientY)
|
||||
);
|
||||
if (groupBlockOnDrop?.id === groupBlock?.id) {
|
||||
groupBlockOnDrop = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
[editor, groupBlock]
|
||||
);
|
||||
|
||||
const handleRootDragEnd = () => {
|
||||
setDragOverGroup(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
hooks.addHook(HookType.ON_ROOTNODE_MOUSE_MOVE, handleRootMouseMove);
|
||||
hooks.addHook(HookType.ON_ROOTNODE_MOUSE_DOWN, handleRootMouseDown);
|
||||
hooks.addHook(HookType.ON_ROOTNODE_DRAG_OVER, handleRootDragOver);
|
||||
hooks.addHook(HookType.ON_ROOTNODE_DRAG_END, handleRootDragEnd);
|
||||
return () => {
|
||||
hooks.removeHook(
|
||||
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,
|
||||
handleRootMouseMove,
|
||||
handleRootMouseDown,
|
||||
handleRootDragOver,
|
||||
handleRootDrop,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupBlock && groupBlock.dom) {
|
||||
if (editor.container) {
|
||||
setPosition(
|
||||
new Point(
|
||||
groupBlock.dom.offsetLeft - editor.container.offsetLeft,
|
||||
groupBlock.dom.offsetTop - editor.container.offsetTop
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [groupBlock, editor]);
|
||||
|
||||
const handleClick = () => {
|
||||
setShowMenu(!showMenu);
|
||||
};
|
||||
|
||||
const handleDragStart = async (e: React.DragEvent<HTMLDivElement>) => {
|
||||
const dragImage = await editor.blockHelper.getBlockDragImg(
|
||||
groupBlock.id
|
||||
);
|
||||
if (dragImage) {
|
||||
e.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||
editor.dragDropManager.setDragGroupInfo(e, groupBlock.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setShowMenu(false);
|
||||
}, [groupBlock]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{groupBlock ? (
|
||||
<Menu
|
||||
editor={editor}
|
||||
groupBlock={groupBlock}
|
||||
position={position}
|
||||
visible={showMenu}
|
||||
setVisible={setShowMenu}
|
||||
setGroupBlock={setGroupBlock}
|
||||
menuRef={menuRef}
|
||||
>
|
||||
<DragItem
|
||||
editor={editor}
|
||||
isShow={!!groupBlock}
|
||||
groupBlock={groupBlock}
|
||||
onClick={handleClick}
|
||||
onDragStart={handleDragStart}
|
||||
onMouseDown={handleMouseDown}
|
||||
draggable={true}
|
||||
/>
|
||||
</Menu>
|
||||
) : null}
|
||||
<Line
|
||||
groupBlock={dragOverGroup}
|
||||
editor={editor}
|
||||
direction={direction}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
GroupDirection,
|
||||
Virgo,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { Rect } from '@toeverything/utils';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type LineProps = {
|
||||
groupBlock: AsyncBlock | null;
|
||||
editor: Virgo;
|
||||
direction: GroupDirection;
|
||||
};
|
||||
|
||||
const LINE_WEIGHT = 2;
|
||||
|
||||
export const Line = function ({ direction, editor, groupBlock }: LineProps) {
|
||||
const [isShow, setIsShow] = useState<boolean>(false);
|
||||
const [rect, setRect] = useState<Rect | null>(null);
|
||||
useEffect(() => {
|
||||
if (groupBlock && groupBlock.dom && editor.container) {
|
||||
setRect(
|
||||
Rect.fromLWTH(
|
||||
groupBlock.dom.offsetLeft - editor.container.offsetLeft,
|
||||
groupBlock.dom.offsetWidth,
|
||||
groupBlock.dom.offsetTop - editor.container.offsetTop,
|
||||
groupBlock.dom.offsetHeight
|
||||
)
|
||||
);
|
||||
setIsShow(true);
|
||||
} else {
|
||||
setIsShow(false);
|
||||
}
|
||||
}, [groupBlock, editor.container]);
|
||||
|
||||
const computeLineStyle = (): React.CSSProperties => {
|
||||
if (!rect) {
|
||||
return {};
|
||||
}
|
||||
return direction === GroupDirection.down
|
||||
? {
|
||||
top: rect.bottom + LINE_WEIGHT,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
}
|
||||
: {
|
||||
top: rect.top - LINE_WEIGHT,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
};
|
||||
};
|
||||
|
||||
return isShow ? <LineDiv style={computeLineStyle()} /> : null;
|
||||
};
|
||||
|
||||
// TODO: use absolute position
|
||||
const LineDiv = styled('div')({
|
||||
zIndex: 2,
|
||||
position: 'absolute',
|
||||
background: '#502EC4',
|
||||
height: LINE_WEIGHT,
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { AsyncBlock, Virgo } from '@toeverything/components/editor-core';
|
||||
import { DeleteCashBinIcon } from '@toeverything/components/icons';
|
||||
import { Popover, styled } from '@toeverything/components/ui';
|
||||
import { Point } from '@toeverything/utils';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { ICON_WIDTH } from './DragItem';
|
||||
|
||||
type MenuProps = {
|
||||
visible: boolean;
|
||||
setVisible: (visible: boolean) => void;
|
||||
setGroupBlock: (groupBlock: AsyncBlock | null) => void;
|
||||
position: Point;
|
||||
editor: Virgo;
|
||||
groupBlock: AsyncBlock;
|
||||
menuRef: React.RefObject<HTMLUListElement>;
|
||||
};
|
||||
|
||||
export const Menu = ({
|
||||
children,
|
||||
position,
|
||||
groupBlock,
|
||||
setVisible,
|
||||
visible,
|
||||
setGroupBlock,
|
||||
menuRef,
|
||||
}: PropsWithChildren<MenuProps>) => {
|
||||
const handlerDeleteGroup = () => {
|
||||
groupBlock.remove();
|
||||
setVisible(false);
|
||||
setGroupBlock(null);
|
||||
};
|
||||
|
||||
const Content = () => {
|
||||
return (
|
||||
<MenuUl ref={menuRef}>
|
||||
<MenuItem onClick={handlerDeleteGroup}>
|
||||
<IconContainer>
|
||||
<DeleteCashBinIcon />
|
||||
</IconContainer>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MenuUl>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={<Content />}
|
||||
placement={'bottom-start'}
|
||||
visible={visible}
|
||||
anchorStyle={{
|
||||
position: 'absolute',
|
||||
top: position.y,
|
||||
left: position.x - ICON_WIDTH,
|
||||
height: ICON_WIDTH,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuUl = styled('ul')(({ theme }) => ({
|
||||
fontFamily: 'PingFang SC',
|
||||
background: '#FFF',
|
||||
color: '#4C6275',
|
||||
fontWeight: '400',
|
||||
}));
|
||||
|
||||
const MenuItem = styled('li')(({ theme }) => ({
|
||||
fontWeight: '400',
|
||||
width: '268px',
|
||||
height: '32px',
|
||||
lineHeight: '32px',
|
||||
display: 'flex',
|
||||
fontSize: '14px',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: '#F5F7F8',
|
||||
},
|
||||
}));
|
||||
|
||||
const IconContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
height: '32px',
|
||||
margin: '0 8px',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
lineHeight: '32px',
|
||||
fontSize: '20px',
|
||||
'&, & > svg': {
|
||||
width: '20px',
|
||||
},
|
||||
'& > svg': {
|
||||
height: '20px',
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,41 @@
|
||||
import { StrictMode } from 'react';
|
||||
|
||||
import { BasePlugin } from '../../base-plugin';
|
||||
import { PluginRenderRoot } from '../../utils';
|
||||
import { GroupMenu } from './GropuMenu';
|
||||
// import { CommandMenu } from './Menu';
|
||||
|
||||
const PLUGIN_NAME = 'group-menu';
|
||||
|
||||
export class GroupMenuPlugin extends BasePlugin {
|
||||
private root?: PluginRenderRoot;
|
||||
public static override get pluginName(): string {
|
||||
return PLUGIN_NAME;
|
||||
}
|
||||
|
||||
protected override on_render(): void {
|
||||
if (this.editor.isWhiteboard) return;
|
||||
const container = document.createElement('div');
|
||||
// TODO remove
|
||||
container.classList.add(`id-${PLUGIN_NAME}`);
|
||||
this.root = new PluginRenderRoot({
|
||||
name: PLUGIN_NAME,
|
||||
render: this.editor.reactRenderRoot.render,
|
||||
});
|
||||
this.root.mount();
|
||||
this._renderGroupMenu();
|
||||
}
|
||||
|
||||
public override dispose() {
|
||||
this.root?.unmount();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _renderGroupMenu(): void {
|
||||
this.root?.render(
|
||||
<StrictMode>
|
||||
<GroupMenu editor={this.editor} hooks={this.hooks} />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Plugin';
|
||||
Reference in New Issue
Block a user