mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 02:26:21 +08:00
init: the first public commit for AFFiNE
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import type { FC } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { BlockEditor } from '@toeverything/framework/virgo';
|
||||
|
||||
type GridHandleProps = {
|
||||
editor: BlockEditor;
|
||||
onDrag?: (e: MouseEvent) => void;
|
||||
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
|
||||
blockId: string;
|
||||
enabledAddItem: boolean;
|
||||
draggable: boolean;
|
||||
};
|
||||
|
||||
export const GridHandle: FC<GridHandleProps> = function ({
|
||||
blockId,
|
||||
editor,
|
||||
enabledAddItem,
|
||||
onDrag,
|
||||
onMouseDown,
|
||||
draggable,
|
||||
}) {
|
||||
const [isMouseDown, setIsMouseDown] = useState<boolean>(false);
|
||||
const handleMouseDown: React.MouseEventHandler<HTMLDivElement> = e => {
|
||||
if (draggable) {
|
||||
const cb = (e: MouseEvent) => {
|
||||
onDrag && onDrag(e);
|
||||
};
|
||||
onMouseDown && onMouseDown(e);
|
||||
setIsMouseDown(true);
|
||||
editor.mouseManager.onMouseMove(cb);
|
||||
editor.mouseManager.onMouseupEventOnce(() => {
|
||||
setIsMouseDown(false);
|
||||
editor.mouseManager.offMouseMove(cb);
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleCreateGridItem = async () => {
|
||||
const [, textBlock] =
|
||||
await editor.commands.blockCommands.createGridItem(blockId);
|
||||
if (textBlock) {
|
||||
editor.selectionManager.setActivatedNodeId(textBlock.id);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<GridHandleContainer
|
||||
style={
|
||||
isMouseDown
|
||||
? {
|
||||
backgroundColor: '#3E6FDB',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
{enabledAddItem ? (
|
||||
<AddGridHandle
|
||||
onClick={handleCreateGridItem}
|
||||
className="grid-add-handle"
|
||||
>
|
||||
+
|
||||
</AddGridHandle>
|
||||
) : null}
|
||||
</GridHandleContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const GridHandleContainer = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
width: '10px',
|
||||
flexGrow: '0',
|
||||
flexShrink: '0',
|
||||
padding: '5px 4px',
|
||||
minHeight: '15px',
|
||||
cursor: 'col-resize',
|
||||
borderRadius: '1px',
|
||||
backgroundClip: 'content-box',
|
||||
' &:hover': {
|
||||
backgroundColor: theme.affine.palette.primary,
|
||||
'.grid-add-handle': {
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const AddGridHandle = styled('div')(({ theme }) => ({
|
||||
display: 'none',
|
||||
position: 'absolute',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
height: '6px',
|
||||
width: '6px',
|
||||
borderRadius: '6px',
|
||||
top: '-6px',
|
||||
left: '2px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.affine.palette.menuSeparator,
|
||||
color: theme.affine.palette.menuSeparator,
|
||||
overflow: 'hidden',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: '18px',
|
||||
textAlign: 'center',
|
||||
' &:hover': {
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: theme.affine.palette.primary,
|
||||
color: theme.affine.palette.white,
|
||||
top: '-20px',
|
||||
left: '-5px',
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,242 @@
|
||||
import { RenderBlock } from '@toeverything/components/editor-core';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||
import { GridHandle } from './GirdHandle';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
GRID_ITEM_CLASS_NAME,
|
||||
GRID_ITEM_CONTENT_CLASS_NAME,
|
||||
} from '../grid-item/GridItem';
|
||||
import { debounce, domToRect, Point } from '@toeverything/utils';
|
||||
import clsx from 'clsx';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
|
||||
const MAX_ITEM_COUNT = 6;
|
||||
const DB_UPDATE_DELAY = 50;
|
||||
const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag';
|
||||
export const GRID_ITEM_MIN_WIDTH = 100 / MAX_ITEM_COUNT;
|
||||
export const GRID_PROPERTY_KEY = 'gridItemWidth';
|
||||
|
||||
export function removePercent(str: string) {
|
||||
return str.replace('%', '');
|
||||
}
|
||||
|
||||
export const Grid: FC<CreateView> = function (props) {
|
||||
const { block, editor } = props;
|
||||
const [isOnDrag, setIsOnDrag] = useState<boolean>(false);
|
||||
const isSetMouseUp = useRef<boolean>(false);
|
||||
const GridContainerRef = useRef<HTMLDivElement>();
|
||||
const mouseStartPoint = useRef<Point>();
|
||||
const gridItemCountRef = useRef<number>();
|
||||
const originalLeftWidth = useRef<number>(GRID_ITEM_MIN_WIDTH);
|
||||
const originalRightWidth = useRef<number>(GRID_ITEM_MIN_WIDTH);
|
||||
|
||||
const getLeftRightGridItemDomByIndex = (index: number) => {
|
||||
const gridItems = Array.from(GridContainerRef.current?.children).filter(
|
||||
child => {
|
||||
return child.classList.contains(GRID_ITEM_CLASS_NAME);
|
||||
}
|
||||
) as Array<HTMLDivElement>;
|
||||
const leftGrid = gridItems[index];
|
||||
const rightGrid = gridItems[index + 1];
|
||||
return { leftGrid, rightGrid };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
mayBeRefreshGridItemWidth();
|
||||
}, [block.childrenIds]);
|
||||
|
||||
const mayBeRefreshGridItemWidth = async () => {
|
||||
const gridItems = (await block.children()).filter(
|
||||
child => child.type === Protocol.Block.Type.gridItem
|
||||
);
|
||||
if (gridItems.length < gridItemCountRef.current) {
|
||||
let totalWidth = 0;
|
||||
const widthList = [];
|
||||
for (const gridItem of gridItems) {
|
||||
const itemWidth = Number(
|
||||
removePercent(gridItem.getProperty(GRID_PROPERTY_KEY))
|
||||
);
|
||||
totalWidth += itemWidth;
|
||||
widthList.push(itemWidth);
|
||||
}
|
||||
if (totalWidth < 100) {
|
||||
const plus = (100 - totalWidth) / gridItems.length;
|
||||
let totalNewWidth = 0;
|
||||
let index = 0;
|
||||
for (const gridItem of gridItems) {
|
||||
const newWidth = widthList.pop() + plus;
|
||||
let newWidthStr = `${newWidth}%`;
|
||||
if (index === gridItems.length - 1) {
|
||||
newWidthStr = `${100 - totalNewWidth}%`;
|
||||
}
|
||||
if (gridItem.dom) {
|
||||
setItemWidth(
|
||||
gridItem.dom.parentElement as HTMLDivElement,
|
||||
newWidthStr
|
||||
);
|
||||
}
|
||||
await gridItem.setProperty(GRID_PROPERTY_KEY, newWidthStr);
|
||||
totalNewWidth += newWidth;
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
gridItemCountRef.current = gridItems.length;
|
||||
};
|
||||
|
||||
const handleMouseDown = (
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
index: number
|
||||
) => {
|
||||
mouseStartPoint.current = new Point(e.clientX, e.clientY);
|
||||
const { leftGrid, rightGrid } = getLeftRightGridItemDomByIndex(index);
|
||||
originalLeftWidth.current = domToRect(leftGrid).width;
|
||||
originalRightWidth.current = domToRect(rightGrid).width;
|
||||
// disable the default behavior of the drag event (selection about)
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const updateDbWidth = debounce(
|
||||
async (
|
||||
leftBlockId: string,
|
||||
leftWidth: string,
|
||||
rightBlockId: string,
|
||||
rightWidth: string
|
||||
) => {
|
||||
const leftBlock = await editor.getBlockById(leftBlockId);
|
||||
const rightBlock = await editor.getBlockById(rightBlockId);
|
||||
leftBlock?.setProperty(GRID_PROPERTY_KEY, leftWidth);
|
||||
rightBlock?.setProperty(GRID_PROPERTY_KEY, rightWidth);
|
||||
},
|
||||
DB_UPDATE_DELAY
|
||||
);
|
||||
|
||||
const setItemWidth = (itemDom: HTMLDivElement, width: string) => {
|
||||
itemDom.style.width = width;
|
||||
};
|
||||
|
||||
const handleDragGrid = (e: MouseEvent, index: number) => {
|
||||
setIsOnDrag(true);
|
||||
window.getSelection().removeAllRanges();
|
||||
if (!isSetMouseUp.current) {
|
||||
isSetMouseUp.current = true;
|
||||
editor.mouseManager.onMouseupEventOnce(() => {
|
||||
setIsOnDrag(false);
|
||||
isSetMouseUp.current = false;
|
||||
originalLeftWidth.current = GRID_ITEM_MIN_WIDTH;
|
||||
originalRightWidth.current = GRID_ITEM_MIN_WIDTH;
|
||||
mouseStartPoint.current = null;
|
||||
});
|
||||
} else {
|
||||
const { leftGrid, rightGrid } =
|
||||
getLeftRightGridItemDomByIndex(index);
|
||||
const leftBlockId = block.childrenIds[index];
|
||||
const rightBlockId = block.childrenIds[index + 1];
|
||||
if (
|
||||
leftGrid &&
|
||||
rightGrid &&
|
||||
mouseStartPoint.current &&
|
||||
GridContainerRef.current
|
||||
) {
|
||||
const currentMousePoint = new Point(e.clientX, e.clientY);
|
||||
const totalWidth =
|
||||
Number(removePercent(leftGrid.style.width)) +
|
||||
Number(removePercent(rightGrid.style.width));
|
||||
const containerWidth = domToRect(
|
||||
GridContainerRef.current
|
||||
).width;
|
||||
const xDistance =
|
||||
mouseStartPoint.current.xDistance(currentMousePoint);
|
||||
const newLeftWidth = originalLeftWidth.current - xDistance;
|
||||
let newLeftPercent = (newLeftWidth / containerWidth) * 100;
|
||||
let newRightPercent = Number(totalWidth) - newLeftPercent;
|
||||
if (newLeftPercent < GRID_ITEM_MIN_WIDTH) {
|
||||
newLeftPercent = GRID_ITEM_MIN_WIDTH;
|
||||
newRightPercent = totalWidth - GRID_ITEM_MIN_WIDTH;
|
||||
} else if (newRightPercent < GRID_ITEM_MIN_WIDTH) {
|
||||
newRightPercent = GRID_ITEM_MIN_WIDTH;
|
||||
newLeftPercent = totalWidth - GRID_ITEM_MIN_WIDTH;
|
||||
}
|
||||
//XXX first change dom style is for animation speed, maybe not a good idea
|
||||
const newLeft = `${newLeftPercent}%`;
|
||||
const newRight = `${newRightPercent}%`;
|
||||
setItemWidth(leftGrid, newLeft);
|
||||
setItemWidth(rightGrid, newRight);
|
||||
updateDbWidth(leftBlockId, newLeft, rightBlockId, newRight);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const children = (
|
||||
<>
|
||||
{block.childrenIds.map((id, i) => {
|
||||
return (
|
||||
<GridItem
|
||||
style={{
|
||||
transition: isOnDrag
|
||||
? 'none'
|
||||
: 'width 0.2s ease-in-out',
|
||||
}}
|
||||
key={id}
|
||||
className={GRID_ITEM_CLASS_NAME}
|
||||
>
|
||||
<RenderBlock hasContainer={false} blockId={id} />
|
||||
<GridHandle
|
||||
onDrag={event => handleDragGrid(event, i)}
|
||||
editor={editor}
|
||||
onMouseDown={event => handleMouseDown(event, i)}
|
||||
blockId={id}
|
||||
enabledAddItem={
|
||||
block.childrenIds.length < MAX_ITEM_COUNT
|
||||
}
|
||||
draggable={i !== block.childrenIds.length - 1}
|
||||
/>
|
||||
</GridItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GridContainer
|
||||
className={clsx({ [GRID_ON_DRAG_CLASS]: isOnDrag })}
|
||||
ref={GridContainerRef}
|
||||
>
|
||||
{children}
|
||||
</GridContainer>
|
||||
{isOnDrag
|
||||
? ReactDOM.createPortal(<GridMask />, window.document.body)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const GridContainer = styled('div')({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
minWidth: `${GRID_ITEM_MIN_WIDTH}%`,
|
||||
[`&:hover .${GRID_ITEM_CONTENT_CLASS_NAME}`]: {
|
||||
backgroundColor: 'rgba(100, 106, 115, 0.05)',
|
||||
},
|
||||
});
|
||||
|
||||
const GridMask = styled('div')({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 9999,
|
||||
cursor: 'col-resize',
|
||||
pointerEvents: 'all',
|
||||
});
|
||||
|
||||
const GridItem = styled('div')({
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { FC } from 'react';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
|
||||
export function GridRender(creator: FC<CreateView>) {
|
||||
return function GridWithItem(props: CreateView) {
|
||||
const { editor, block } = props;
|
||||
return (
|
||||
<BlockContainer editor={editor} block={block} selected={false}>
|
||||
{creator({ ...props })}
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Grid } from './Grid';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, BaseView } from '@toeverything/framework/virgo';
|
||||
import { GridItem } from '../grid-item/GridItem';
|
||||
import { GridRender } from './GridRender';
|
||||
export { GRID_ITEM_MIN_WIDTH, GRID_PROPERTY_KEY, removePercent } from './Grid';
|
||||
|
||||
export class GridBlock extends BaseView {
|
||||
public override selectable = false;
|
||||
public override activatable = false;
|
||||
public override allowPendant = false;
|
||||
|
||||
type = Protocol.Block.Type.grid;
|
||||
View = GridRender(Grid);
|
||||
|
||||
override ChildrenView = GridItem;
|
||||
|
||||
override async onCreate(block: AsyncBlock): Promise<AsyncBlock> {
|
||||
return block;
|
||||
}
|
||||
|
||||
override async onDeleteChild(block: AsyncBlock): Promise<boolean> {
|
||||
if (block.childrenIds.length === 1) {
|
||||
// the children of grid will always be a grid item
|
||||
const firstChildren = await block.childAt(0);
|
||||
const itemChildren = await firstChildren.children();
|
||||
const beforeBlock = await block.previousSibling();
|
||||
if (beforeBlock) {
|
||||
await beforeBlock.after(...itemChildren);
|
||||
} else {
|
||||
const parent = await block.parent();
|
||||
if (parent) {
|
||||
await parent.prepend(...itemChildren);
|
||||
}
|
||||
}
|
||||
return block.remove();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user