mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 03:26:47 +08:00
Merge remote-tracking branch 'origin/develop' into fix/experience
This commit is contained in:
@@ -22,7 +22,6 @@ const AffineBoard = ({
|
||||
editor,
|
||||
}: AffineBoardProps & { editor: BlockEditor }) => {
|
||||
const [app, set_app] = useState<TldrawApp>();
|
||||
|
||||
const [document] = useState(() => {
|
||||
return {
|
||||
...deepCopy(TldrawApp.default_document),
|
||||
@@ -49,7 +48,7 @@ const AffineBoard = ({
|
||||
};
|
||||
});
|
||||
|
||||
const shapes = useShapes(workspace, rootBlockId);
|
||||
const { shapes } = useShapes(workspace, rootBlockId);
|
||||
useEffect(() => {
|
||||
if (app) {
|
||||
app.replacePageContent(shapes || {}, {}, {});
|
||||
@@ -66,6 +65,9 @@ const AffineBoard = ({
|
||||
onMount(app) {
|
||||
set_app(app);
|
||||
},
|
||||
async onPaste(e, data) {
|
||||
console.log('e,data: ', e, data);
|
||||
},
|
||||
async onCopy(e, groupIds) {
|
||||
const clip =
|
||||
await editor.clipboard.clipboardUtils.getClipDataOfBlocksById(
|
||||
@@ -77,7 +79,7 @@ const AffineBoard = ({
|
||||
clip.getData()
|
||||
);
|
||||
},
|
||||
onChangePage(app, shapes, bindings, assets) {
|
||||
async onChangePage(app, shapes, bindings, assets) {
|
||||
Promise.all(
|
||||
Object.entries(shapes).map(async ([id, shape]) => {
|
||||
if (shape === undefined) {
|
||||
@@ -106,7 +108,7 @@ const AffineBoard = ({
|
||||
});
|
||||
}
|
||||
shape.affineId = block.id;
|
||||
return services.api.editorBlock.update({
|
||||
return await services.api.editorBlock.update({
|
||||
workspace: shape.workspace,
|
||||
id: block.id,
|
||||
properties: {
|
||||
|
||||
@@ -9,28 +9,37 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
const { pageClientWidth } = usePageClientWidth();
|
||||
// page padding left and right total 300px
|
||||
const editorShapeInitSize = pageClientWidth - 300;
|
||||
const [blocks, setBlocks] = useState<ReturnEditorBlock[]>();
|
||||
const [blocks, setBlocks] = useState<{
|
||||
shapes: [ReturnEditorBlock[]];
|
||||
}>();
|
||||
useEffect(() => {
|
||||
services.api.editorBlock
|
||||
.get({ workspace, ids: [rootBlockId] })
|
||||
.then(async blockData => {
|
||||
const shapes = await Promise.all(
|
||||
(blockData?.[0]?.children || []).map(async childId => {
|
||||
const childBlock = (
|
||||
await services.api.editorBlock.get({
|
||||
workspace,
|
||||
ids: [childId],
|
||||
})
|
||||
)?.[0];
|
||||
return childBlock;
|
||||
})
|
||||
);
|
||||
setBlocks(shapes);
|
||||
Promise.all([
|
||||
services.api.editorBlock
|
||||
.get({ workspace, ids: [rootBlockId] })
|
||||
.then(async blockData => {
|
||||
const shapes = await Promise.all(
|
||||
(blockData?.[0]?.children || []).map(async childId => {
|
||||
const childBlock = (
|
||||
await services.api.editorBlock.get({
|
||||
workspace,
|
||||
ids: [childId],
|
||||
})
|
||||
)?.[0];
|
||||
return childBlock;
|
||||
})
|
||||
);
|
||||
return shapes;
|
||||
}),
|
||||
]).then(shapes => {
|
||||
setBlocks({
|
||||
shapes: shapes,
|
||||
});
|
||||
});
|
||||
|
||||
let unobserve: () => void;
|
||||
services.api.editorBlock
|
||||
.observe({ workspace, id: rootBlockId }, async blockData => {
|
||||
const shapes = await Promise.all(
|
||||
Promise.all(
|
||||
(blockData?.children || []).map(async childId => {
|
||||
const childBlock = (
|
||||
await services.api.editorBlock.get({
|
||||
@@ -40,8 +49,11 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
)?.[0];
|
||||
return childBlock;
|
||||
})
|
||||
);
|
||||
setBlocks(shapes);
|
||||
).then(shapes => {
|
||||
setBlocks({
|
||||
shapes: [shapes],
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(cb => {
|
||||
unobserve = cb;
|
||||
@@ -53,8 +65,7 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
}, [workspace, rootBlockId]);
|
||||
|
||||
let groupCount = 0;
|
||||
|
||||
return blocks?.reduce((acc, block) => {
|
||||
let blocksShapes = blocks?.shapes[0]?.reduce((acc, block) => {
|
||||
const shapeProps = block.properties.shapeProps?.value
|
||||
? JSON.parse(block.properties.shapeProps.value)
|
||||
: {};
|
||||
@@ -75,4 +86,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, TDShape>);
|
||||
|
||||
return {
|
||||
shapes: blocksShapes,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
|
||||
import {
|
||||
RenderBlock,
|
||||
RenderRoot,
|
||||
type BlockEditor,
|
||||
} from '@toeverything/components/editor-core';
|
||||
@@ -88,9 +87,7 @@ export const AffineEditor = forwardRef<BlockEditor, AffineEditorProps>(
|
||||
editor={editor}
|
||||
editorElement={AffineEditor as any}
|
||||
scrollBlank={scrollBlank}
|
||||
>
|
||||
<RenderBlock blockId={editor.getRootBlockId()} />
|
||||
</RenderRoot>
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
TDStatus,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
// import { FocusButton } from '~components/FocusButton';
|
||||
import { usePageClientWidth } from '@toeverything/datasource/state';
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
@@ -22,22 +24,20 @@ import {
|
||||
useState,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { CommandPanel } from './components/command-panel';
|
||||
// import { TopPanel } from '~components/TopPanel';
|
||||
import { ContextMenu } from './components/context-menu';
|
||||
import { ErrorFallback } from './components/error-fallback';
|
||||
import { Loading } from './components/loading';
|
||||
import { ToolsPanel } from './components/tools-panel';
|
||||
import { ZoomBar } from './components/zoom-bar';
|
||||
import {
|
||||
TldrawContext,
|
||||
useKeyboardShortcuts,
|
||||
useStylesheet,
|
||||
useTldrawApp,
|
||||
} from './hooks';
|
||||
// import { TopPanel } from '~components/TopPanel';
|
||||
import { ContextMenu } from './components/context-menu';
|
||||
// import { FocusButton } from '~components/FocusButton';
|
||||
import { usePageClientWidth } from '@toeverything/datasource/state';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { CommandPanel } from './components/command-panel';
|
||||
import { ErrorFallback } from './components/error-fallback';
|
||||
import { Loading } from './components/loading';
|
||||
import { ZoomBar } from './components/zoom-bar';
|
||||
|
||||
export interface TldrawProps extends TldrawAppCtorProps {
|
||||
/**
|
||||
@@ -295,7 +295,6 @@ const InnerTldraw = memo(function InnerTldraw({
|
||||
const pageState = document.pageStates[page.id];
|
||||
const assets = document.assets;
|
||||
const { selectedIds } = pageState;
|
||||
|
||||
const isHideBoundsShape =
|
||||
selectedIds.length === 1 &&
|
||||
page.shapes[selectedIds[0]] &&
|
||||
@@ -363,7 +362,7 @@ const InnerTldraw = memo(function InnerTldraw({
|
||||
|
||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||
const hideBounds =
|
||||
(isInSession && app.session?.constructor.name !== 'BrushSession') ||
|
||||
(isInSession && app.session?.constructor.name === 'BrushSession') ||
|
||||
!isSelecting ||
|
||||
isHideBoundsShape ||
|
||||
!!pageState.editingId;
|
||||
|
||||
@@ -56,7 +56,8 @@ const SelectableContainer = styled('div')<{ selected?: boolean }>(
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
margin: '5px',
|
||||
padding: '3px',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
boxSizing: 'border-box',
|
||||
'&:hover': {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import type {
|
||||
ArrowBinding,
|
||||
TDShape,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { ConnectorIcon } from '@toeverything/components/icons';
|
||||
import { IconButton, Popover, Tooltip } from '@toeverything/components/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ListItemContainer, ListItemTitle } from './FontSizeConfig';
|
||||
|
||||
interface GroupAndUnGroupProps {
|
||||
app: TldrawApp;
|
||||
shapes: TDShape[];
|
||||
}
|
||||
|
||||
export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
const [arrowToArr, setarrowToArr] = useState([]);
|
||||
useEffect(() => {
|
||||
let allShape = app.shapes;
|
||||
let bindings = app.page.bindings;
|
||||
let activeShape = shapes[0];
|
||||
let toNextShapBindings: ArrowBinding[] = [];
|
||||
let bindingId = '';
|
||||
if (!activeShape) {
|
||||
return;
|
||||
}
|
||||
Object.keys(bindings).forEach(key => {
|
||||
if (bindings[key].toId === activeShape.id) {
|
||||
bindingId = bindings[key].fromId;
|
||||
}
|
||||
});
|
||||
Object.keys(bindings).forEach(key => {
|
||||
if (bindings[key].fromId === bindingId) {
|
||||
toNextShapBindings.push(bindings[key]);
|
||||
}
|
||||
});
|
||||
let ArrowToArr: TDShape[] = [];
|
||||
toNextShapBindings.forEach(binding => {
|
||||
if (binding.toId !== activeShape.id) {
|
||||
allShape.forEach(item => {
|
||||
if (item.id === binding.toId) {
|
||||
ArrowToArr.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
setarrowToArr(ArrowToArr);
|
||||
return () => {};
|
||||
}, [app.page.bindings]);
|
||||
const jumpToNextShap = (shape: TDShape) => {
|
||||
app.zoomToShapes([shape]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
trigger="hover"
|
||||
placement="bottom-start"
|
||||
content={
|
||||
<div>
|
||||
{arrowToArr.map((arrow: any) => {
|
||||
return (
|
||||
<ListItemContainer
|
||||
key={arrow.id}
|
||||
onClick={() => jumpToNextShap(arrow)}
|
||||
>
|
||||
<ListItemTitle>{arrow.name}</ListItemTitle>
|
||||
</ListItemContainer>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tooltip content="ArrowToEditor" placement="top-start">
|
||||
<IconButton>
|
||||
<ConnectorIcon></ConnectorIcon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -106,6 +106,12 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => {
|
||||
shapes={config.deleteShapes.selectedShapes}
|
||||
></AlignOperation>
|
||||
) : null,
|
||||
// toNextShap: (
|
||||
// <ArrowTo
|
||||
// app={app}
|
||||
// shapes={config.deleteShapes.selectedShapes}
|
||||
// ></ArrowTo>
|
||||
// ),
|
||||
};
|
||||
|
||||
const nodes = Object.entries(configNodes).filter(([key, node]) => !!node);
|
||||
|
||||
@@ -86,7 +86,7 @@ export const FontSizeConfig = ({ app, shapes }: FontSizeConfigProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ListItemContainer = styled('div')(({ theme }) => ({
|
||||
export const ListItemContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
@@ -100,7 +100,7 @@ const ListItemContainer = styled('div')(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const ListItemTitle = styled('span')(({ theme }) => ({
|
||||
export const ListItemTitle = styled('span')(({ theme }) => ({
|
||||
marginLeft: '12px',
|
||||
color: theme.affine.palette.primaryText,
|
||||
}));
|
||||
|
||||
@@ -35,6 +35,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
|
||||
|
||||
export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
|
||||
TDShapeType.Line
|
||||
@@ -51,8 +52,10 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<ShapesContainer>
|
||||
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
|
||||
@@ -60,6 +63,7 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
app.selectTool(type);
|
||||
setVisible(false);
|
||||
setLastActiveTool(type);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -71,6 +71,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
|
||||
|
||||
export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
|
||||
TDShapeType.Rectangle
|
||||
@@ -87,8 +88,10 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<ShapesContainer>
|
||||
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
|
||||
@@ -96,6 +99,7 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
app.selectTool(type);
|
||||
setVisible(false);
|
||||
setLastActiveTool(type);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
import {
|
||||
IconButton,
|
||||
PopoverContainer,
|
||||
styled,
|
||||
// MuiIconButton as IconButton,
|
||||
// MuiTooltip as Tooltip,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from '@toeverything/components/ui';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { TldrawApp } from '@toeverything/components/board-state';
|
||||
import {
|
||||
@@ -90,8 +90,8 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
|
||||
}}
|
||||
direction="none"
|
||||
>
|
||||
<div className={styles('container')}>
|
||||
<div className={styles('toolBar')}>
|
||||
<Container>
|
||||
<ToolBar>
|
||||
{tools.map(
|
||||
({
|
||||
type,
|
||||
@@ -127,24 +127,22 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ToolBar>
|
||||
</Container>
|
||||
</PopoverContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = style9.create({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
toolBar: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '10px',
|
||||
padding: '4px 4px',
|
||||
},
|
||||
const Container = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
});
|
||||
const ToolBar = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '10px',
|
||||
padding: '4px 4px',
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
styled,
|
||||
Tooltip,
|
||||
} from '@toeverything/components/ui';
|
||||
import { ReactElement, type CSSProperties } from 'react';
|
||||
import { useState, type CSSProperties, type ReactElement } from 'react';
|
||||
import { Palette } from '../../palette';
|
||||
import { Pen } from './Pen';
|
||||
|
||||
@@ -106,6 +106,7 @@ const PENCIL_CONFIGS_MAP = PENCIL_CONFIGS.reduce<
|
||||
|
||||
export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
const appCurrentTool = app.useStore(state => state.appState.activeTool);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const chosenPen =
|
||||
PENCIL_CONFIGS.find(config => config.key === appCurrentTool) ||
|
||||
PENCIL_CONFIGS[0];
|
||||
@@ -134,8 +135,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<Container>
|
||||
<PensContainer>
|
||||
@@ -153,7 +156,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
name={title}
|
||||
primaryColor={color_vars['--color-0']}
|
||||
secondaryColor={color_vars['--color-1']}
|
||||
onClick={() => setPen(key)}
|
||||
onClick={() => {
|
||||
setVisible(false);
|
||||
setPen(key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -163,7 +169,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
<Palette
|
||||
selected={chosenColor}
|
||||
colors={PENCIL_CONFIGS_MAP[chosenPenKey].colors}
|
||||
onSelect={color => setPenColor(color)}
|
||||
onSelect={color => {
|
||||
setVisible(false);
|
||||
setPenColor(color);
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
</Container>
|
||||
|
||||
@@ -42,7 +42,7 @@ type E = HTMLDivElement;
|
||||
export class ArrowUtil extends TDShapeUtil<T, E> {
|
||||
type = TDShapeType.Arrow as const;
|
||||
|
||||
override hideBounds = true;
|
||||
override hideBounds = false;
|
||||
|
||||
override canEdit = true;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TDShapeType,
|
||||
TransformInfo,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { BlockEditor } from '@toeverything/components/editor-core';
|
||||
import { MIN_PAGE_WIDTH } from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { MouseEvent, SyntheticEvent } from 'react';
|
||||
@@ -66,6 +67,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
|
||||
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
||||
({ shape, meta: { app }, events, isEditing, onShapeChange }, ref) => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const editorRef = useRef<BlockEditor>();
|
||||
const {
|
||||
workspace,
|
||||
rootBlockId,
|
||||
@@ -135,6 +137,18 @@ export class EditorUtil extends TDShapeUtil<T, E> {
|
||||
}
|
||||
}, [app, state, shape.id, editingText, editingId]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (isEditing) {
|
||||
const lastBlock =
|
||||
await editorRef.current.getLastBlock();
|
||||
editorRef.current.selectionManager.activeNodeByNodeId(
|
||||
lastBlock.id
|
||||
);
|
||||
}
|
||||
})();
|
||||
}, [isEditing]);
|
||||
|
||||
const onMouseDown = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (e.detail === 2) {
|
||||
@@ -159,6 +173,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
|
||||
rootBlockId={rootBlockId}
|
||||
scrollBlank={false}
|
||||
isEdgeless
|
||||
ref={editorRef}
|
||||
/>
|
||||
{editingText ? null : <Mask />}
|
||||
</Container>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Utils } from '@tldraw/core';
|
||||
import type { ShapeStyles } from '@toeverything/components/board-types';
|
||||
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
|
||||
import {
|
||||
BINDING_DISTANCE,
|
||||
ShapeStyles,
|
||||
} from '@toeverything/components/board-types';
|
||||
import * as React from 'react';
|
||||
import { getShapeStyle } from '../../shared';
|
||||
|
||||
@@ -56,23 +58,20 @@ export const DashedRectangle = React.memo(function DashedRectangle({
|
||||
return (
|
||||
<>
|
||||
<rect
|
||||
className={
|
||||
isSelected || style.isFilled
|
||||
? 'tl-fill-hitarea'
|
||||
: 'tl-stroke-hitarea'
|
||||
}
|
||||
className={'tl-fill-hitarea'}
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
opacity={1}
|
||||
strokeWidth={BINDING_DISTANCE}
|
||||
/>
|
||||
{style.isFilled && (
|
||||
<rect
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
width={w - sw / 2}
|
||||
height={h - sw / 2}
|
||||
fill={fill}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
@@ -175,6 +175,10 @@ interface TDCallbacks {
|
||||
* (optional) A callback to run when the shape is copied.
|
||||
*/
|
||||
onCopy?: (e: ClipboardEvent, ids: string[]) => void;
|
||||
/**
|
||||
* (optional) A callback to run when the shape is paste.
|
||||
*/
|
||||
onPaste?: (e: ClipboardEvent, data: any) => void;
|
||||
}
|
||||
|
||||
export interface TldrawAppCtorProps {
|
||||
@@ -444,7 +448,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
if (visitedShapes.has(fromShape)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only need to update the binding's "from" shape (an arrow)
|
||||
const fromDelta = TLDR.update_arrow_bindings(
|
||||
page,
|
||||
@@ -627,7 +630,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
private prev_bindings = this.page.bindings;
|
||||
private prev_assets = this.document.assets;
|
||||
|
||||
private _broadcastPageChanges = () => {
|
||||
private _broadcastPageChanges = async () => {
|
||||
const visited = new Set<string>();
|
||||
|
||||
const changedShapes: Record<string, TDShape | undefined> = {};
|
||||
@@ -684,7 +687,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
Object.keys(changedAssets).length > 0
|
||||
) {
|
||||
this.just_sent = true;
|
||||
this.callbacks.onChangePage?.(
|
||||
await this.callbacks.onChangePage?.(
|
||||
this,
|
||||
changedShapes,
|
||||
changedBindings,
|
||||
@@ -860,7 +863,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
if (!page.bindings[binding.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromShape = page.shapes[binding.fromId] as ArrowShape;
|
||||
|
||||
if (visitedShapes.has(fromShape)) {
|
||||
@@ -868,6 +870,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
}
|
||||
|
||||
// We only need to update the binding's "from" shape (an arrow)
|
||||
|
||||
const fromDelta = TLDR.update_arrow_bindings(page, fromShape);
|
||||
visitedShapes.add(fromShape);
|
||||
|
||||
@@ -1957,6 +1960,53 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
paste = async (point?: number[], e?: ClipboardEvent) => {
|
||||
if (this.readOnly) return;
|
||||
|
||||
if (e) {
|
||||
const data = e.clipboardData?.getData('text/html');
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
return json;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
this.callbacks.onPaste?.(e, paste_as_html(data));
|
||||
return this;
|
||||
}
|
||||
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
|
||||
return;
|
||||
} else {
|
||||
throw Error('Not tldraw data!');
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html);
|
||||
return;
|
||||
}
|
||||
};
|
||||
const pasteInCurrentPage = (
|
||||
shapes: TDShape[],
|
||||
bindings: TDBinding[],
|
||||
@@ -2108,31 +2158,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
// this.select(shapeId);
|
||||
};
|
||||
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
|
||||
return;
|
||||
} else {
|
||||
throw Error('Not tldraw data!');
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (e !== undefined) {
|
||||
const items: DataTransferItemList =
|
||||
e.clipboardData?.items ?? ({} as DataTransferItemList);
|
||||
@@ -3854,7 +3879,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
|
||||
private get_viewbox_from_svg = (svgStr: string | ArrayBuffer | null) => {
|
||||
if (typeof svgStr === 'string') {
|
||||
let viewBox = new DOMParser().parseFromString(svgStr, 'text/xml');
|
||||
const viewBox = new DOMParser().parseFromString(svgStr, 'text/xml');
|
||||
return viewBox.children[0].getAttribute('viewBox');
|
||||
}
|
||||
|
||||
@@ -4138,7 +4163,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
};
|
||||
|
||||
onPointerDown: TLPointerEventHandler = (info, e) => {
|
||||
if (e.buttons === 4) {
|
||||
if (e.button === 1) {
|
||||
this.patchState({
|
||||
settings: {
|
||||
forcePanning: true,
|
||||
@@ -4155,6 +4180,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
};
|
||||
|
||||
onPointerUp: TLPointerEventHandler = (info, e) => {
|
||||
if (e.button === 1) {
|
||||
this.patchState({
|
||||
settings: {
|
||||
forcePanning: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.isPointing = false;
|
||||
this.updateInputs(info, e);
|
||||
this.currentTool.onPointerUp?.(info, e);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BaseEditor } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { DoubleLinkElement } from './text/plugins/DoubleLink';
|
||||
import { LinkElement } from './text/plugins/link';
|
||||
import { RefLinkElement } from './text/plugins/reflink';
|
||||
|
||||
@@ -8,7 +9,8 @@ export type CustomElement =
|
||||
| { type: string; children: CustomElement[] }
|
||||
| CustomText
|
||||
| LinkElement
|
||||
| RefLinkElement;
|
||||
| RefLinkElement
|
||||
| DoubleLinkElement;
|
||||
declare module 'slate' {
|
||||
interface CustomTypes {
|
||||
Editor: BaseEditor & ReactEditor;
|
||||
@@ -19,35 +21,32 @@ declare module 'slate' {
|
||||
|
||||
export { BlockPreview, StyledBlockPreview } from './block-preview';
|
||||
export { default as Button } from './button';
|
||||
export type { CommonListItem } from './list';
|
||||
export { CommonList, BackLink, commonListContainer } from './list';
|
||||
export * from './Logo';
|
||||
export { default as Toolbar } from './toolbar';
|
||||
export { CollapsibleTitle } from './collapsible-title';
|
||||
|
||||
export * from './text';
|
||||
|
||||
export * from './comming-soon/CommingSoon';
|
||||
export {
|
||||
NewpageIcon,
|
||||
AddIcon,
|
||||
ClockIcon,
|
||||
ViewSidebarIcon,
|
||||
CloseIcon,
|
||||
CodeBlockInlineIcon,
|
||||
DocumentIcon,
|
||||
FilterIcon,
|
||||
FullScreenIcon,
|
||||
HighlighterDuotoneIcon,
|
||||
KanbanIcon,
|
||||
ListIcon,
|
||||
SpaceIcon,
|
||||
NewpageIcon,
|
||||
PagesIcon,
|
||||
PencilDotDuotoneIcon,
|
||||
PencilDuotoneIcon,
|
||||
HighlighterDuotoneIcon,
|
||||
CodeBlockInlineIcon,
|
||||
PagesIcon,
|
||||
CloseIcon,
|
||||
DocumentIcon,
|
||||
TodoListIcon,
|
||||
KanbanIcon,
|
||||
TableIcon,
|
||||
AddIcon,
|
||||
FilterIcon,
|
||||
SorterIcon,
|
||||
FullScreenIcon,
|
||||
SpaceIcon,
|
||||
TableIcon,
|
||||
TodoListIcon,
|
||||
UnGroupIcon,
|
||||
ViewSidebarIcon,
|
||||
} from './icon';
|
||||
|
||||
export * from './comming-soon/CommingSoon';
|
||||
export { BackLink, CommonList, commonListContainer } from './list';
|
||||
export type { CommonListItem } from './list';
|
||||
export * from './Logo';
|
||||
export * from './text';
|
||||
export { default as Toolbar } from './toolbar';
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import clsx from 'clsx';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { BackwardUndoIcon } from '@toeverything/components/icons';
|
||||
import {
|
||||
BaseButton,
|
||||
ListButton,
|
||||
@@ -12,9 +8,11 @@ import {
|
||||
} from '@toeverything/components/ui';
|
||||
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
|
||||
import { BlockSearchItem } from '@toeverything/datasource/jwt';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import style9 from 'style9';
|
||||
import { BlockPreview } from '../block-preview';
|
||||
import { BackwardUndoIcon } from '@toeverything/components/icons';
|
||||
|
||||
export const commonListContainer = 'commonListContainer';
|
||||
|
||||
@@ -28,6 +26,7 @@ export type CommonListItem = {
|
||||
divider?: string;
|
||||
content?: Content;
|
||||
block?: BlockSearchItem;
|
||||
renderCustom?: (props: CommonListItem) => JSX.Element;
|
||||
};
|
||||
|
||||
type MenuItemsProps = {
|
||||
@@ -45,7 +44,7 @@ export const CommonList = (props: MenuItemsProps) => {
|
||||
// ]);
|
||||
// TODO Insert bidirectional link to be developed
|
||||
const JSONUnsupportedBlockTypes = ['page'];
|
||||
let usedItems = items.filter(item => {
|
||||
const usedItems = items.filter(item => {
|
||||
return !JSONUnsupportedBlockTypes.includes(item?.content?.id);
|
||||
});
|
||||
return (
|
||||
@@ -58,7 +57,9 @@ export const CommonList = (props: MenuItemsProps) => {
|
||||
>
|
||||
{usedItems?.length ? (
|
||||
usedItems.map((item, idx) => {
|
||||
if (item.block) {
|
||||
if (item.renderCustom) {
|
||||
return item.renderCustom(item);
|
||||
} else if (item.block) {
|
||||
return (
|
||||
<BlockPreview
|
||||
className={clsx(
|
||||
|
||||
@@ -1,54 +1,53 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { ErrorBoundary, isEqual } from '@toeverything/utils';
|
||||
import isHotkey from 'is-hotkey';
|
||||
import isUrl from 'is-url';
|
||||
import React, {
|
||||
CSSProperties,
|
||||
DragEvent,
|
||||
forwardRef,
|
||||
KeyboardEvent,
|
||||
KeyboardEventHandler,
|
||||
MouseEvent,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
forwardRef,
|
||||
MouseEventHandler,
|
||||
useLayoutEffect,
|
||||
CSSProperties,
|
||||
MouseEvent,
|
||||
DragEvent,
|
||||
} from 'react';
|
||||
import isHotkey from 'is-hotkey';
|
||||
import {
|
||||
createEditor,
|
||||
Descendant,
|
||||
Range,
|
||||
Element as SlateElement,
|
||||
Editor,
|
||||
Transforms,
|
||||
Element as SlateElement,
|
||||
Node,
|
||||
Path,
|
||||
Range,
|
||||
Transforms,
|
||||
} from 'slate';
|
||||
import {
|
||||
Editable,
|
||||
withReact,
|
||||
Slate,
|
||||
ReactEditor,
|
||||
Slate,
|
||||
useSlateStatic,
|
||||
withReact,
|
||||
} from 'slate-react';
|
||||
|
||||
import { ErrorBoundary, isEqual, uaHelper } from '@toeverything/utils';
|
||||
|
||||
import { Contents, SlateUtils, isSelectAll } from './slate-utils';
|
||||
import { CustomElement } from '..';
|
||||
import { HOTKEYS, INLINE_STYLES } from './constants';
|
||||
import { TextWithComments } from './element-leaf/TextWithComments';
|
||||
import { InlineDate, withDate } from './plugins/date';
|
||||
import { DoubleLinkComponent } from './plugins/DoubleLink';
|
||||
import { LinkComponent, LinkModal, withLinks, wrapLink } from './plugins/link';
|
||||
import { InlineRefLink } from './plugins/reflink';
|
||||
import { Contents, isSelectAll, SlateUtils } from './slate-utils';
|
||||
import {
|
||||
getCommentsIdsOnTextNode,
|
||||
getExtraPropertiesFromEditorOutmostNode,
|
||||
isInterceptCharacter,
|
||||
matchMarkdown,
|
||||
} from './utils';
|
||||
import { HOTKEYS, INLINE_STYLES } from './constants';
|
||||
import { LinkComponent, LinkModal, withLinks, wrapLink } from './plugins/link';
|
||||
import { withDate, InlineDate } from './plugins/date';
|
||||
import { CustomElement } from '..';
|
||||
import isUrl from 'is-url';
|
||||
import { InlineRefLink } from './plugins/reflink';
|
||||
import { TextWithComments } from './element-leaf/TextWithComments';
|
||||
|
||||
export interface TextProps {
|
||||
/** read only */
|
||||
@@ -739,6 +738,9 @@ const EditorElement = (props: any) => {
|
||||
|
||||
switch (element.type) {
|
||||
case 'link': {
|
||||
if (element.linkType === 'doubleLink') {
|
||||
return <DoubleLinkComponent {...props} editor={editor} />;
|
||||
}
|
||||
return (
|
||||
<LinkComponent
|
||||
{...props}
|
||||
@@ -802,7 +804,7 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => {
|
||||
backgroundColor: '#F2F5F9',
|
||||
borderRadius: '5px',
|
||||
color: '#3A4C5C',
|
||||
padding: '3px 8px',
|
||||
padding: '1px 8px',
|
||||
margin: '0 2px',
|
||||
}}
|
||||
>
|
||||
@@ -839,6 +841,14 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (leaf.doubleLinkSearch) {
|
||||
customChildren = (
|
||||
<span style={{ backgroundColor: '#eee', borderRadius: '4px' }}>
|
||||
{customChildren}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
customChildren = (
|
||||
<TextWithComments commentsIds={commentsIds}>
|
||||
{customChildren}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { PagesIcon } from '@toeverything/components/icons';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Descendant } from 'slate';
|
||||
import { RenderElementProps } from 'slate-react';
|
||||
|
||||
export type DoubleLinkElement = {
|
||||
type: 'link';
|
||||
workspaceId: string;
|
||||
blockId: string;
|
||||
children: Descendant[];
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const DoubleLinkComponent = (props: RenderElementProps) => {
|
||||
const { attributes, children, element } = props;
|
||||
const doubleLinkElement = element as DoubleLinkElement;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClickLinkText = useCallback(
|
||||
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
const { workspaceId, blockId } = doubleLinkElement;
|
||||
navigate(`/${workspaceId}/${blockId}`);
|
||||
},
|
||||
[doubleLinkElement, navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
<span>
|
||||
<PagesIcon style={{ verticalAlign: 'middle', height: '20px' }} />
|
||||
<a
|
||||
{...attributes}
|
||||
style={{ cursor: 'pointer' }}
|
||||
href={`/${doubleLinkElement.workspaceId}/${doubleLinkElement.blockId}`}
|
||||
>
|
||||
<span onClick={handleClickLinkText}>{children}</span>
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Transforms,
|
||||
} from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
|
||||
import type { CustomElement } from '..';
|
||||
import {
|
||||
fontBgColorPalette,
|
||||
fontColorPalette,
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import {
|
||||
getCommentsIdsOnTextNode,
|
||||
getEditorMarkForCommentId,
|
||||
getRandomString,
|
||||
MARKDOWN_STYLE_MAP,
|
||||
MatchRes,
|
||||
} from './utils';
|
||||
@@ -247,7 +248,11 @@ Editor.insertText = function (
|
||||
const { path, offset } = at;
|
||||
if (text.length > 0) {
|
||||
const marks = Editor.marks(editor);
|
||||
if (text === '\u0020' && Object.keys(marks).length) {
|
||||
if (
|
||||
text === '\u0020' &&
|
||||
Object.keys(marks).length &&
|
||||
!(marks as any).doubleLinkSearch
|
||||
) {
|
||||
// If the input is a space and the mark has content, remove the mark
|
||||
const newPath = [path[0], path[1] + 1];
|
||||
const newPoint = {
|
||||
@@ -594,8 +599,13 @@ class SlateUtils {
|
||||
|
||||
const fragmentChildren = firstFragment.children;
|
||||
|
||||
const textChildren: Text[] = [];
|
||||
for (const child of fragmentChildren) {
|
||||
const textChildren: CustomElement[] = [];
|
||||
for (let i = 0; i < fragmentChildren.length; i++) {
|
||||
const child = fragmentChildren[i];
|
||||
if ('type' in child && child.type === 'link') {
|
||||
i !== fragmentChildren.length - 1 && textChildren.push(child);
|
||||
continue;
|
||||
}
|
||||
if (!('text' in child)) {
|
||||
console.error('Debug information:', point1, point2, fragment);
|
||||
throw new Error('Fragment exists nested!');
|
||||
@@ -1119,34 +1129,130 @@ class SlateUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public insertReference(reference: string) {
|
||||
try {
|
||||
// Transforms.setSelection(this.editor, this.getEndSelection());
|
||||
const { anchor, focus } = this.getEndSelection();
|
||||
Transforms.insertNodes(
|
||||
public setDoubleLinkSearchSlash(point: Point) {
|
||||
const str = Editor.string(this.editor, {
|
||||
anchor: this.getStart(),
|
||||
focus: point,
|
||||
});
|
||||
if (str.endsWith('[[')) {
|
||||
Transforms.select(this.editor, {
|
||||
anchor: point,
|
||||
focus: Object.assign({}, point, {
|
||||
offset: point.offset - 2,
|
||||
}),
|
||||
});
|
||||
Editor.addMark(this.editor, 'doubleLinkSearch', true);
|
||||
Transforms.select(this.editor, {
|
||||
anchor: this.editor.selection.anchor,
|
||||
focus: this.editor.selection.anchor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getDoubleLinkSearchSlashText() {
|
||||
const nodes = Editor.nodes(this.editor, {
|
||||
at: [],
|
||||
//@ts-ignore
|
||||
match: node => !!node.doubleLinkSearch,
|
||||
});
|
||||
const searchNode = nodes.next().value;
|
||||
if (searchNode && (searchNode[0] as { text?: string }).text) {
|
||||
return (searchNode[0] as { text?: string }).text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public setSelectDoubleLinkSearchSlash() {
|
||||
const nodes = Editor.nodes(this.editor, {
|
||||
at: [],
|
||||
//@ts-ignore
|
||||
match: node => !!node.doubleLinkSearch,
|
||||
});
|
||||
const searchNode = nodes.next().value;
|
||||
if (searchNode) {
|
||||
const text = (searchNode[0] as { text?: string })?.text || '';
|
||||
const path = searchNode[1];
|
||||
const anchor = Editor.before(
|
||||
this.editor,
|
||||
{
|
||||
type: 'reflink',
|
||||
reference,
|
||||
children: [],
|
||||
path,
|
||||
offset: 1,
|
||||
},
|
||||
{ at: focus || anchor }
|
||||
{
|
||||
unit: 'offset',
|
||||
}
|
||||
);
|
||||
|
||||
// requestAnimationFrame(() => {
|
||||
// console.log(this.editor.selection, this.editor.insertNode);
|
||||
// this.editor.insertNode({
|
||||
// type: 'reflink',
|
||||
// reference,
|
||||
// children: [{ text: '' }]
|
||||
// });
|
||||
// // Transforms.select();
|
||||
// });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
const focus = Editor.after(
|
||||
this.editor,
|
||||
{
|
||||
path,
|
||||
offset: text.length - 1,
|
||||
},
|
||||
{
|
||||
unit: 'offset',
|
||||
}
|
||||
);
|
||||
Transforms.select(this.editor, { anchor, focus });
|
||||
}
|
||||
}
|
||||
|
||||
public removeDoubleLinkSearchSlash(isRemoveSlash?: boolean) {
|
||||
if (isRemoveSlash) {
|
||||
const nodes = Editor.nodes(this.editor, {
|
||||
at: [],
|
||||
//@ts-ignore
|
||||
match: node => !!node.doubleLinkSearch,
|
||||
});
|
||||
const searchNode = nodes.next().value;
|
||||
if (searchNode) {
|
||||
const text = (searchNode[0] as { text?: string })?.text || '';
|
||||
if (text.startsWith('[[')) {
|
||||
const path = searchNode[1];
|
||||
Transforms.delete(this.editor, {
|
||||
at: {
|
||||
path,
|
||||
offset: 0,
|
||||
},
|
||||
distance: text.length,
|
||||
unit: 'character',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Transforms.setNodes(
|
||||
this.editor,
|
||||
{ doubleLinkSearch: null } as Partial<SlateNode>,
|
||||
{
|
||||
at: [],
|
||||
match: node => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
return !!node.doubleLinkSearch;
|
||||
},
|
||||
}
|
||||
);
|
||||
this.editor.removeMark('doubleLinkSearch');
|
||||
}
|
||||
|
||||
public insertDoubleLink(
|
||||
workspaceId: string,
|
||||
linkBlockId: string,
|
||||
children: any[]
|
||||
) {
|
||||
const link = {
|
||||
type: 'link',
|
||||
linkType: 'doubleLink',
|
||||
workspaceId: workspaceId,
|
||||
blockId: linkBlockId,
|
||||
children: children,
|
||||
id: getRandomString('link'),
|
||||
};
|
||||
Transforms.insertNodes(this.editor, link);
|
||||
requestAnimationFrame(() => {
|
||||
ReactEditor.focus(this.editor);
|
||||
});
|
||||
}
|
||||
|
||||
/** todo improve if selection is collapsed */
|
||||
public getCommentsIdsBySelection() {
|
||||
const commentedTextNodes = Editor.nodes(this.editor, {
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import type { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
services,
|
||||
Protocol,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
supportChildren,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { List } from '../../components/style-container';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { BulletIcon, getChildrenType, NumberType } from './data';
|
||||
import { BulletBlock, BulletProperties } from './types';
|
||||
import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, BulletIcon, NumberType } from './data';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
export const defaultBulletProps: BulletProperties = {
|
||||
text: { value: [{ text: '' }] },
|
||||
@@ -208,9 +207,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
|
||||
</div>
|
||||
</List>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { defaultBulletProps, BulletView } from './BulletView';
|
||||
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import { BulletView, defaultBulletProps } from './BulletView';
|
||||
|
||||
export class BulletBlock extends BaseView {
|
||||
public type = Protocol.Block.Type.bullet;
|
||||
|
||||
|
||||
@@ -1,58 +1,54 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { StyleWithAtRules } from 'style9';
|
||||
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { sql, MySQL, PostgreSQL } from '@codemirror/lang-sql';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { rust } from '@codemirror/lang-rust';
|
||||
import { cpp } from '@codemirror/lang-cpp';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { lezer } from '@codemirror/lang-lezer';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { php } from '@codemirror/lang-php';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { rust } from '@codemirror/lang-rust';
|
||||
import { MySQL, PostgreSQL, sql } from '@codemirror/lang-sql';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { go } from '@codemirror/legacy-modes/mode/go';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { brainfuck } from '@codemirror/legacy-modes/mode/brainfuck';
|
||||
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
|
||||
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
|
||||
import { elixir } from 'codemirror-lang-elixir';
|
||||
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
|
||||
import { perl } from '@codemirror/legacy-modes/mode/perl';
|
||||
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
|
||||
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
|
||||
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
|
||||
import { clojure } from '@codemirror/legacy-modes/mode/clojure';
|
||||
import { coffeeScript } from '@codemirror/legacy-modes/mode/coffeescript';
|
||||
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
|
||||
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
|
||||
import { go } from '@codemirror/legacy-modes/mode/go';
|
||||
import { julia } from '@codemirror/legacy-modes/mode/julia';
|
||||
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
|
||||
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
|
||||
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
|
||||
import { perl } from '@codemirror/legacy-modes/mode/perl';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { r } from '@codemirror/legacy-modes/mode/r';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb';
|
||||
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { Extension } from '@codemirror/state';
|
||||
import { Option, Select } from '@toeverything/components/ui';
|
||||
|
||||
import {
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { DuplicateIcon } from '@toeverything/components/icons';
|
||||
import { Option, Select, styled } from '@toeverything/components/ui';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { elixir } from 'codemirror-lang-elixir';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { StyleWithAtRules } from 'style9';
|
||||
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
|
||||
|
||||
interface CreateCodeView extends CreateView {
|
||||
style9?: StyleWithAtRules;
|
||||
@@ -110,13 +106,15 @@ const CodeBlock = styled('div')(({ theme }) => ({
|
||||
borderRadius: theme.affine.shape.borderRadius,
|
||||
'&:hover': {
|
||||
'.operation': {
|
||||
display: 'flex',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
'.operation': {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
opacity: 0,
|
||||
transition: 'opacity 1.5s',
|
||||
},
|
||||
'.copy-block': {
|
||||
padding: '0px 10px',
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
export class CodeBlock extends BaseView {
|
||||
type = Protocol.Block.Type.code;
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
// View = CodeView;
|
||||
View = CodeView;
|
||||
override async onCreate(block: AsyncBlock): Promise<AsyncBlock> {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
BlockPendantProvider,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { useState } from 'react';
|
||||
import { SourceView } from '../../components/source-view';
|
||||
import { LinkContainer } from '../../components/style-container';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
|
||||
const MESSAGES = {
|
||||
ADD_EMBED_LINK: 'Add embed link',
|
||||
@@ -38,7 +38,6 @@ export const EmbedLinkView = (props: EmbedLinkView) => {
|
||||
{embedLinkUrl ? (
|
||||
<SourceView
|
||||
block={block}
|
||||
editorElement={props.editorElement}
|
||||
isSelected={isSelect}
|
||||
viewType="embedLink"
|
||||
link={embedLinkUrl}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class EmbedLinkBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.embedLink;
|
||||
View = EmbedLinkView;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class FigmaBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.figma;
|
||||
View = FigmaView;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class FileBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.file;
|
||||
View = FileView;
|
||||
override async block2html({ block }: Block2HtmlProps) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RenderBlock } from '@toeverything/components/editor-core';
|
||||
import { useBlockRender } from '@toeverything/components/editor-core';
|
||||
import { ChildrenView, CreateView } from '@toeverything/framework/virgo';
|
||||
|
||||
export const GridItemRender = function (
|
||||
@@ -6,10 +6,11 @@ export const GridItemRender = function (
|
||||
) {
|
||||
const GridItem = function (props: CreateView) {
|
||||
const { block } = props;
|
||||
const { BlockRender } = useBlockRender();
|
||||
const children = (
|
||||
<>
|
||||
{block.childrenIds.map(id => {
|
||||
return <RenderBlock key={id} blockId={id} />;
|
||||
return <BlockRender key={id} blockId={id} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GridItemRender } from './GridItemRender';
|
||||
|
||||
export class GridItemBlock extends BaseView {
|
||||
public override selectable = false;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
public override allowPendant = false;
|
||||
public override layoutOnly = true;
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { RenderBlock } from '@toeverything/components/editor-core';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { GridHandle } from './GirdHandle';
|
||||
import { useBlockRender } from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { debounce, domToRect, Point } from '@toeverything/utils';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
GRID_ITEM_CLASS_NAME,
|
||||
GRID_ITEM_CONTENT_CLASS_NAME,
|
||||
} from '../grid-item/GridItem';
|
||||
import { debounce, domToRect, Point } from '@toeverything/utils';
|
||||
import clsx from 'clsx';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { GridHandle } from './GirdHandle';
|
||||
|
||||
const DB_UPDATE_DELAY = 50;
|
||||
const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag';
|
||||
@@ -31,6 +31,7 @@ export const Grid = function (props: CreateView) {
|
||||
const originalLeftWidth = useRef<number>(gridItemMinWidth);
|
||||
const originalRightWidth = useRef<number>(gridItemMinWidth);
|
||||
const [alertHandleId, setAlertHandleId] = useState<string>(null);
|
||||
const { BlockRender } = useBlockRender();
|
||||
|
||||
const getLeftRightGridItemDomByIndex = (index: number) => {
|
||||
const gridItems = Array.from(gridContainerRef.current?.children).filter(
|
||||
@@ -226,7 +227,7 @@ export const Grid = function (props: CreateView) {
|
||||
key={id}
|
||||
className={GRID_ITEM_CLASS_NAME}
|
||||
>
|
||||
<RenderBlock hasContainer={false} blockId={id} />
|
||||
<BlockRender hasContainer={false} blockId={id} />
|
||||
<GridHandle
|
||||
onDrag={event => handleDragGrid(event, i)}
|
||||
editor={editor}
|
||||
|
||||
@@ -7,7 +7,7 @@ export { GRID_PROPERTY_KEY, removePercent } from './Grid';
|
||||
|
||||
export class GridBlock extends BaseView {
|
||||
public override selectable = false;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
public override allowPendant = false;
|
||||
public override layoutOnly = true;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
export class Group extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
public override allowPendant = false;
|
||||
|
||||
type = Protocol.Block.Type.group;
|
||||
|
||||
@@ -2,5 +2,5 @@ import { RenderBlockChildren } from '@toeverything/components/editor-core';
|
||||
import type { CreateView } from '@toeverything/framework/virgo';
|
||||
|
||||
export const ScenePage = ({ block }: CreateView) => {
|
||||
return <RenderBlockChildren block={block} />;
|
||||
return <RenderBlockChildren block={block} indent={false} />;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import type { KanbanCard } from '@toeverything/components/editor-core';
|
||||
import {
|
||||
RenderBlock,
|
||||
KanbanCard,
|
||||
useBlockRender,
|
||||
useEditor,
|
||||
useKanban,
|
||||
useRefPage,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { PenIcon } from '@toeverything/components/icons';
|
||||
import {
|
||||
IconButton,
|
||||
MuiClickAwayListener,
|
||||
styled,
|
||||
} from '@toeverything/components/ui';
|
||||
import { useFlag } from '@toeverything/datasource/feature-flags';
|
||||
import { useState, type MouseEvent } from 'react';
|
||||
import { useRefPage } from './RefPage';
|
||||
|
||||
const CardContent = styled('div')({
|
||||
margin: '20px',
|
||||
@@ -23,6 +30,7 @@ const CardActions = styled('div')({
|
||||
fontWeight: '300',
|
||||
color: '#98ACBD',
|
||||
transition: 'all ease-in 0.2s',
|
||||
zIndex: 1,
|
||||
|
||||
':hover': {
|
||||
background: '#F5F7F8',
|
||||
@@ -39,11 +47,13 @@ const PlusIcon = styled('div')({
|
||||
});
|
||||
|
||||
const CardContainer = styled('div')({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #E2E7ED',
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
|
||||
[CardActions.toString()]: {
|
||||
opacity: '0',
|
||||
@@ -55,6 +65,23 @@ const CardContainer = styled('div')({
|
||||
},
|
||||
});
|
||||
|
||||
const Overlay = styled('div')({
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'transparent',
|
||||
|
||||
'& > *': {
|
||||
visibility: 'hidden',
|
||||
position: 'absolute',
|
||||
right: '24px',
|
||||
top: '16px',
|
||||
},
|
||||
'&:hover > *': {
|
||||
visibility: 'visible',
|
||||
},
|
||||
});
|
||||
|
||||
export const CardItem = ({
|
||||
id,
|
||||
block,
|
||||
@@ -64,24 +91,44 @@ export const CardItem = ({
|
||||
}) => {
|
||||
const { addSubItem } = useKanban();
|
||||
const { openSubPage } = useRefPage();
|
||||
const [editable, setEditable] = useState(false);
|
||||
const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false);
|
||||
const { editor } = useEditor();
|
||||
const { BlockRender } = useBlockRender();
|
||||
|
||||
const onAddItem = async () => {
|
||||
setEditable(true);
|
||||
await addSubItem(block);
|
||||
};
|
||||
|
||||
const onClickCard = async () => {
|
||||
showKanbanRefPageFlag && openSubPage(id);
|
||||
openSubPage(id);
|
||||
};
|
||||
|
||||
const onClickPen = (e: MouseEvent<Element>) => {
|
||||
e.stopPropagation();
|
||||
setEditable(true);
|
||||
editor.selectionManager.activeNodeByNodeId(block.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<CardContainer onClick={onClickCard}>
|
||||
<CardContent>
|
||||
<RenderBlock blockId={id} />
|
||||
</CardContent>
|
||||
<CardActions onClick={onAddItem}>
|
||||
<PlusIcon />
|
||||
<span>Add a sub-block</span>
|
||||
</CardActions>
|
||||
</CardContainer>
|
||||
<MuiClickAwayListener onClickAway={() => setEditable(false)}>
|
||||
<CardContainer>
|
||||
<CardContent>
|
||||
<BlockRender blockId={id} />
|
||||
</CardContent>
|
||||
{showKanbanRefPageFlag && !editable && (
|
||||
<Overlay onClick={onClickCard}>
|
||||
<IconButton backgroundColor="#fff" onClick={onClickPen}>
|
||||
<PenIcon />
|
||||
</IconButton>
|
||||
</Overlay>
|
||||
)}
|
||||
<CardActions onClick={onAddItem}>
|
||||
<PlusIcon />
|
||||
<span>Add a sub-block</span>
|
||||
</CardActions>
|
||||
</CardContainer>
|
||||
</MuiClickAwayListener>
|
||||
);
|
||||
};
|
||||
|
||||
+15
-3
@@ -1,7 +1,7 @@
|
||||
import { useEditor } from '@toeverything/components/editor-core';
|
||||
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,
|
||||
@@ -30,7 +30,7 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
|
||||
onClick={closeSubPage}
|
||||
>
|
||||
<Dialog
|
||||
onClick={e => {
|
||||
onClick={(e: { stopPropagation: () => void }) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
@@ -43,9 +43,21 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
|
||||
};
|
||||
|
||||
const ModalPage = ({ blockId }: { blockId: string | null }) => {
|
||||
const { editor, editorElement } = useEditor();
|
||||
|
||||
const AffineEditor = editorElement as any;
|
||||
|
||||
return (
|
||||
<Modal open={!!blockId}>
|
||||
{blockId && <RenderBlock blockId={blockId} />}
|
||||
{blockId && (
|
||||
<AffineEditor
|
||||
workspace={editor.workspace}
|
||||
rootBlockId={blockId}
|
||||
scrollBlank={false}
|
||||
// use edgeless mode prevent padding and blank bottom
|
||||
isEdgeless
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -4,30 +4,33 @@ import { SceneKanbanContext } from './context';
|
||||
import { CardContainerWrapper } from './dndable/wrapper/CardContainerWrapper';
|
||||
import type { ComponentType } from 'react';
|
||||
import type { CreateView } from '@toeverything/framework/virgo';
|
||||
import { RefPageProvider } from './RefPage';
|
||||
|
||||
export const SceneKanban: ComponentType<CreateView> = withKanban<CreateView>(
|
||||
({ editor, block }) => {
|
||||
const { kanban } = useKanban();
|
||||
|
||||
return (
|
||||
<SceneKanbanContext.Provider value={{ editor, block }}>
|
||||
<CardContainerWrapper
|
||||
dataSource={kanban}
|
||||
render={({
|
||||
activeId,
|
||||
items,
|
||||
containerIds,
|
||||
isSortingContainer,
|
||||
}) => (
|
||||
<CardContainer
|
||||
activeId={activeId}
|
||||
items={items}
|
||||
isSortingContainer={isSortingContainer}
|
||||
containerIds={containerIds}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</SceneKanbanContext.Provider>
|
||||
<RefPageProvider>
|
||||
<SceneKanbanContext.Provider value={{ editor, block }}>
|
||||
<CardContainerWrapper
|
||||
dataSource={kanban}
|
||||
render={({
|
||||
activeId,
|
||||
items,
|
||||
containerIds,
|
||||
isSortingContainer,
|
||||
}) => (
|
||||
<CardContainer
|
||||
activeId={activeId}
|
||||
items={items}
|
||||
isSortingContainer={isSortingContainer}
|
||||
containerIds={containerIds}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</SceneKanbanContext.Provider>
|
||||
</RefPageProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getRandomString } from '@toeverything/components/common';
|
||||
|
||||
export class ImageBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.image;
|
||||
View = ImageView;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
services,
|
||||
Protocol,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
@@ -11,18 +11,17 @@ import {
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import type { Numbered, NumberedAsyncBlock } from './types';
|
||||
|
||||
import { getChildrenType, getNumber } from './data';
|
||||
import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
supportChildren,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, getNumber } from './data';
|
||||
|
||||
export const defaultTodoProps: Numbered = {
|
||||
text: { value: [{ text: '' }] },
|
||||
@@ -204,9 +203,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
|
||||
</List>
|
||||
</BlockPendantProvider>
|
||||
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { defaultTodoProps, NumberedView } from './NumberedView';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import { defaultTodoProps, NumberedView } from './NumberedView';
|
||||
|
||||
export class NumberedBlock extends BaseView {
|
||||
public type = Protocol.Block.Type.numbered;
|
||||
|
||||
@@ -12,7 +12,6 @@ import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import { TextManage } from '../../components/text-manage';
|
||||
import { dedentBlock, tabBlock } from '../../utils/indent';
|
||||
interface CreateTextView extends CreateView {
|
||||
@@ -255,9 +254,7 @@ export const TextView = ({
|
||||
handleTab={onTab}
|
||||
/>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BlockPendantProvider,
|
||||
CreateView,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useRef } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
@@ -36,6 +42,10 @@ const todoIsEmpty = (contentValue: ContentColumnValue): boolean => {
|
||||
export const TodoView = ({ block, editor }: CreateView) => {
|
||||
const properties = { ...defaultTodoProps, ...block.getProperties() };
|
||||
const text_ref = useRef<ExtendedTextUtils>(null);
|
||||
const [isSelect, setIsSelect] = useState<boolean>(false);
|
||||
useOnSelect(block.id, (isSelect: boolean) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
|
||||
const turn_into_text_block = async () => {
|
||||
// Convert to text block
|
||||
@@ -121,28 +131,34 @@ export const TodoView = ({ block, editor }: CreateView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={properties.checked?.value ? 'checked' : ''}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={
|
||||
properties.checked?.value ? 'checked' : ''
|
||||
}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
</BlockPendantProvider>
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
withTreeViewChildren,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { withTreeViewChildren } from '../../utils/WithTreeViewChildren';
|
||||
import { TodoView, defaultTodoProps } from './TodoView';
|
||||
import type { TodoAsyncBlock } from './types';
|
||||
import { defaultTodoProps, TodoView } from './TodoView';
|
||||
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import type { TodoAsyncBlock } from './types';
|
||||
|
||||
export class TodoBlock extends BaseView {
|
||||
type = Protocol.Block.Type.todo;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
export class YoutubeBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.youtube;
|
||||
View = YoutubeView;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const BlockContainer = function ({
|
||||
);
|
||||
};
|
||||
|
||||
export const Container = styled('div')<{ selected: boolean }>(
|
||||
export const Container = styled('div')<{ selected?: boolean }>(
|
||||
({ selected, theme }) => ({
|
||||
backgroundColor: selected ? theme.affine.palette.textSelected : '',
|
||||
marginBottom: '2px',
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { ChildrenView } from '@toeverything/framework/virgo';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
/**
|
||||
* Indent rendering child nodes
|
||||
*/
|
||||
export const IndentWrapper = (props: PropsWithChildren) => {
|
||||
return <StyledIdentWrapper>{props.children}</StyledIdentWrapper>;
|
||||
};
|
||||
|
||||
const StyledIdentWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// TODO: marginLeft should use theme provided by styled
|
||||
marginLeft: '30px',
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from './IndentWrapper';
|
||||
@@ -1,9 +1,9 @@
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { StyledBlockPreview } from '@toeverything/components/common';
|
||||
import { AsyncBlock, useEditor } from '@toeverything/components/editor-core';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock } from '@toeverything/framework/virgo';
|
||||
import { debounce, sleep } from '@toeverything/utils';
|
||||
|
||||
const updateTitle = async (
|
||||
@@ -73,15 +73,15 @@ const useBlockTitle = (block: AsyncBlock, blockId: string) => {
|
||||
type BlockPreviewProps = {
|
||||
block: AsyncBlock;
|
||||
blockId: string;
|
||||
editorElement?: () => JSX.Element;
|
||||
};
|
||||
|
||||
const InternalBlockPreview = (props: BlockPreviewProps) => {
|
||||
const container = useRef<HTMLDivElement>();
|
||||
const [preview, setPreview] = useState(true);
|
||||
const title = useBlockTitle(props.block, props.blockId);
|
||||
const { editorElement } = useEditor();
|
||||
|
||||
const AffineEditor = props.editorElement as any;
|
||||
const AffineEditor = editorElement as any;
|
||||
|
||||
useEffect(() => {
|
||||
if (container?.current) {
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
useCurrentView,
|
||||
useLazyIframe,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { ReactElement, ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { SCENE_CONFIG } from '../../blocks/group/config';
|
||||
import { BlockPreview } from './BlockView';
|
||||
import { formatUrl } from './format-url';
|
||||
|
||||
export interface Props {
|
||||
block: AsyncBlock;
|
||||
editorElement?: () => JSX.Element;
|
||||
viewType?: string;
|
||||
link: string;
|
||||
// onResizeEnd: (data: any) => void;
|
||||
@@ -150,7 +148,7 @@ const LoadingContiner = () => {
|
||||
};
|
||||
|
||||
export const SourceView = (props: Props) => {
|
||||
const { link, isSelected, block, editorElement } = props;
|
||||
const { link, isSelected, block } = props;
|
||||
const src = formatUrl(link);
|
||||
// let iframeShow = useLazyIframe(src, 3000, iframeContainer);
|
||||
const [currentView] = useCurrentView();
|
||||
@@ -161,10 +159,7 @@ export const SourceView = (props: Props) => {
|
||||
<SourceViewContainer isSelected={isSelected} scene={type}>
|
||||
<MouseMaskContainer />
|
||||
|
||||
<LazyIframe
|
||||
src={src}
|
||||
fallback={LoadingContiner()}
|
||||
></LazyIframe>
|
||||
<LazyIframe src={src} fallback={LoadingContiner()} />
|
||||
</SourceViewContainer>
|
||||
</div>
|
||||
);
|
||||
@@ -175,11 +170,7 @@ export const SourceView = (props: Props) => {
|
||||
style={{ padding: '0' }}
|
||||
scene={type}
|
||||
>
|
||||
<BlockPreview
|
||||
block={block}
|
||||
editorElement={editorElement}
|
||||
blockId={src}
|
||||
/>
|
||||
<BlockPreview block={block} blockId={src} />
|
||||
</SourceViewContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const isYoutubeUrl = (url?: string): boolean => {
|
||||
if (!url) return false;
|
||||
const allowedHosts = ['www.youtu.be', 'www.youtube.com'];
|
||||
const host = new URL(url).host;
|
||||
return allowedHosts.includes(host);
|
||||
|
||||
@@ -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 +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',
|
||||
});
|
||||
+27
-82
@@ -1,10 +1,3 @@
|
||||
import {
|
||||
BlockPendantProvider,
|
||||
CreateView,
|
||||
RenderBlock,
|
||||
useCurrentView,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type {
|
||||
ComponentPropsWithoutRef,
|
||||
@@ -12,9 +5,10 @@ import type {
|
||||
CSSProperties,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { SCENE_CONFIG } from '../blocks/group/config';
|
||||
import { BlockContainer } from '../components/BlockContainer';
|
||||
import { forwardRef } from 'react';
|
||||
import { CreateView } from '../editor';
|
||||
import { useBlockRender } from './Context';
|
||||
import { NullBlockRender } from './RenderBlock';
|
||||
|
||||
type WithChildrenConfig = {
|
||||
indent: CSSProperties['marginLeft'];
|
||||
@@ -41,45 +35,6 @@ const TreeView = forwardRef<
|
||||
);
|
||||
});
|
||||
|
||||
interface ChildrenViewProp {
|
||||
childrenIds: string[];
|
||||
handleCollapse: () => void;
|
||||
indent?: string | number;
|
||||
}
|
||||
|
||||
const ChildrenView = ({
|
||||
childrenIds,
|
||||
handleCollapse,
|
||||
indent,
|
||||
}: ChildrenViewProp) => {
|
||||
const [currentView] = useCurrentView();
|
||||
const isKanbanScene = currentView.type === SCENE_CONFIG.KANBAN;
|
||||
|
||||
return (
|
||||
<Children style={{ ...(!isKanbanScene && { marginLeft: indent }) }}>
|
||||
{childrenIds.map((childId, idx) => {
|
||||
if (isKanbanScene) {
|
||||
return (
|
||||
<StyledBorder key={childId}>
|
||||
<RenderBlock blockId={childId} />
|
||||
</StyledBorder>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
key={childId}
|
||||
lastItem={idx === childrenIds.length - 1}
|
||||
onClick={handleCollapse}
|
||||
>
|
||||
<RenderBlock key={childId} blockId={childId} />
|
||||
</TreeView>
|
||||
);
|
||||
})}
|
||||
</Children>
|
||||
);
|
||||
};
|
||||
|
||||
const CollapsedNode = forwardRef<
|
||||
HTMLDivElement,
|
||||
ComponentPropsWithoutRef<'div'>
|
||||
@@ -104,15 +59,15 @@ export const withTreeViewChildren = (
|
||||
};
|
||||
|
||||
return (props: CreateView) => {
|
||||
const { block, editor } = props;
|
||||
const { block } = props;
|
||||
const { BlockRender } = useBlockRender();
|
||||
const collapsed = block.getProperty('collapsed')?.value;
|
||||
const childrenIds = block.childrenIds;
|
||||
const showChildren = !collapsed && childrenIds.length > 0;
|
||||
const showChildren =
|
||||
!collapsed &&
|
||||
childrenIds.length > 0 &&
|
||||
BlockRender !== NullBlockRender;
|
||||
|
||||
const [isSelect, setIsSelect] = useState<boolean>(false);
|
||||
useOnSelect(block.id, (isSelect: boolean) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
const handleCollapse = () => {
|
||||
block.setProperty('collapsed', { value: true });
|
||||
};
|
||||
@@ -122,15 +77,8 @@ export const withTreeViewChildren = (
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockContainer
|
||||
editor={props.editor}
|
||||
block={block}
|
||||
selected={isSelect}
|
||||
className={Wrapper.toString()}
|
||||
>
|
||||
<BlockPendantProvider block={block}>
|
||||
<div>{creator(props)}</div>
|
||||
</BlockPendantProvider>
|
||||
<>
|
||||
{creator(props)}
|
||||
|
||||
{collapsed && (
|
||||
<CollapsedNode
|
||||
@@ -138,22 +86,24 @@ export const withTreeViewChildren = (
|
||||
style={{ marginLeft: config.indent }}
|
||||
/>
|
||||
)}
|
||||
{showChildren && (
|
||||
<ChildrenView
|
||||
childrenIds={childrenIds}
|
||||
handleCollapse={handleCollapse}
|
||||
indent={config.indent}
|
||||
/>
|
||||
)}
|
||||
</BlockContainer>
|
||||
{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 Wrapper = styled('div')({ display: 'flex', flexDirection: 'column' });
|
||||
|
||||
const Children = Wrapper;
|
||||
|
||||
const TREE_COLOR = '#D5DFE6';
|
||||
// adjust left and right margins of the the tree line
|
||||
const TREE_LINE_LEFT_OFFSET = '-16px';
|
||||
@@ -163,6 +113,7 @@ const TREE_LINE_WIDTH = '12px';
|
||||
|
||||
const TreeWrapper = styled('div')({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const StyledTreeView = styled('div')({
|
||||
@@ -228,9 +179,3 @@ const LastItemRadius = styled('div')({
|
||||
borderRadius: '0 0 0 3px',
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
const StyledBorder = styled('div')({
|
||||
border: '1px solid #E0E6EB',
|
||||
borderRadius: '5px',
|
||||
margin: '4px',
|
||||
});
|
||||
@@ -1,2 +1,4 @@
|
||||
export { BlockRenderProvider, useBlockRender } from './Context';
|
||||
export * from './RenderBlock';
|
||||
export * from './RenderBlockChildren';
|
||||
export { withTreeViewChildren } from './WithTreeViewChildren';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { PluginCreator } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
LeftMenuPlugin,
|
||||
InlineMenuPlugin,
|
||||
CommandMenuPlugin,
|
||||
ReferenceMenuPlugin,
|
||||
SelectionGroupPlugin,
|
||||
GroupMenuPlugin,
|
||||
} from './menu';
|
||||
import { TemplatePlugin } from './template';
|
||||
import { FullTextSearchPlugin } from './search';
|
||||
import { AddCommentPlugin } from './comment';
|
||||
import {
|
||||
CommandMenuPlugin,
|
||||
DoubleLinkMenuPlugin,
|
||||
GroupMenuPlugin,
|
||||
InlineMenuPlugin,
|
||||
LeftMenuPlugin,
|
||||
SelectionGroupPlugin,
|
||||
} from './menu';
|
||||
import { FullTextSearchPlugin } from './search';
|
||||
import { TemplatePlugin } from './template';
|
||||
// import { PlaceholderPlugin } from './placeholder';
|
||||
|
||||
// import { BlockPropertyPlugin } from './block-property';
|
||||
@@ -19,9 +19,9 @@ export const plugins: PluginCreator[] = [
|
||||
LeftMenuPlugin,
|
||||
InlineMenuPlugin,
|
||||
CommandMenuPlugin,
|
||||
ReferenceMenuPlugin,
|
||||
DoubleLinkMenuPlugin,
|
||||
TemplatePlugin,
|
||||
SelectionGroupPlugin,
|
||||
// SelectionGroupPlugin,
|
||||
AddCommentPlugin,
|
||||
GroupMenuPlugin,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { CommonList, CommonListItem } from '@toeverything/components/common';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type DoubleLinkMenuContainerProps = {
|
||||
editor: Virgo;
|
||||
hooks: PluginHooks;
|
||||
style?: React.CSSProperties;
|
||||
blockId: string;
|
||||
onSelected?: (item: string) => void;
|
||||
onClose?: () => void;
|
||||
types?: Array<string>;
|
||||
items?: CommonListItem[];
|
||||
};
|
||||
|
||||
export const DoubleLinkMenuContainer = (
|
||||
props: DoubleLinkMenuContainerProps
|
||||
) => {
|
||||
const { hooks, onSelected, onClose, types, style, items } = props;
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [currentItem, setCurrentItem] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (types && !currentItem) {
|
||||
setCurrentItem(types[0]);
|
||||
}
|
||||
}, [currentItem, onClose, types]);
|
||||
|
||||
const handleUpDownKey = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (types && ['ArrowUp', 'ArrowDown'].includes(event.code)) {
|
||||
event.preventDefault();
|
||||
const isUpkey = event.code === 'ArrowUp';
|
||||
if (!currentItem && types.length) {
|
||||
setCurrentItem(types[isUpkey ? types.length - 1 : 0]);
|
||||
}
|
||||
if (currentItem) {
|
||||
const idx = types.indexOf(currentItem);
|
||||
if (isUpkey ? idx > 0 : idx < types.length - 1) {
|
||||
setCurrentItem(types[isUpkey ? idx - 1 : idx + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[currentItem, types]
|
||||
);
|
||||
const handleClickUp = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
handleUpDownKey(event);
|
||||
},
|
||||
[handleUpDownKey]
|
||||
);
|
||||
|
||||
const handleClickDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
handleUpDownKey(event);
|
||||
},
|
||||
[handleUpDownKey]
|
||||
);
|
||||
|
||||
const handleClickEnter = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.code === 'Enter' && currentItem) {
|
||||
event.preventDefault();
|
||||
onSelected && onSelected(currentItem);
|
||||
}
|
||||
},
|
||||
[currentItem, onSelected]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
handleClickUp(event);
|
||||
handleClickDown(event);
|
||||
handleClickEnter(event);
|
||||
},
|
||||
[handleClickUp, handleClickDown, handleClickEnter]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = hooks
|
||||
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
|
||||
.subscribe(handleKeyDown);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}, [hooks, handleKeyDown]);
|
||||
|
||||
return (
|
||||
<RootContainer
|
||||
ref={menuRef}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
style={style}
|
||||
>
|
||||
<ContentContainer>
|
||||
<CommonList
|
||||
items={items}
|
||||
onSelected={type => onSelected?.(type)}
|
||||
currentItem={currentItem}
|
||||
setCurrentItem={setCurrentItem}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</RootContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const RootContainer = styled('div')(({ theme }) => ({
|
||||
zIndex: 1,
|
||||
borderRadius: '10px',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
backgroundColor: '#fff',
|
||||
padding: '8px 4px',
|
||||
}));
|
||||
|
||||
const ContentContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
maxHeight: '300px',
|
||||
}));
|
||||
@@ -0,0 +1,376 @@
|
||||
import { CommonListItem } from '@toeverything/components/common';
|
||||
import { AddIcon } from '@toeverything/components/icons';
|
||||
import {
|
||||
Input,
|
||||
ListButton,
|
||||
MuiClickAwayListener,
|
||||
MuiGrow as Grow,
|
||||
MuiPaper as Paper,
|
||||
MuiPopper as Popper,
|
||||
styled,
|
||||
} from '@toeverything/components/ui';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo';
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { QueryBlocks, QueryResult } from '../../search';
|
||||
import { DoubleLinkMenuContainer } from './Container';
|
||||
|
||||
const ADD_NEW_SUB_PAGE = 'AddNewSubPage';
|
||||
const ADD_NEW_PAGE = 'AddNewPage';
|
||||
const ARRAY_CODES = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'];
|
||||
|
||||
export type DoubleLinkMenuProps = {
|
||||
editor: Virgo;
|
||||
hooks: PluginHooks;
|
||||
style?: { left: number; top: number };
|
||||
};
|
||||
|
||||
type DoubleLinkMenuStyle = {
|
||||
left: number;
|
||||
top: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export const DoubleLinkMenu = ({
|
||||
editor,
|
||||
hooks,
|
||||
style,
|
||||
}: DoubleLinkMenuProps) => {
|
||||
const { page_id: curPageId } = useParams();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const dialogRef = useRef<HTMLDivElement>();
|
||||
const newPageSearchRef = useRef<HTMLInputElement>();
|
||||
const [doubleLinkMenuStyle, setDoubleLinkMenuStyle] =
|
||||
useState<DoubleLinkMenuStyle>({
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
const [curBlockId, setCurBlockId] = useState<string>();
|
||||
const [searchText, setSearchText] = useState<string>();
|
||||
const [inAddNewPage, setInAddNewPage] = useState(false);
|
||||
const [filterText, setFilterText] = useState<string>('');
|
||||
const [searchResultBlocks, setSearchResultBlocks] = useState<QueryResult>(
|
||||
[]
|
||||
);
|
||||
|
||||
const menuTypes = useMemo(() => {
|
||||
return Object.values(searchResultBlocks)
|
||||
.map(({ id }) => id)
|
||||
.concat([ADD_NEW_SUB_PAGE, ADD_NEW_PAGE]);
|
||||
}, [searchResultBlocks]);
|
||||
|
||||
const menuItems: CommonListItem[] = useMemo(() => {
|
||||
const items: CommonListItem[] = [];
|
||||
if (searchResultBlocks?.length > 0) {
|
||||
items.push({
|
||||
renderCustom: () => {
|
||||
return (
|
||||
<ListButton
|
||||
content={
|
||||
inAddNewPage ? 'SUGGESTED' : 'LINK TO PAGE'
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
items.push(
|
||||
...(searchResultBlocks?.map(
|
||||
block =>
|
||||
({
|
||||
block: {
|
||||
...block,
|
||||
content: block.content || 'Untitled',
|
||||
},
|
||||
} as CommonListItem)
|
||||
) || [])
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
items.push({ divider: 'newPage' });
|
||||
}
|
||||
items.push({
|
||||
content: {
|
||||
id: ADD_NEW_SUB_PAGE,
|
||||
content: 'Add new sub-page',
|
||||
icon: AddIcon,
|
||||
},
|
||||
});
|
||||
!inAddNewPage &&
|
||||
items.push({
|
||||
content: {
|
||||
id: ADD_NEW_PAGE,
|
||||
content: 'Add new page in...',
|
||||
icon: AddIcon,
|
||||
},
|
||||
});
|
||||
return items;
|
||||
}, [searchResultBlocks, inAddNewPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const text = inAddNewPage ? filterText : searchText;
|
||||
QueryBlocks(editor, text, result => {
|
||||
if (!inAddNewPage) {
|
||||
result = result.filter(item => item.id !== curPageId);
|
||||
}
|
||||
setSearchResultBlocks(result);
|
||||
});
|
||||
}, [editor, searchText, filterText, inAddNewPage, curPageId]);
|
||||
|
||||
const hideMenu = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
setInAddNewPage(false);
|
||||
editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId);
|
||||
editor.scrollManager.unLock();
|
||||
}, [curBlockId, editor]);
|
||||
|
||||
const resetState = useCallback(
|
||||
(preNodeId: string, nextNodeId: string) => {
|
||||
editor.blockHelper.removeDoubleLinkSearchSlash(preNodeId);
|
||||
setCurBlockId(nextNodeId);
|
||||
setSearchText('');
|
||||
setIsOpen(true);
|
||||
editor.scrollManager.lock();
|
||||
const clientRect =
|
||||
editor.selection.currentSelectInfo?.browserSelection
|
||||
?.getRangeAt(0)
|
||||
?.getBoundingClientRect();
|
||||
if (clientRect) {
|
||||
const rectTop = clientRect.top;
|
||||
const { top, left } = editor.container.getBoundingClientRect();
|
||||
setDoubleLinkMenuStyle({
|
||||
top: rectTop - top,
|
||||
left: clientRect.left - left,
|
||||
height: clientRect.height,
|
||||
});
|
||||
setAnchorEl(dialogRef.current);
|
||||
}
|
||||
const textSelection = editor.blockHelper.selectionToSlateRange(
|
||||
nextNodeId,
|
||||
editor.selection.currentSelectInfo?.browserSelection
|
||||
);
|
||||
if (textSelection) {
|
||||
const { anchor } = textSelection;
|
||||
editor.blockHelper.setDoubleLinkSearchSlash(nextNodeId, anchor);
|
||||
}
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const searchChange = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (ARRAY_CODES.includes(event.code)) {
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Backspace') {
|
||||
const searchText =
|
||||
editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId);
|
||||
if (!searchText || searchText === '[[') {
|
||||
hideMenu();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { type, anchorNode } = editor.selection.currentSelectInfo;
|
||||
if (
|
||||
!isOpen ||
|
||||
(type === 'Range' &&
|
||||
anchorNode &&
|
||||
anchorNode.id !== curBlockId &&
|
||||
editor.blockHelper.isSelectionCollapsed(anchorNode.id))
|
||||
) {
|
||||
const text = editor.blockHelper.getBlockTextBeforeSelection(
|
||||
anchorNode.id
|
||||
);
|
||||
if (text.endsWith('[[')) {
|
||||
resetState(curBlockId, anchorNode.id);
|
||||
}
|
||||
}
|
||||
if (isOpen) {
|
||||
const searchText =
|
||||
editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId);
|
||||
if (searchText && searchText.startsWith('[[')) {
|
||||
setSearchText(searchText.slice(2).trim());
|
||||
}
|
||||
}
|
||||
},
|
||||
[editor, isOpen, curBlockId, hideMenu]
|
||||
);
|
||||
|
||||
const handleKeyup = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => searchChange(event),
|
||||
[searchChange]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Escape') {
|
||||
hideMenu();
|
||||
}
|
||||
|
||||
if (event.code === 'Backspace') {
|
||||
const searchText =
|
||||
editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId);
|
||||
if (!searchText || searchText === '[[') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
[hideMenu, editor, isOpen, curBlockId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = hooks
|
||||
.get(HookType.ON_ROOT_NODE_KEYUP)
|
||||
.subscribe(handleKeyup);
|
||||
sub.add(
|
||||
hooks
|
||||
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
|
||||
.subscribe(handleKeyDown)
|
||||
);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}, [handleKeyup, handleKeyDown, hooks]);
|
||||
|
||||
const insertDoubleLink = useCallback(
|
||||
async (pageId: string) => {
|
||||
editor.blockHelper.setSelectDoubleLinkSearchSlash(curBlockId);
|
||||
await editor.blockHelper.insertDoubleLink(
|
||||
editor.workspace,
|
||||
pageId,
|
||||
curBlockId
|
||||
);
|
||||
hideMenu();
|
||||
},
|
||||
[editor, curBlockId, hideMenu]
|
||||
);
|
||||
|
||||
const addSubPage = useCallback(
|
||||
async (parentPageId: string) => {
|
||||
const newPage = await services.api.editorBlock.create({
|
||||
workspace: editor.workspace,
|
||||
type: 'page' as const,
|
||||
});
|
||||
services.api.editorBlock.update({
|
||||
id: newPage.id,
|
||||
workspace: editor.workspace,
|
||||
properties: {
|
||||
text: { value: [{ text: searchText }] },
|
||||
},
|
||||
});
|
||||
await services.api.pageTree.addChildPageToWorkspace(
|
||||
editor.workspace,
|
||||
parentPageId,
|
||||
newPage.id
|
||||
);
|
||||
return newPage.id;
|
||||
},
|
||||
[searchText, editor]
|
||||
);
|
||||
|
||||
const handleSelected = async (id: string) => {
|
||||
if (curBlockId) {
|
||||
if (id === ADD_NEW_PAGE) {
|
||||
setInAddNewPage(true);
|
||||
setTimeout(() => {
|
||||
newPageSearchRef.current?.focus();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (id === ADD_NEW_SUB_PAGE) {
|
||||
const pageId = await addSubPage(curPageId);
|
||||
insertDoubleLink(pageId);
|
||||
return;
|
||||
}
|
||||
if (inAddNewPage) {
|
||||
const pageId = await addSubPage(id);
|
||||
insertDoubleLink(pageId);
|
||||
} else {
|
||||
insertDoubleLink(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value;
|
||||
|
||||
await setFilterText(text);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dialogRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '10px',
|
||||
...doubleLinkMenuStyle,
|
||||
}}
|
||||
>
|
||||
<MuiClickAwayListener onClickAway={() => hideMenu()}>
|
||||
<Popper
|
||||
open={isOpen}
|
||||
anchorEl={anchorEl}
|
||||
transition
|
||||
placement="bottom-start"
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: 'left bottom',
|
||||
}}
|
||||
>
|
||||
<Paper>
|
||||
{inAddNewPage && (
|
||||
<NewPageSearchContainer>
|
||||
<Input
|
||||
ref={newPageSearchRef}
|
||||
value={filterText}
|
||||
onChange={handleFilterChange}
|
||||
placeholder="Search page to add to..."
|
||||
/>
|
||||
</NewPageSearchContainer>
|
||||
)}
|
||||
<DoubleLinkMenuContainer
|
||||
editor={editor}
|
||||
hooks={hooks}
|
||||
style={style}
|
||||
blockId={curBlockId}
|
||||
onSelected={handleSelected}
|
||||
onClose={hideMenu}
|
||||
items={menuItems}
|
||||
types={menuTypes}
|
||||
/>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</MuiClickAwayListener>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const NewPageSearchContainer = styled('div')({
|
||||
padding: '8px 8px 0px 8px',
|
||||
});
|
||||
+3
-4
@@ -1,12 +1,11 @@
|
||||
import { StrictMode } from 'react';
|
||||
|
||||
import { BasePlugin } from '../../base-plugin';
|
||||
import { PluginRenderRoot } from '../../utils';
|
||||
import { ReferenceMenu } from './ReferenceMenu';
|
||||
import { DoubleLinkMenu } from './DoubleLinkMenu';
|
||||
|
||||
const PLUGIN_NAME = 'reference-menu';
|
||||
|
||||
export class ReferenceMenuPlugin extends BasePlugin {
|
||||
export class DoubleLinkMenuPlugin extends BasePlugin {
|
||||
private _root?: PluginRenderRoot;
|
||||
|
||||
public static override get pluginName(): string {
|
||||
@@ -22,7 +21,7 @@ export class ReferenceMenuPlugin extends BasePlugin {
|
||||
|
||||
this._root?.render(
|
||||
<StrictMode>
|
||||
<ReferenceMenu editor={this.editor} hooks={this.hooks} />
|
||||
<DoubleLinkMenu editor={this.editor} hooks={this.hooks} />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DoubleLinkMenuPlugin } from './Plugin';
|
||||
@@ -1,9 +1,7 @@
|
||||
export { CommandMenuPlugin } from './command-menu';
|
||||
export { DoubleLinkMenuPlugin } from './double-link-menu';
|
||||
export { GroupMenuPlugin } from './group-menu';
|
||||
export { InlineMenuPlugin } from './inline-menu';
|
||||
export { LeftMenuPlugin } from './left-menu/LeftMenuPlugin';
|
||||
export { CommandMenuPlugin } from './command-menu';
|
||||
export { ReferenceMenuPlugin } from './reference-menu';
|
||||
export { SelectionGroupPlugin } from './selection-group-menu';
|
||||
|
||||
export { MENU_WIDTH as menuWidth } from './left-menu/menu-config';
|
||||
|
||||
export { GroupMenuPlugin } from './group-menu';
|
||||
export { SelectionGroupPlugin } from './selection-group-menu';
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
|
||||
import { Virgo, PluginHooks, HookType } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
CommonList,
|
||||
CommonListItem,
|
||||
commonListContainer,
|
||||
} from '@toeverything/components/common';
|
||||
import { domToRect } from '@toeverything/utils';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
import { QueryResult } from '../../search';
|
||||
|
||||
export type ReferenceMenuContainerProps = {
|
||||
editor: Virgo;
|
||||
hooks: PluginHooks;
|
||||
style?: React.CSSProperties;
|
||||
isShow?: boolean;
|
||||
blockId: string;
|
||||
onSelected?: (item: string) => void;
|
||||
onClose?: () => void;
|
||||
searchBlocks?: QueryResult;
|
||||
types?: Array<string>;
|
||||
};
|
||||
|
||||
export const ReferenceMenuContainer = ({
|
||||
hooks,
|
||||
isShow = false,
|
||||
onSelected,
|
||||
onClose,
|
||||
types,
|
||||
searchBlocks,
|
||||
style,
|
||||
}: ReferenceMenuContainerProps) => {
|
||||
const menu_ref = useRef<HTMLDivElement>(null);
|
||||
const [current_item, set_current_item] = useState<string | undefined>();
|
||||
const [need_check_into_view, set_need_check_into_view] =
|
||||
useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (need_check_into_view) {
|
||||
if (current_item && menu_ref.current) {
|
||||
const item_ele =
|
||||
menu_ref.current.querySelector<HTMLButtonElement>(
|
||||
`.item-${current_item}`
|
||||
);
|
||||
const scroll_ele =
|
||||
menu_ref.current.querySelector<HTMLButtonElement>(
|
||||
`.${commonListContainer}`
|
||||
);
|
||||
if (item_ele) {
|
||||
const itemRect = domToRect(item_ele);
|
||||
const scrollRect = domToRect(scroll_ele);
|
||||
if (
|
||||
itemRect.top < scrollRect.top ||
|
||||
itemRect.bottom > scrollRect.bottom
|
||||
) {
|
||||
// IMP: may be do it with self function
|
||||
item_ele.scrollIntoView({
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
set_need_check_into_view(false);
|
||||
}
|
||||
}, [need_check_into_view, current_item]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isShow && types && !current_item) set_current_item(types[0]);
|
||||
if (!isShow) onClose?.();
|
||||
}, [current_item, isShow, onClose, types]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isShow && types) {
|
||||
if (!types.includes(current_item)) {
|
||||
set_need_check_into_view(true);
|
||||
if (types.length) {
|
||||
set_current_item(types[0]);
|
||||
} else {
|
||||
set_current_item(undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isShow, types, current_item]);
|
||||
|
||||
const handle_click_up = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (isShow && types && event.code === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
if (!current_item && types.length) {
|
||||
set_current_item(types[types.length - 1]);
|
||||
}
|
||||
if (current_item) {
|
||||
const idx = types.indexOf(current_item);
|
||||
if (idx > 0) {
|
||||
set_need_check_into_view(true);
|
||||
set_current_item(types[idx - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[isShow, types, current_item]
|
||||
);
|
||||
|
||||
const handle_click_down = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (isShow && types && event.code === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
if (!current_item && types.length) {
|
||||
set_current_item(types[0]);
|
||||
}
|
||||
if (current_item) {
|
||||
const idx = types.indexOf(current_item);
|
||||
if (idx < types.length - 1) {
|
||||
set_need_check_into_view(true);
|
||||
set_current_item(types[idx + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[isShow, types, current_item]
|
||||
);
|
||||
|
||||
const handle_click_enter = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (isShow && event.code === 'Enter' && current_item) {
|
||||
event.preventDefault();
|
||||
onSelected && onSelected(current_item);
|
||||
}
|
||||
},
|
||||
[isShow, current_item, onSelected]
|
||||
);
|
||||
|
||||
const handle_key_down = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
handle_click_up(event);
|
||||
handle_click_down(event);
|
||||
handle_click_enter(event);
|
||||
},
|
||||
[handle_click_up, handle_click_down, handle_click_enter]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = hooks
|
||||
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
|
||||
.subscribe(handle_key_down);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}, [hooks, handle_key_down]);
|
||||
|
||||
return isShow ? (
|
||||
<RootContainer
|
||||
ref={menu_ref}
|
||||
onKeyDownCapture={handle_key_down}
|
||||
style={style}
|
||||
>
|
||||
<ContentContainer>
|
||||
<CommonList
|
||||
items={
|
||||
searchBlocks?.map(
|
||||
block => ({ block } as CommonListItem)
|
||||
) || []
|
||||
}
|
||||
onSelected={type => onSelected?.(type)}
|
||||
currentItem={current_item}
|
||||
setCurrentItem={set_current_item}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</RootContainer>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const RootContainer = styled('div')(({ theme }) => ({
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
maxHeight: '525px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
backgroundColor: '#fff',
|
||||
padding: '8px 4px',
|
||||
}));
|
||||
|
||||
const ContentContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
maxHeight: '493px',
|
||||
}));
|
||||
@@ -1,148 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { MuiClickAwayListener, styled } from '@toeverything/components/ui';
|
||||
import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo';
|
||||
import { Point } from '@toeverything/utils';
|
||||
|
||||
import { ReferenceMenuContainer } from './Container';
|
||||
import { QueryBlocks, QueryResult } from '../../search';
|
||||
|
||||
export type ReferenceMenuProps = {
|
||||
editor: Virgo;
|
||||
hooks: PluginHooks;
|
||||
style?: { left: number; top: number };
|
||||
};
|
||||
|
||||
export type RefLinkComponent = {
|
||||
type: 'reflink';
|
||||
reference: string;
|
||||
};
|
||||
|
||||
const BEFORE_REGEX = /\[\[(.*)$/;
|
||||
|
||||
export const ReferenceMenu = ({ editor, hooks, style }: ReferenceMenuProps) => {
|
||||
const [is_show, set_is_show] = useState(false);
|
||||
const [block_id, set_block_id] = useState<string>();
|
||||
const [position, set_position] = useState<Point>(new Point(0, 0));
|
||||
|
||||
const [search_text, set_search_text] = useState<string>('');
|
||||
const [search_blocks, set_search_blocks] = useState<QueryResult>([]);
|
||||
|
||||
useEffect(() => {
|
||||
QueryBlocks(editor, search_text, result => set_search_blocks(result));
|
||||
}, [editor, search_text]);
|
||||
|
||||
const search_block_ids = useMemo(
|
||||
() => Object.values(search_blocks).map(({ id }) => id),
|
||||
[search_blocks]
|
||||
);
|
||||
|
||||
const handle_search = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const { type, anchorNode } = editor.selection.currentSelectInfo;
|
||||
if (
|
||||
type === 'Range' &&
|
||||
anchorNode &&
|
||||
editor.blockHelper.isSelectionCollapsed(anchorNode.id)
|
||||
) {
|
||||
const text = editor.blockHelper.getBlockTextBeforeSelection(
|
||||
anchorNode.id
|
||||
);
|
||||
const matched = BEFORE_REGEX.exec(text)?.[1];
|
||||
|
||||
if (typeof matched === 'string') {
|
||||
if (event.key === '[') set_is_show(true);
|
||||
|
||||
set_block_id(anchorNode.id);
|
||||
set_search_text(matched);
|
||||
|
||||
const rect =
|
||||
editor.selection.currentSelectInfo?.browserSelection
|
||||
?.getRangeAt(0)
|
||||
?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
set_position(new Point(rect.left, rect.top + 24));
|
||||
}
|
||||
} else if (is_show) {
|
||||
set_is_show(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[editor, is_show]
|
||||
);
|
||||
|
||||
const handle_keyup = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => handle_search(event),
|
||||
[handle_search]
|
||||
);
|
||||
|
||||
const handle_key_down = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.code === 'Escape') {
|
||||
set_is_show(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = hooks
|
||||
.get(HookType.ON_ROOT_NODE_KEYUP)
|
||||
.subscribe(handle_keyup);
|
||||
sub.add(
|
||||
hooks
|
||||
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
|
||||
.subscribe(handle_key_down)
|
||||
);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}, [handle_keyup, handle_key_down, hooks]);
|
||||
|
||||
const handle_selected = async (reference: string) => {
|
||||
if (block_id) {
|
||||
const { anchorNode } = editor.selection.currentSelectInfo;
|
||||
editor.blockHelper.insertReference(
|
||||
reference,
|
||||
anchorNode.id,
|
||||
editor.selection.currentSelectInfo?.browserSelection,
|
||||
-search_text.length - 2
|
||||
);
|
||||
}
|
||||
|
||||
set_is_show(false);
|
||||
};
|
||||
|
||||
const handle_close = () => {
|
||||
block_id && editor.blockHelper.removeSearchSlash(block_id);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReferenceMenuWrapper
|
||||
style={{ top: position.y, left: position.x }}
|
||||
onKeyUp={handle_keyup}
|
||||
>
|
||||
<MuiClickAwayListener onClickAway={() => set_is_show(false)}>
|
||||
<div>
|
||||
<ReferenceMenuContainer
|
||||
editor={editor}
|
||||
hooks={hooks}
|
||||
style={style}
|
||||
isShow={is_show && !!search_text}
|
||||
blockId={block_id}
|
||||
onSelected={handle_selected}
|
||||
onClose={handle_close}
|
||||
searchBlocks={search_blocks}
|
||||
types={search_block_ids}
|
||||
/>
|
||||
</div>
|
||||
</MuiClickAwayListener>
|
||||
</ReferenceMenuWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const ReferenceMenuWrapper = styled('div')({
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export { ReferenceMenuPlugin } from './Plugin';
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { BlockPreview } from '@toeverything/components/common';
|
||||
import {
|
||||
TransitionsModal,
|
||||
MuiBox as Box,
|
||||
MuiBox,
|
||||
styled,
|
||||
TransitionsModal,
|
||||
} from '@toeverything/components/ui';
|
||||
import { Virgo, BlockEditor } from '@toeverything/framework/virgo';
|
||||
import { BlockEditor, Virgo } from '@toeverything/framework/virgo';
|
||||
import { throttle } from '@toeverything/utils';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import style9 from 'style9';
|
||||
|
||||
const styles = style9.create({
|
||||
wrapper: {
|
||||
@@ -37,9 +36,7 @@ const query_blocks = (
|
||||
search: string,
|
||||
callback: (result: QueryResult) => void
|
||||
) => {
|
||||
(editor as BlockEditor)
|
||||
.search(search)
|
||||
.then(pages => callback(pages.filter(b => !!b.content)));
|
||||
(editor as BlockEditor).search(search).then(pages => callback(pages));
|
||||
};
|
||||
|
||||
export const QueryBlocks = throttle(query_blocks, 500);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { SvgIconProps } from '../svg-icon';
|
||||
import { BaseButton } from './base-button';
|
||||
|
||||
@@ -35,7 +34,7 @@ const styles = style9.create({
|
||||
|
||||
type ListButtonProps = {
|
||||
className?: string;
|
||||
onClick: () => void;
|
||||
onClick?: () => void;
|
||||
onMouseOver?: () => void;
|
||||
content?: string;
|
||||
children?: () => JSX.Element;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ArrowRightIcon } from '@toeverything/components/icons';
|
||||
import { ReactElement, useRef, useState } from 'react';
|
||||
import { useRef, useState, type ReactElement } from 'react';
|
||||
import { Divider } from '../divider';
|
||||
import {
|
||||
MuiGrow as Grow,
|
||||
|
||||
@@ -35,6 +35,8 @@ export const Popper = ({
|
||||
offset = [0, 5],
|
||||
showArrow = false,
|
||||
popperHandlerRef,
|
||||
onClick,
|
||||
onClickAway,
|
||||
...popperProps
|
||||
}: PopperProps) => {
|
||||
const [anchorEl, setAnchorEl] = useState<VirtualElement>(null);
|
||||
@@ -97,15 +99,20 @@ export const Popper = ({
|
||||
return (
|
||||
<ClickAwayListener
|
||||
onClickAway={() => {
|
||||
setVisible(false);
|
||||
if (visibleControlledByParent) {
|
||||
onClickAway?.();
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
{isAnchorCustom ? null : (
|
||||
<div
|
||||
ref={(dom: HTMLDivElement) => setAnchorEl(dom)}
|
||||
onClick={() => {
|
||||
onClick={e => {
|
||||
if (!hasClickTrigger || visibleControlledByParent) {
|
||||
onClick?.(e);
|
||||
return;
|
||||
}
|
||||
setVisible(!visible);
|
||||
|
||||
@@ -62,4 +62,6 @@ export type PopperProps = {
|
||||
showArrow?: boolean;
|
||||
|
||||
popperHandlerRef?: Ref<PopperHandler>;
|
||||
|
||||
onClickAway?: () => void;
|
||||
} & Omit<PopperUnstyledProps, 'open' | 'ref'>;
|
||||
|
||||
@@ -233,7 +233,10 @@ export const Theme = {
|
||||
},
|
||||
shadows: {
|
||||
none: 'none',
|
||||
shadow1: '0px 1px 5px rgba(152, 172, 189, 0.2)',
|
||||
shadow1:
|
||||
'0px 1px 10px -6px rgba(24, 39, 75, 0.08), 0px 3px 16px -6px rgba(24, 39, 75, 0.04)',
|
||||
shadow2:
|
||||
'0px 6px 16px -8px rgba(0,0,0,0.08), 0px 9px 14px 0px rgba(0,0,0,0.05), 0px 12px 24px 16px rgba(0,0,0,0.03)',
|
||||
},
|
||||
border: ['none'],
|
||||
spacing: {
|
||||
|
||||
Reference in New Issue
Block a user