mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 09:06:19 +08:00
Merge remote-tracking branch 'origin/develop' into fix/experience
This commit is contained in:
@@ -1,22 +1,34 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BlockEditor, AsyncBlock } from './editor';
|
||||
import { genErrorObj } from '@toeverything/utils';
|
||||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
import type { AsyncBlock, BlockEditor } from './editor';
|
||||
|
||||
const RootContext = createContext<{
|
||||
type EditorProps = {
|
||||
editor: BlockEditor;
|
||||
// TODO: Temporary fix, dependencies in the new architecture are bottom-up, editors do not need to be passed down from the top
|
||||
editorElement: () => JSX.Element;
|
||||
}>(
|
||||
};
|
||||
|
||||
const EditorContext = createContext<EditorProps>(
|
||||
genErrorObj(
|
||||
'Failed to get context! The context only can use under the "render-root"'
|
||||
'Failed to get EditorContext! The context only can use under the "render-root"'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as any
|
||||
);
|
||||
|
||||
export const EditorProvider = RootContext.Provider;
|
||||
|
||||
export const useEditor = () => {
|
||||
return useContext(RootContext);
|
||||
return useContext(EditorContext);
|
||||
};
|
||||
|
||||
export const EditorProvider = ({
|
||||
editor,
|
||||
editorElement,
|
||||
children,
|
||||
}: PropsWithChildren<EditorProps>) => {
|
||||
return (
|
||||
<EditorContext.Provider value={{ editor, editorElement }}>
|
||||
{children}
|
||||
</EditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
services,
|
||||
type ReturnUnobserve,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { EditorProvider } from './Contexts';
|
||||
import type { BlockEditor } from './editor';
|
||||
import { useIsOnDrag } from './hooks';
|
||||
import { addNewGroup, appendNewGroup } from './recast-block';
|
||||
import { BlockRenderProvider, RenderBlock } from './render-block';
|
||||
import { SelectionRect, SelectionRef } from './Selection';
|
||||
|
||||
interface RenderRootProps {
|
||||
@@ -24,11 +24,7 @@ interface RenderRootProps {
|
||||
const MAX_PAGE_WIDTH = 5000;
|
||||
export const MIN_PAGE_WIDTH = 1480;
|
||||
|
||||
export const RenderRoot = ({
|
||||
editor,
|
||||
editorElement,
|
||||
children,
|
||||
}: PropsWithChildren<RenderRootProps>) => {
|
||||
export const RenderRoot = ({ editor, editorElement }: RenderRootProps) => {
|
||||
const selectionRef = useRef<SelectionRef>(null);
|
||||
const triggeredBySelect = useRef(false);
|
||||
const [pageWidth, setPageWidth] = useState<number>(MIN_PAGE_WIDTH);
|
||||
@@ -158,39 +154,43 @@ export const RenderRoot = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorProvider value={{ editor, editorElement }}>
|
||||
<Container
|
||||
isEdgeless={editor.isEdgeless}
|
||||
ref={ref => {
|
||||
if (ref != null && ref !== editor.container) {
|
||||
editor.container = ref;
|
||||
editor.getHooks().render();
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOverCapture={onDragOverCapture}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
<Content style={{ maxWidth: pageWidth + 'px' }}>
|
||||
{children}
|
||||
</Content>
|
||||
{/** TODO: remove selectionManager insert */}
|
||||
{editor && <SelectionRect ref={selectionRef} editor={editor} />}
|
||||
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
<EditorProvider editor={editor} editorElement={editorElement}>
|
||||
<BlockRenderProvider blockRender={RenderBlock}>
|
||||
<Container
|
||||
isEdgeless={editor.isEdgeless}
|
||||
ref={ref => {
|
||||
if (ref != null && ref !== editor.container) {
|
||||
editor.container = ref;
|
||||
editor.getHooks().render();
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOverCapture={onDragOverCapture}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
<Content style={{ maxWidth: pageWidth + 'px' }}>
|
||||
<RenderBlock blockId={editor.getRootBlockId()} />
|
||||
</Content>
|
||||
{/** TODO: remove selectionManager insert */}
|
||||
{editor && (
|
||||
<SelectionRect ref={selectionRef} editor={editor} />
|
||||
)}
|
||||
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
</BlockRenderProvider>
|
||||
</EditorProvider>
|
||||
);
|
||||
};
|
||||
@@ -251,7 +251,7 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollBlankContainter
|
||||
<ScrollBlankContainer
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseMove={onMouseMove}
|
||||
onClick={onClick}
|
||||
@@ -283,7 +283,7 @@ const Content = styled('div')({
|
||||
transitionTimingFunction: 'ease-in',
|
||||
});
|
||||
|
||||
const ScrollBlankContainter = styled('div')({
|
||||
const ScrollBlankContainer = styled('div')({
|
||||
paddingBottom: '30vh',
|
||||
margin: `0 -${PADDING_X}px`,
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
/* eslint-disable max-lines */
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
ReturnEditorBlock,
|
||||
UpdateEditorBlock,
|
||||
DefaultColumnsValue,
|
||||
Protocol,
|
||||
ReturnEditorBlock,
|
||||
UpdateEditorBlock,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
isDev,
|
||||
createNoopWithMessage,
|
||||
lowerFirst,
|
||||
isDev,
|
||||
last,
|
||||
lowerFirst,
|
||||
} from '@toeverything/utils';
|
||||
import { BlockProvider } from './block-provider';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { BaseView, BaseView as BlockView } from './../views/base-view';
|
||||
import { BlockProvider } from './block-provider';
|
||||
|
||||
type EventType = 'update';
|
||||
export interface EventData {
|
||||
@@ -154,6 +154,7 @@ export class AsyncBlock {
|
||||
}
|
||||
this.initialized = true;
|
||||
this.raw_data = await this.filterPageInvalidChildren(this.raw_data);
|
||||
this.raw_data = await this.updateDoubleLinkBlock(this.raw_data);
|
||||
const { workspace, id } = this.raw_data;
|
||||
this.unobserve = await this.services.observe(
|
||||
{ workspace, id },
|
||||
@@ -161,6 +162,7 @@ export class AsyncBlock {
|
||||
const oldData = this.raw_data;
|
||||
this.raw_data = blockData;
|
||||
this.raw_data = await this.filterPageInvalidChildren(blockData);
|
||||
this.raw_data = await this.updateDoubleLinkBlock(this.raw_data);
|
||||
this.emit('update', { block: this, oldData });
|
||||
}
|
||||
);
|
||||
@@ -495,4 +497,33 @@ export class AsyncBlock {
|
||||
getBoundingClientRect() {
|
||||
return this.dom?.getBoundingClientRect();
|
||||
}
|
||||
|
||||
async updateDoubleLinkBlock(rawData: ReturnEditorBlock) {
|
||||
const values = rawData.properties?.text?.value || [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const item = values[i] as any;
|
||||
if (item.linkType === 'doubleLink') {
|
||||
const linkBlock = await this.services.load({
|
||||
workspace: item.workspaceId,
|
||||
id: item.blockId,
|
||||
});
|
||||
|
||||
let children = linkBlock?.getProperties().text?.value || [];
|
||||
if (children.length === 1 && !children[0].text) {
|
||||
children = [{ text: 'Untitled' }];
|
||||
}
|
||||
if (
|
||||
children.map(v => v.text).join('') !==
|
||||
(item.children || []).map((v: any) => v.text).join('')
|
||||
) {
|
||||
const newItem = {
|
||||
...item,
|
||||
children: children,
|
||||
};
|
||||
values.splice(i, 1, newItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rawData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ type TextUtilsFunctions =
|
||||
| 'setSearchSlash'
|
||||
| 'removeSearchSlash'
|
||||
| 'getSearchSlashText'
|
||||
| 'setDoubleLinkSearchSlash'
|
||||
| 'getDoubleLinkSearchSlashText'
|
||||
| 'setSelectDoubleLinkSearchSlash'
|
||||
| 'removeDoubleLinkSearchSlash'
|
||||
| 'insertDoubleLink'
|
||||
| 'selectionToSlateRange'
|
||||
| 'transformPoint'
|
||||
| 'toggleTextFormatBySelection'
|
||||
@@ -37,7 +42,6 @@ type TextUtilsFunctions =
|
||||
| 'getCommentsIdsBySelection'
|
||||
| 'getCurrentSelection'
|
||||
| 'removeSelection'
|
||||
| 'insertReference'
|
||||
| 'isCollapsed'
|
||||
| 'blur'
|
||||
| 'setSelection'
|
||||
@@ -257,27 +261,60 @@ export class BlockHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public insertReference(
|
||||
reference: string,
|
||||
public setDoubleLinkSearchSlash(blockId: string, point: Point) {
|
||||
const textUtils = this._blockTextUtilsMap[blockId];
|
||||
if (textUtils) {
|
||||
textUtils.setDoubleLinkSearchSlash(point);
|
||||
} else {
|
||||
console.warn('Could find the block text utils');
|
||||
}
|
||||
}
|
||||
|
||||
public getDoubleLinkSearchSlashText(blockId: string) {
|
||||
const textUtils = this._blockTextUtilsMap[blockId];
|
||||
if (textUtils) {
|
||||
return textUtils.getDoubleLinkSearchSlashText();
|
||||
}
|
||||
console.warn('Could find the block text utils');
|
||||
return '';
|
||||
}
|
||||
public setSelectDoubleLinkSearchSlash(blockId: string) {
|
||||
const textUtils = this._blockTextUtilsMap[blockId];
|
||||
if (textUtils) {
|
||||
return textUtils.setSelectDoubleLinkSearchSlash();
|
||||
}
|
||||
console.warn('Could find the block text utils');
|
||||
return '';
|
||||
}
|
||||
|
||||
public removeDoubleLinkSearchSlash(
|
||||
blockId: string,
|
||||
selection: Selection,
|
||||
offset: number
|
||||
isRemoveSlash?: boolean
|
||||
) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (text_utils) {
|
||||
const offsetSelection = window.getSelection();
|
||||
offsetSelection.setBaseAndExtent(
|
||||
selection.anchorNode,
|
||||
selection.anchorOffset,
|
||||
selection.focusNode,
|
||||
selection.focusOffset + offset
|
||||
);
|
||||
const textUtils = this._blockTextUtilsMap[blockId];
|
||||
if (textUtils) {
|
||||
textUtils.removeDoubleLinkSearchSlash(isRemoveSlash);
|
||||
} else {
|
||||
console.warn('Could find the block text utils');
|
||||
}
|
||||
}
|
||||
|
||||
text_utils.removeSelection(offsetSelection);
|
||||
text_utils.insertReference(reference);
|
||||
|
||||
// range.
|
||||
// text_utils.toggleTextFormatBySelection(format, range);
|
||||
public async insertDoubleLink(
|
||||
workspaceId: string,
|
||||
linkBlockId: string,
|
||||
blockId: string
|
||||
) {
|
||||
const textUtils = this._blockTextUtilsMap[blockId];
|
||||
if (textUtils) {
|
||||
const linkBlock = await this._editor.getBlock({
|
||||
workspace: workspaceId,
|
||||
id: linkBlockId,
|
||||
});
|
||||
let children = linkBlock.getProperties().text?.value || [];
|
||||
if (children.length === 1 && !children[0].text) {
|
||||
children = [{ text: 'Untitled' }];
|
||||
}
|
||||
textUtils.insertDoubleLink(workspaceId, linkBlockId, children);
|
||||
}
|
||||
console.warn('Could find the block text utils');
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
/* eslint-disable max-lines */
|
||||
import HotKeys from 'hotkeys-js';
|
||||
|
||||
import type { PatchNode } from '@toeverything/components/ui';
|
||||
import { Commands } from '@toeverything/datasource/commands';
|
||||
import type {
|
||||
BlockFlavors,
|
||||
ReturnEditorBlock,
|
||||
UpdateEditorBlock,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
|
||||
import { Commands } from '@toeverything/datasource/commands';
|
||||
import { domToRect, last, Point, sleep } from '@toeverything/utils';
|
||||
import assert from 'assert';
|
||||
import HotKeys from 'hotkeys-js';
|
||||
import type { WorkspaceAndBlockId } from './block';
|
||||
import { AsyncBlock } from './block';
|
||||
import { BlockHelper } from './block/block-helper';
|
||||
@@ -295,7 +293,7 @@ export class Editor implements Virgo {
|
||||
return await blockView.onCreate(block);
|
||||
}
|
||||
|
||||
private async getBlock({
|
||||
public async getBlock({
|
||||
workspace,
|
||||
id,
|
||||
}: WorkspaceAndBlockId): Promise<AsyncBlock | null> {
|
||||
|
||||
@@ -408,13 +408,13 @@ export class SelectionManager implements VirgoSelection {
|
||||
|
||||
/**
|
||||
*
|
||||
* get previous activatable blockNode
|
||||
* get previous editable blockNode
|
||||
* @private
|
||||
* @param {AsyncBlock} node
|
||||
* @return {*} {(Promise<AsyncBlock | null>)}
|
||||
* @memberof SelectionManager
|
||||
*/
|
||||
private async _getPreviousActivatableBlockNode(
|
||||
private async _getPreviousEditableBlockNode(
|
||||
node: AsyncBlock
|
||||
): Promise<AsyncBlock | null> {
|
||||
const preNode = await node.physicallyPerviousSibling();
|
||||
@@ -422,8 +422,8 @@ export class SelectionManager implements VirgoSelection {
|
||||
if (!preNode) {
|
||||
return null;
|
||||
}
|
||||
const { activatable, selectable } = this._editor.getView(preNode.type);
|
||||
if (activatable) {
|
||||
const { editable, selectable } = this._editor.getView(preNode.type);
|
||||
if (editable) {
|
||||
this.setSelectedNodesIds([]);
|
||||
return preNode;
|
||||
}
|
||||
@@ -432,18 +432,18 @@ export class SelectionManager implements VirgoSelection {
|
||||
(document.activeElement as HTMLInputElement)?.blur();
|
||||
return null;
|
||||
}
|
||||
return this._getPreviousActivatableBlockNode(preNode);
|
||||
return this._getPreviousEditableBlockNode(preNode);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* get next activatable blockNode
|
||||
* get next editable blockNode
|
||||
* @private
|
||||
* @param {AsyncBlock} node
|
||||
* @return {*} {(Promise<AsyncBlock | null>)}
|
||||
* @memberof SelectionManager
|
||||
*/
|
||||
private async _getNextActivatableBlockNode(
|
||||
private async _getNextEditableBlockNode(
|
||||
node: AsyncBlock,
|
||||
ignoreSelf = true
|
||||
): Promise<AsyncBlock | null> {
|
||||
@@ -482,8 +482,8 @@ export class SelectionManager implements VirgoSelection {
|
||||
}
|
||||
}
|
||||
|
||||
const { activatable, selectable } = this._editor.getView(nextNode.type);
|
||||
if (activatable) {
|
||||
const { editable, selectable } = this._editor.getView(nextNode.type);
|
||||
if (editable) {
|
||||
this.setSelectedNodesIds([]);
|
||||
return nextNode;
|
||||
}
|
||||
@@ -492,7 +492,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
(document.activeElement as HTMLInputElement)?.blur();
|
||||
return null;
|
||||
}
|
||||
return this._getNextActivatableBlockNode(nextNode);
|
||||
return this._getNextEditableBlockNode(nextNode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -513,7 +513,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
return;
|
||||
}
|
||||
}
|
||||
preNode = await this._getPreviousActivatableBlockNode(node);
|
||||
preNode = await this._getPreviousEditableBlockNode(node);
|
||||
if (preNode) {
|
||||
this.activeNodeByNodeId(preNode.id, position);
|
||||
}
|
||||
@@ -622,7 +622,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
}
|
||||
}
|
||||
|
||||
nextNode = await this._getNextActivatableBlockNode(node);
|
||||
nextNode = await this._getNextEditableBlockNode(node);
|
||||
if (nextNode) {
|
||||
this.activeNodeByNodeId(nextNode.id, position);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { HTML2BlockResult } from '../clipboard/types';
|
||||
export interface CreateView {
|
||||
block: AsyncBlock;
|
||||
editor: Editor;
|
||||
editorElement: () => JSX.Element;
|
||||
/**
|
||||
* @deprecated Use recast table instead
|
||||
*/
|
||||
@@ -32,10 +31,10 @@ export abstract class BaseView {
|
||||
abstract type: string;
|
||||
|
||||
/**
|
||||
* activatable means can be focused
|
||||
* editable means can be focused
|
||||
* @memberof BaseView
|
||||
*/
|
||||
public activatable = true;
|
||||
public editable = true;
|
||||
|
||||
public selectable = true;
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
export { RenderRoot, MIN_PAGE_WIDTH } from './RenderRoot';
|
||||
export * from './render-block';
|
||||
export * from './hooks';
|
||||
|
||||
export { RenderBlock } from './render-block';
|
||||
|
||||
export * from './recast-block';
|
||||
export * from './recast-block/types';
|
||||
|
||||
export * from './block-pendant';
|
||||
|
||||
export { useEditor } from './Contexts';
|
||||
export * from './editor';
|
||||
export * from './hooks';
|
||||
export * from './kanban';
|
||||
export * from './kanban/types';
|
||||
|
||||
export * from './recast-block';
|
||||
export * from './recast-block/types';
|
||||
export {
|
||||
BlockRenderProvider,
|
||||
RenderBlockChildren,
|
||||
useBlockRender,
|
||||
withTreeViewChildren,
|
||||
} from './render-block';
|
||||
export { MIN_PAGE_WIDTH, RenderRoot } from './RenderRoot';
|
||||
export * from './utils';
|
||||
|
||||
export * from './editor';
|
||||
|
||||
export { RefPageProvider, useRefPage } from './ref-page';
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
RecastMetaProperty,
|
||||
RecastPropertyId,
|
||||
} from '../recast-block/types';
|
||||
import { BlockRenderProvider } from '../render-block';
|
||||
import { KanbanBlockRender } from '../render-block/RenderKanbanBlock';
|
||||
import { useInitKanbanEffect, useRecastKanban } from './kanban';
|
||||
import { KanbanGroup } from './types';
|
||||
|
||||
@@ -54,9 +56,11 @@ export const KanbanProvider = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<KanbanContext.Provider value={value}>
|
||||
{children}
|
||||
</KanbanContext.Provider>
|
||||
<BlockRenderProvider blockRender={KanbanBlockRender}>
|
||||
<KanbanContext.Provider value={value}>
|
||||
{children}
|
||||
</KanbanContext.Provider>
|
||||
</BlockRenderProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock } from '../editor';
|
||||
import { ComponentType, createContext, ReactNode, useContext } from 'react';
|
||||
import { RecastBlock } from './types';
|
||||
import { RefPageProvider } from '../ref-page';
|
||||
|
||||
/**
|
||||
* Determine whether the block supports RecastBlock
|
||||
@@ -48,7 +47,7 @@ export const RecastBlockProvider = ({
|
||||
|
||||
return (
|
||||
<RecastBlockContext.Provider value={block}>
|
||||
<RefPageProvider>{children}</RefPageProvider>
|
||||
{children}
|
||||
</RecastBlockContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui';
|
||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { RenderBlock } from '../render-block';
|
||||
|
||||
const Dialog = styled('div')({
|
||||
flex: 1,
|
||||
width: '880px',
|
||||
margin: '72px auto',
|
||||
background: '#fff',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
borderRadius: '10px',
|
||||
padding: '72px 120px',
|
||||
overflow: 'scroll',
|
||||
});
|
||||
|
||||
const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
|
||||
const theme = useTheme();
|
||||
const { closeSubPage } = useRefPage();
|
||||
|
||||
return createPortal(
|
||||
<MuiBackdrop
|
||||
open={open}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'rgba(58, 76, 92, 0.4)',
|
||||
zIndex: theme.affine.zIndex.popover,
|
||||
}}
|
||||
onClick={closeSubPage}
|
||||
>
|
||||
<Dialog
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
</MuiBackdrop>,
|
||||
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
const ModalPage = ({ blockId }: { blockId: string | null }) => {
|
||||
return (
|
||||
<Modal open={!!blockId}>
|
||||
{blockId && <RenderBlock blockId={blockId} />}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const RefPageContext = createContext<
|
||||
ReturnType<typeof useState<string | null>> | undefined
|
||||
>(undefined);
|
||||
|
||||
export const RefPageProvider = ({ children }: { children: ReactNode }) => {
|
||||
const state = useState<string | null>();
|
||||
const [blockId, setBlockId] = state;
|
||||
|
||||
return (
|
||||
<RefPageContext.Provider value={state}>
|
||||
{children}
|
||||
<ModalPage blockId={blockId ?? null} />
|
||||
</RefPageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useRefPage = () => {
|
||||
const context = useContext(RefPageContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'Wrap your app inside of a `SubPageProvider` to have access to the hook context!'
|
||||
);
|
||||
}
|
||||
const [blockId, setBlockId] = context;
|
||||
const openSubPage = (blockId: string) => {
|
||||
setBlockId(blockId);
|
||||
};
|
||||
const closeSubPage = () => {
|
||||
setBlockId(null);
|
||||
};
|
||||
|
||||
return { blockId, open: !!blockId, openSubPage, closeSubPage };
|
||||
};
|
||||
|
||||
// export const openSubPage = () => {};
|
||||
@@ -1 +0,0 @@
|
||||
export { useRefPage, RefPageProvider } from './ModalPage';
|
||||
@@ -0,0 +1,35 @@
|
||||
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
|
||||
);
|
||||
|
||||
/**
|
||||
* CAUTION! DO NOT PROVIDE A DYNAMIC BLOCK RENDER!
|
||||
*/
|
||||
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 { useBlock } from '../hooks';
|
||||
|
||||
interface RenderBlockProps {
|
||||
/**
|
||||
* Render nothing
|
||||
*/
|
||||
export const NullBlockRender = (): null => null;
|
||||
|
||||
export interface RenderBlockProps {
|
||||
blockId: string;
|
||||
hasContainer?: boolean;
|
||||
}
|
||||
@@ -13,7 +18,7 @@ export function RenderBlock({
|
||||
blockId,
|
||||
hasContainer = true,
|
||||
}: RenderBlockProps) {
|
||||
const { editor, editorElement } = useEditor();
|
||||
const { editor } = useEditor();
|
||||
const { block } = useBlock(blockId);
|
||||
|
||||
const setRef = useCallback(
|
||||
@@ -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) {
|
||||
@@ -50,7 +55,6 @@ export function RenderBlock({
|
||||
block={block}
|
||||
columns={columns.columns}
|
||||
columnsFromId={columns.fromId}
|
||||
editorElement={editorElement}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -65,4 +69,5 @@ export function RenderBlock({
|
||||
|
||||
const BlockContainer = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
@@ -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' }),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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 './RenderBlockChildren';
|
||||
export { withTreeViewChildren } from './WithTreeViewChildren';
|
||||
|
||||
Reference in New Issue
Block a user