From eb02e62a0e0b17e58104bfa5c3353837fc1e17d5 Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Wed, 24 Aug 2022 01:20:52 +0800 Subject: [PATCH] feat: new useBlockRender API --- .../editor-core/src/render-block/Context.tsx | 32 ++++ .../src/render-block/RenderBlock.tsx | 9 +- .../src/render-block/RenderBlockChildren.tsx | 35 +++- .../src/render-block/RenderKanbanBlock.tsx | 56 ++++++ .../src/render-block/WithTreeViewChildren.tsx | 181 ++++++++++++++++++ .../editor-core/src/render-block/index.ts | 2 + 6 files changed, 307 insertions(+), 8 deletions(-) create mode 100644 libs/components/editor-core/src/render-block/Context.tsx create mode 100644 libs/components/editor-core/src/render-block/RenderKanbanBlock.tsx create mode 100644 libs/components/editor-core/src/render-block/WithTreeViewChildren.tsx diff --git a/libs/components/editor-core/src/render-block/Context.tsx b/libs/components/editor-core/src/render-block/Context.tsx new file mode 100644 index 0000000000..8a1b68465c --- /dev/null +++ b/libs/components/editor-core/src/render-block/Context.tsx @@ -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( + 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) => { + return ( + + {children} + + ); +}; + +export const useBlockRender = () => { + const { blockRender } = useContext(BlockRenderContext); + return { + BlockRender: blockRender, + }; +}; diff --git a/libs/components/editor-core/src/render-block/RenderBlock.tsx b/libs/components/editor-core/src/render-block/RenderBlock.tsx index 0a1531a709..1dc945e870 100644 --- a/libs/components/editor-core/src/render-block/RenderBlock.tsx +++ b/libs/components/editor-core/src/render-block/RenderBlock.tsx @@ -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) { diff --git a/libs/components/editor-core/src/render-block/RenderBlockChildren.tsx b/libs/components/editor-core/src/render-block/RenderBlockChildren.tsx index 4c87ccc8e8..d79d0cd002 100644 --- a/libs/components/editor-core/src/render-block/RenderBlockChildren.tsx +++ b/libs/components/editor-core/src/render-block/RenderBlockChildren.tsx @@ -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 ? ( - <> + {block.childrenIds.map(childId => { - return ; + return ; })} - + ) : 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' }), + }) +); diff --git a/libs/components/editor-core/src/render-block/RenderKanbanBlock.tsx b/libs/components/editor-core/src/render-block/RenderKanbanBlock.tsx new file mode 100644 index 0000000000..54f9e2276e --- /dev/null +++ b/libs/components/editor-core/src/render-block/RenderKanbanBlock.tsx @@ -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 ( + + + + ); +}; + +/** + * Render a block, but only one level of children. + */ +const OneLevelBlockRender = ({ blockId }: RenderBlockProps) => { + return ( + + + + ); +}; + +export const KanbanBlockRender = ({ blockId }: RenderBlockProps) => { + const { block } = useBlock(blockId); + + if (!block) { + return ( + + + + ); + } + + return ( + + + {block?.childrenIds.map(childId => ( + + + + ))} + + ); +}; + +const StyledBorder = styled('div')({ + border: '1px solid #E0E6EB', + borderRadius: '5px', + margin: '4px', + padding: '0 4px', +}); diff --git a/libs/components/editor-core/src/render-block/WithTreeViewChildren.tsx b/libs/components/editor-core/src/render-block/WithTreeViewChildren.tsx new file mode 100644 index 0000000000..8c3642ef6d --- /dev/null +++ b/libs/components/editor-core/src/render-block/WithTreeViewChildren.tsx @@ -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 ( + + + + + {lastItem && } + + {/* maybe need a child wrapper */} + {children} + + ); +}); + +const CollapsedNode = forwardRef< + HTMLDivElement, + ComponentPropsWithoutRef<'div'> +>((props, ref) => { + return ( + + ยทยทยท + + ); +}); + +/** + * Indent rendering child nodes + */ +export const withTreeViewChildren = ( + creator: (props: CreateView) => ReactElement, + customConfig: Partial = {} +) => { + 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 && ( + + )} + {showChildren && + childrenIds.map((childId, idx) => { + return ( + + + + ); + })} + + ); + }; +}; + +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', +}); diff --git a/libs/components/editor-core/src/render-block/index.ts b/libs/components/editor-core/src/render-block/index.ts index 820bac8b57..0d5c809c4c 100644 --- a/libs/components/editor-core/src/render-block/index.ts +++ b/libs/components/editor-core/src/render-block/index.ts @@ -1,2 +1,4 @@ +export { BlockRenderProvider, useBlockRender } from './Context'; export * from './RenderBlock'; export * from './RenderBlockChildren'; +export { withTreeViewChildren } from './WithTreeViewChildren';