feat: new useBlockRender API

This commit is contained in:
lawvs 2022-08-24 01:20:52 +08:00
parent a6848dda51
commit eb02e62a0e
6 changed files with 307 additions and 8 deletions

View File

@ -0,0 +1,32 @@
import { genErrorObj } from '@toeverything/utils';
import { createContext, PropsWithChildren, useContext } from 'react';
import { RenderBlockProps } from './RenderBlock';
type BlockRenderProps = {
blockRender: (args: RenderBlockProps) => JSX.Element | null;
};
export const BlockRenderContext = createContext<BlockRenderProps>(
genErrorObj(
'Failed to get BlockChildrenContext! The context only can use under the "render-root"'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
);
export const BlockRenderProvider = ({
blockRender,
children,
}: PropsWithChildren<BlockRenderProps>) => {
return (
<BlockRenderContext.Provider value={{ blockRender }}>
{children}
</BlockRenderContext.Provider>
);
};
export const useBlockRender = () => {
const { blockRender } = useContext(BlockRenderContext);
return {
BlockRender: blockRender,
};
};

View File

@ -4,7 +4,12 @@ import { useCallback, useMemo } from 'react';
import { useEditor } from '../Contexts';
import { useBlock } from '../hooks';
interface RenderBlockProps {
/**
* Render nothing
*/
export const NullBlockRender = (): null => null;
export interface RenderBlockProps {
blockId: string;
hasContainer?: boolean;
}
@ -29,7 +34,7 @@ export function RenderBlock({
if (block?.type) {
return editor.getView(block.type).View;
}
return () => null;
return (): null => null;
}, [editor, block?.type]);
if (!block) {

View File

@ -1,16 +1,39 @@
import { styled } from '@toeverything/components/ui';
import type { AsyncBlock } from '../editor';
import { RenderBlock } from './RenderBlock';
import { useBlockRender } from './Context';
import { NullBlockRender } from './RenderBlock';
interface RenderChildrenProps {
export interface RenderChildrenProps {
block: AsyncBlock;
indent?: boolean;
}
export const RenderBlockChildren = ({ block }: RenderChildrenProps) => {
export const RenderBlockChildren = ({
block,
indent = true,
}: RenderChildrenProps) => {
const { BlockRender } = useBlockRender();
if (BlockRender === NullBlockRender) {
return null;
}
return block.childrenIds.length ? (
<>
<StyledIdentWrapper indent={indent}>
{block.childrenIds.map(childId => {
return <RenderBlock key={childId} blockId={childId} />;
return <BlockRender key={childId} blockId={childId} />;
})}
</>
</StyledIdentWrapper>
) : null;
};
/**
* Indent rendering child nodes
*/
const StyledIdentWrapper = styled('div')<{ indent?: boolean }>(
({ indent }) => ({
display: 'flex',
flexDirection: 'column',
// TODO: marginLeft should use theme provided by styled
...(indent && { marginLeft: '30px' }),
})
);

View File

@ -0,0 +1,56 @@
import { styled } from '@toeverything/components/ui';
import { useBlock } from '../hooks';
import { BlockRenderProvider } from './Context';
import { NullBlockRender, RenderBlock, RenderBlockProps } from './RenderBlock';
/**
* Render block without children.
*/
const BlockWithoutChildrenRender = ({ blockId }: RenderBlockProps) => {
return (
<BlockRenderProvider blockRender={NullBlockRender}>
<RenderBlock blockId={blockId} />
</BlockRenderProvider>
);
};
/**
* Render a block, but only one level of children.
*/
const OneLevelBlockRender = ({ blockId }: RenderBlockProps) => {
return (
<BlockRenderProvider blockRender={BlockWithoutChildrenRender}>
<RenderBlock blockId={blockId} />
</BlockRenderProvider>
);
};
export const KanbanBlockRender = ({ blockId }: RenderBlockProps) => {
const { block } = useBlock(blockId);
if (!block) {
return (
<BlockRenderProvider blockRender={NullBlockRender}>
<RenderBlock blockId={blockId} />
</BlockRenderProvider>
);
}
return (
<BlockRenderProvider blockRender={NullBlockRender}>
<RenderBlock blockId={blockId} />
{block?.childrenIds.map(childId => (
<StyledBorder key={childId}>
<RenderBlock blockId={childId} />
</StyledBorder>
))}
</BlockRenderProvider>
);
};
const StyledBorder = styled('div')({
border: '1px solid #E0E6EB',
borderRadius: '5px',
margin: '4px',
padding: '0 4px',
});

View File

@ -0,0 +1,181 @@
import { styled } from '@toeverything/components/ui';
import type {
ComponentPropsWithoutRef,
ComponentPropsWithRef,
CSSProperties,
ReactElement,
} from 'react';
import { forwardRef } from 'react';
import { CreateView } from '../editor';
import { useBlockRender } from './Context';
import { NullBlockRender } from './RenderBlock';
type WithChildrenConfig = {
indent: CSSProperties['marginLeft'];
};
const defaultConfig: WithChildrenConfig = {
indent: '30px',
};
const TreeView = forwardRef<
HTMLDivElement,
{ lastItem?: boolean } & ComponentPropsWithRef<'div'>
>(({ lastItem = false, children, onClick, ...restProps }, ref) => {
return (
<TreeWrapper ref={ref} {...restProps}>
<StyledTreeView>
<VerticalLine last={lastItem} onClick={onClick} />
<HorizontalLine last={lastItem} onClick={onClick} />
{lastItem && <LastItemRadius />}
</StyledTreeView>
{/* maybe need a child wrapper */}
{children}
</TreeWrapper>
);
});
const CollapsedNode = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<'div'>
>((props, ref) => {
return (
<TreeView ref={ref} lastItem={true} {...props}>
<Collapsed onClick={props.onClick}>···</Collapsed>
</TreeView>
);
});
/**
* Indent rendering child nodes
*/
export const withTreeViewChildren = (
creator: (props: CreateView) => ReactElement,
customConfig: Partial<WithChildrenConfig> = {}
) => {
const config = {
...defaultConfig,
...customConfig,
};
return (props: CreateView) => {
const { block } = props;
const { BlockRender } = useBlockRender();
const collapsed = block.getProperty('collapsed')?.value;
const childrenIds = block.childrenIds;
const showChildren =
!collapsed &&
childrenIds.length > 0 &&
BlockRender !== NullBlockRender;
const handleCollapse = () => {
block.setProperty('collapsed', { value: true });
};
const handleExpand = () => {
block.setProperty('collapsed', { value: false });
};
return (
<>
{creator(props)}
{collapsed && (
<CollapsedNode
onClick={handleExpand}
style={{ marginLeft: config.indent }}
/>
)}
{showChildren &&
childrenIds.map((childId, idx) => {
return (
<TreeView
key={childId}
lastItem={idx === childrenIds.length - 1}
onClick={handleCollapse}
style={{ marginLeft: config.indent }}
>
<BlockRender key={childId} blockId={childId} />
</TreeView>
);
})}
</>
);
};
};
const TREE_COLOR = '#D5DFE6';
// adjust left and right margins of the the tree line
const TREE_LINE_LEFT_OFFSET = '-16px';
// determine the position of the horizontal line by the type of the item
const TREE_LINE_TOP_OFFSET = '20px'; // '50%'
const TREE_LINE_WIDTH = '12px';
const TreeWrapper = styled('div')({
position: 'relative',
display: 'flex',
});
const StyledTreeView = styled('div')({
position: 'absolute',
left: TREE_LINE_LEFT_OFFSET,
height: '100%',
});
const Line = styled('div')({
position: 'absolute',
cursor: 'pointer',
backgroundColor: TREE_COLOR,
// somehow tldraw would override this
boxSizing: 'content-box!important' as any,
// 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',
});
const VerticalLine = styled(Line)<{ last: boolean }>(({ last }) => ({
width: '1px',
height: last ? TREE_LINE_TOP_OFFSET : '100%',
paddingTop: 0,
paddingBottom: 0,
transform: 'translate(-50%, 0)',
opacity: last ? 0 : 'unset',
}));
const HorizontalLine = styled(Line)<{ last: boolean }>(({ last }) => ({
width: TREE_LINE_WIDTH,
height: '1px',
paddingLeft: 0,
paddingRight: 0,
top: TREE_LINE_TOP_OFFSET,
transform: 'translate(0, -50%)',
opacity: last ? 0 : 'unset',
}));
const Collapsed = styled('div')({
cursor: 'pointer',
display: 'inline-block',
color: '#98ACBD',
padding: '8px',
});
const LastItemRadius = styled('div')({
boxSizing: 'content-box',
position: 'absolute',
left: '-0.5px',
top: 0,
height: TREE_LINE_TOP_OFFSET,
bottom: '50%',
width: TREE_LINE_WIDTH,
borderWidth: '1px',
borderStyle: 'solid',
borderLeftColor: TREE_COLOR,
borderBottomColor: TREE_COLOR,
borderTop: 'none',
borderRight: 'none',
borderRadius: '0 0 0 3px',
pointerEvents: 'none',
});

View File

@ -1,2 +1,4 @@
export { BlockRenderProvider, useBlockRender } from './Context';
export * from './RenderBlock';
export * from './RenderBlockChildren';
export { withTreeViewChildren } from './WithTreeViewChildren';