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,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;
}
}