mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 10:42:00 +03:00
feat: new useBlockRender API
This commit is contained in:
parent
a6848dda51
commit
eb02e62a0e
32
libs/components/editor-core/src/render-block/Context.tsx
Normal file
32
libs/components/editor-core/src/render-block/Context.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
@ -4,7 +4,12 @@ import { useCallback, useMemo } from 'react';
|
|||||||
import { useEditor } from '../Contexts';
|
import { useEditor } from '../Contexts';
|
||||||
import { useBlock } from '../hooks';
|
import { useBlock } from '../hooks';
|
||||||
|
|
||||||
interface RenderBlockProps {
|
/**
|
||||||
|
* Render nothing
|
||||||
|
*/
|
||||||
|
export const NullBlockRender = (): null => null;
|
||||||
|
|
||||||
|
export interface RenderBlockProps {
|
||||||
blockId: string;
|
blockId: string;
|
||||||
hasContainer?: boolean;
|
hasContainer?: boolean;
|
||||||
}
|
}
|
||||||
@ -29,7 +34,7 @@ export function RenderBlock({
|
|||||||
if (block?.type) {
|
if (block?.type) {
|
||||||
return editor.getView(block.type).View;
|
return editor.getView(block.type).View;
|
||||||
}
|
}
|
||||||
return () => null;
|
return (): null => null;
|
||||||
}, [editor, block?.type]);
|
}, [editor, block?.type]);
|
||||||
|
|
||||||
if (!block) {
|
if (!block) {
|
||||||
|
@ -1,16 +1,39 @@
|
|||||||
|
import { styled } from '@toeverything/components/ui';
|
||||||
import type { AsyncBlock } from '../editor';
|
import type { AsyncBlock } from '../editor';
|
||||||
import { RenderBlock } from './RenderBlock';
|
import { useBlockRender } from './Context';
|
||||||
|
import { NullBlockRender } from './RenderBlock';
|
||||||
|
|
||||||
interface RenderChildrenProps {
|
export interface RenderChildrenProps {
|
||||||
block: AsyncBlock;
|
block: AsyncBlock;
|
||||||
|
indent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RenderBlockChildren = ({
|
||||||
|
block,
|
||||||
|
indent = true,
|
||||||
|
}: RenderChildrenProps) => {
|
||||||
|
const { BlockRender } = useBlockRender();
|
||||||
|
if (BlockRender === NullBlockRender) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RenderBlockChildren = ({ block }: RenderChildrenProps) => {
|
|
||||||
return block.childrenIds.length ? (
|
return block.childrenIds.length ? (
|
||||||
<>
|
<StyledIdentWrapper indent={indent}>
|
||||||
{block.childrenIds.map(childId => {
|
{block.childrenIds.map(childId => {
|
||||||
return <RenderBlock key={childId} blockId={childId} />;
|
return <BlockRender key={childId} blockId={childId} />;
|
||||||
})}
|
})}
|
||||||
</>
|
</StyledIdentWrapper>
|
||||||
) : null;
|
) : 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' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
@ -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',
|
||||||
|
});
|
@ -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',
|
||||||
|
});
|
@ -1,2 +1,4 @@
|
|||||||
|
export { BlockRenderProvider, useBlockRender } from './Context';
|
||||||
export * from './RenderBlock';
|
export * from './RenderBlock';
|
||||||
export * from './RenderBlockChildren';
|
export * from './RenderBlockChildren';
|
||||||
|
export { withTreeViewChildren } from './WithTreeViewChildren';
|
||||||
|
Loading…
Reference in New Issue
Block a user