mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-31 13:49:12 +08:00
init: the first public commit for AFFiNE
This commit is contained in:
@@ -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];
|
||||
}
|
||||
Reference in New Issue
Block a user