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,267 @@
import {
RenderBlock,
useOnSelect,
useRecastBlockScene,
WrapperWithPendantAndDragDrop,
} from '@toeverything/components/editor-core';
import { CreateView } from '@toeverything/framework/virgo';
import type {
ComponentPropsWithoutRef,
ComponentPropsWithRef,
CSSProperties,
ReactElement,
} from 'react';
import { forwardRef, useState } from 'react';
import style9 from 'style9';
import { BlockContainer } from '../components/BlockContainer';
import { styled } from '@toeverything/components/ui';
import { SCENE_CONFIG } from '../blocks/group/config';
type WithChildrenConfig = {
indent: CSSProperties['marginLeft'];
};
const defaultConfig: WithChildrenConfig = {
indent: '30px',
};
const TreeView = forwardRef<
HTMLDivElement,
{ lastItem?: boolean } & ComponentPropsWithRef<'div'>
>(({ lastItem, children, onClick, ...restProps }, ref) => {
return (
<div ref={ref} className={treeStyles('treeWrapper')} {...restProps}>
<div className={treeStyles('treeView')}>
<div
className={treeStyles({
line: true,
verticalLine: true,
lastItemVerticalLine: lastItem,
})}
onClick={onClick}
/>
<div
className={treeStyles({
line: true,
horizontalLine: true,
lastItemHorizontalLine: lastItem,
})}
onClick={onClick}
/>
{lastItem && <div className={treeStyles('lastItemRadius')} />}
</div>
{/* maybe need a child wrapper */}
{children}
</div>
);
});
interface ChildrenViewProp {
childrenIds: string[];
handleCollapse: () => void;
indent?: string | number;
}
const ChildrenView = ({
childrenIds,
handleCollapse,
indent,
}: ChildrenViewProp) => {
const { scene } = useRecastBlockScene();
const isKanbanScene = scene === SCENE_CONFIG.KANBAN;
return (
<div
className={styles('children')}
style={{ ...(!isKanbanScene && { marginLeft: indent }) }}
>
{childrenIds.map((childId, idx) => {
if (isKanbanScene) {
return (
<StyledBorder>
<RenderBlock key={childId} blockId={childId} />
</StyledBorder>
);
}
return (
<TreeView
key={childId}
lastItem={idx === childrenIds.length - 1}
onClick={handleCollapse}
>
<RenderBlock key={childId} blockId={childId} />
</TreeView>
);
})}
</div>
);
};
const CollapsedNode = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<'div'>
>((props, ref) => {
return (
<TreeView ref={ref} lastItem={true} {...props}>
<div className={treeStyles('collapsed')} onClick={props.onClick}>
···
</div>
</TreeView>
);
});
/**
* Indent rendering child nodes
*/
export const withTreeViewChildren = (
creator: (props: CreateView) => ReactElement,
customConfig: Partial<WithChildrenConfig> = {}
) => {
const config = {
...defaultConfig,
...customConfig,
};
return (props: CreateView) => {
const { block, editor } = props;
const collapsed = block.getProperty('collapsed')?.value;
const childrenIds = block.childrenIds;
const showChildren = !collapsed && childrenIds.length > 0;
const [isSelect, setIsSelect] = useState<boolean>();
useOnSelect(block.id, (is_select: boolean) => {
setIsSelect(is_select);
});
const handleCollapse = () => {
block.setProperty('collapsed', { value: true });
};
const handleExpand = () => {
block.setProperty('collapsed', { value: false });
};
return (
<BlockContainer
editor={props.editor}
block={block}
selected={isSelect}
className={styles('wrapper')}
>
<WrapperWithPendantAndDragDrop editor={editor} block={block}>
<div className={styles('node')}>{creator(props)}</div>
</WrapperWithPendantAndDragDrop>
{collapsed && (
<CollapsedNode
onClick={handleExpand}
style={{ marginLeft: config.indent }}
/>
)}
{showChildren && (
<ChildrenView
childrenIds={childrenIds}
handleCollapse={handleCollapse}
indent={config.indent}
/>
)}
</BlockContainer>
);
};
};
const styles = style9.create({
wrapper: {
display: 'flex',
flexDirection: 'column',
},
node: {},
children: {
display: 'flex',
flexDirection: 'column',
},
});
const treeColor = '#D5DFE6';
// TODO determine the position of the horizontal line by the type of the item
const itemPointHeight = '12.5px'; // '50%'
const treeStyles = style9.create({
treeWrapper: {
position: 'relative',
},
treeView: {
position: 'absolute',
left: '-21px',
height: '100%',
},
line: {
position: 'absolute',
cursor: 'pointer',
backgroundColor: treeColor,
boxSizing: 'content-box',
// See [Can I add background color only for padding?](https://stackoverflow.com/questions/14628601/can-i-add-background-color-only-for-padding)
backgroundClip: 'content-box',
backgroundOrigin: 'content-box',
// Increase click hot spot
padding: '10px',
},
verticalLine: {
width: '1px',
height: '100%',
paddingTop: 0,
paddingBottom: 0,
transform: 'translate(-50%, 0)',
},
horizontalLine: {
width: '16px',
height: '1px',
paddingLeft: 0,
paddingRight: 0,
top: itemPointHeight,
transform: 'translate(0, -50%)',
},
noItemHorizontalLine: {
display: 'none',
},
lastItemHorizontalLine: {
opacity: 0,
},
lastItemVerticalLine: {
height: itemPointHeight,
opacity: 0,
},
lastItemRadius: {
boxSizing: 'content-box',
position: 'absolute',
left: '-0.5px',
top: 0,
height: itemPointHeight,
bottom: '50%',
width: '16px',
borderWidth: '1px',
borderStyle: 'solid',
borderLeftColor: treeColor,
borderBottomColor: treeColor,
borderTop: 'none',
borderRight: 'none',
borderRadius: '0 0 0 3px',
pointerEvents: 'none',
},
collapsed: {
cursor: 'pointer',
display: 'inline-block',
color: '#B9CAD5',
},
});
const StyledBorder = styled('div')({
border: '1px solid #E0E6EB',
borderRadius: '5px',
margin: '4px',
});
@@ -0,0 +1,188 @@
import { supportChildren } from '@toeverything/components/editor-core';
import { Protocol } from '@toeverything/datasource/db-service';
import { AsyncBlock } from '@toeverything/framework/virgo';
import type { TodoAsyncBlock } from '../blocks/todo/types';
/**
* Is the block in top level
*/
export const isTopLevelBlock = (parentBlock: AsyncBlock): boolean => {
return (
parentBlock.type === Protocol.Block.Type.group ||
parentBlock.type === Protocol.Block.Type.page
);
};
/**
* @returns true if indent is success
* @example
* ```
* [ ]
* └─ [ ]
* [x] <- tab
* └─ [ ]
*
* ↓
*
* [ ]
* ├─ [ ]
* ├─ [x] <-
* └─ [ ]
* ```
*/
const indentBlock = async (block: TodoAsyncBlock) => {
// Move down
const previousBlock = await block.previousSibling();
if (!previousBlock || !supportChildren(previousBlock)) {
// Bottom, can not indent, do nothing
return false;
}
// Indent current node, but preserve child node hierarchy
const previousTodo = previousBlock as TodoAsyncBlock;
await previousTodo.setProperties({
collapsed: { value: false },
});
// 1. save target block children
const children = await block.children();
// 2. remove current block and children
await block.remove();
await block.removeChildren();
// 3. append target block and children to previous node
await previousTodo.append(block, ...children);
return true;
};
/**
* @returns true if dedent is success
* @example
* ```
* [ ]
* ├─ [ ]
* ├─ [x] <- shift + tab
* └─ [ ]
*
* ↓
*
* [ ]
* └─ [ ]
* [x] <-
* └─ [ ]
* ```
*/
const dedentBlock = async (block: AsyncBlock) => {
// Move up
let parentBlock = await block.parent();
if (!parentBlock) {
throw new Error('Failed to dedent block! Parent block not found!');
}
if (isTopLevelBlock(parentBlock)) {
// Top, do nothing
return false;
}
// 1. save child blocks of the parent block
const previousSiblings = await block.previousSiblings();
const nextSiblings = await block.nextSiblings();
// const children = await parentBlock.children();
// 2. remove all child blocks after the target block from the parent block
await parentBlock.removeChildren();
// TODO fix block sync with db-service
// Need update block from service now
parentBlock = await block.parent();
if (!parentBlock) {
throw new Error('Failed to dedent block! Parent block not found!');
}
await parentBlock.append(...previousSiblings);
parentBlock = await block.parent();
if (!parentBlock) {
throw new Error('Failed to dedent block! Parent block not found!');
}
// 3. remove current block
await block.remove();
// 4. append parent children to target block
await block.append(...nextSiblings);
// 5. append block to parent
await parentBlock.after(block);
return true;
};
export const tabBlock = async (block: AsyncBlock, isShiftKey: boolean) => {
if (isShiftKey) {
return await dedentBlock(block);
} else {
return await indentBlock(block);
}
};
const type2Map: Record<number, string> = {};
const type3Map: Record<number, string> = {};
export function getNumber(type: string, index: number) {
if (type === 'type2') {
if (type2Map[index]) {
return type2Map[index];
}
type2Map[index] = getType2(index - 1);
return type2Map[index];
}
if (type === 'type3') {
if (type3Map[index]) {
return type3Map[index];
}
type3Map[index] = getType3(index);
return type3Map[index];
}
return index;
}
const getType2 = (n: number) => {
const ordA = 'a'.charCodeAt(0);
const ordZ = 'z'.charCodeAt(0);
const len = ordZ - ordA + 1;
let s = '';
while (n >= 0) {
s = String.fromCharCode((n % len) + ordA) + s;
n = Math.floor(n / len) - 1;
}
return s;
};
const getType3 = (num: number) => {
const lookup = {
m: 1000,
cm: 900,
d: 500,
cd: 400,
c: 100,
xc: 90,
l: 50,
xl: 40,
x: 10,
ix: 9,
v: 5,
iv: 4,
i: 1,
};
let romanStr = '';
for (const i in lookup) {
while (num >= lookup[i as keyof typeof lookup]) {
romanStr += i;
num -= lookup[i as keyof typeof lookup];
}
}
return romanStr;
};
export function getChildrenType(type: string) {
const typeMap: Record<string, string> = {
type1: 'type2',
type2: 'type3',
type3: 'type1',
};
return typeMap[type];
}